diff --git a/src/vs/workbench/contrib/notebook/browser/notebookActions.ts b/src/vs/workbench/contrib/notebook/browser/notebookActions.ts index aef2a4fe581d081d147b0b9ae08a03f8d6306f2e..44cc579c362fc6769fd39db5ed3f842e09bf69d4 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookActions.ts @@ -124,7 +124,7 @@ registerAction2(class extends Action2 { let activeCell = editor.getActiveCell(); if (activeCell) { - editor.editNotebookCell(undefined, activeCell); + editor.editNotebookCell(activeCell); } } }); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index ed80b8a57bf1e042f330ba58d80e2a4462ccdebf..eccb3200ec0a29304970b200e24cf1e5f4883b6b 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -16,10 +16,10 @@ export const KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED = new RawContextKey export interface INotebookEditor { viewType: string | undefined; - insertEmptyNotebookCell(index: number | undefined, cell: CellViewModel, type: 'markdown' | 'code', direction: 'above' | 'below'): Promise; - deleteNotebookCell(index: number | undefined, cell: CellViewModel): void; - editNotebookCell(index: number | undefined, cell: CellViewModel): void; - saveNotebookCell(index: number | undefined, cell: CellViewModel): void; + insertEmptyNotebookCell(cell: CellViewModel, type: 'markdown' | 'code', direction: 'above' | 'below'): Promise; + deleteNotebookCell(cell: CellViewModel): void; + editNotebookCell(cell: CellViewModel): void; + saveNotebookCell(cell: CellViewModel): void; focusNotebookCell(cell: CellViewModel, focusEditor: boolean): void; getActiveCell(): CellViewModel | undefined; layoutNotebookCell(cell: CellViewModel, height: number): void; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index cca198a44fac89b855f9614bda85d32e1f60c78a..ccf95891b4b7afe1487584708da0742998f1f1d2 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -38,6 +38,7 @@ import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; import { Emitter, Event } from 'vs/base/common/event'; import { NotebookCellList } from 'vs/workbench/contrib/notebook/browser/notebookCellList'; import { NotebookFindWidget, NotebookFindDelegate, CellFindMatch } from 'vs/workbench/contrib/notebook/browser/notebookFindWidget'; +import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/notebookViewModel'; const $ = DOM.$; const NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'NotebookEditorViewState'; @@ -112,19 +113,14 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor, Noteb static readonly ID: string = 'workbench.editor.notebook'; private rootElement!: HTMLElement; private body!: HTMLElement; - private contentWidgets!: HTMLElement; private webview: BackLayerWebView | null = null; - private list: NotebookCellList | undefined; private control: ICompositeCodeEditor | undefined; private renderedEditors: Map = new Map(); - private model: NotebookEditorModel | undefined; - viewType: string | undefined; - private viewCells: CellViewModel[] = []; + private notebookViewModel: NotebookViewModel | undefined; private localStore: DisposableStore = this._register(new DisposableStore()); private editorMemento: IEditorMemento; private fontInfo: BareFontInfo | undefined; - // private relayoutDisposable: IDisposable | null = null; private dimension: DOM.Dimension | null = null; private editorFocus: IContextKey | null = null; private outputRenderer: OutputRenderer; @@ -157,6 +153,8 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor, Noteb set minimumWidth(value: number) { /*noop*/ } set maximumWidth(value: number) { /*noop*/ } + get viewType() { return this.notebookViewModel?.viewType; } + //#region Editor @@ -184,10 +182,6 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor, Noteb DOM.addClass(this.body, 'cell-list-container'); this.createCellList(); DOM.append(parent, this.body); - - this.contentWidgets = document.createElement('div'); - DOM.addClass(this.contentWidgets, 'notebook-content-widgets'); - DOM.append(this.body, this.contentWidgets); DOM.append(this.body, this.findWidget.getDomNode()); } @@ -255,10 +249,9 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor, Noteb this.list?.splice(0, this.list?.length); - if (this.model && !this.model.isDirty()) { - this.notebookService.destoryNotebookDocument(this.viewType!, this.model!.notebook!); - this.model = undefined; - this.viewType = undefined; + if (this.notebookViewModel && !this.notebookViewModel.isDirty()) { + this.notebookService.destoryNotebookDocument(this.viewType!, this.notebookViewModel!.notebookDocument); + this.notebookViewModel = undefined; } super.onHide(); @@ -267,11 +260,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor, Noteb setVisible(visible: boolean, group?: IEditorGroup): void { super.setVisible(visible, group); if (!visible) { - this.viewCells.forEach(cell => { - if (cell.getText() !== '') { - cell.isEditing = false; - } - }); + this.notebookViewModel?.hide(); } } @@ -280,97 +269,91 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor, Noteb this.editorFocus?.set(true); } - setInput(input: NotebookEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + async setInput(input: NotebookEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { if (this.input instanceof NotebookEditorInput) { this.saveTextEditorViewState(this.input); } - return super.setInput(input, options, token) - .then(() => { - return input.resolve(); - }) - .then(async model => { - if (this.model !== undefined && this.model === model && this.webview !== null) { - return; - } + await super.setInput(input, options, token); + const model = await input.resolve(); - this.localStore.clear(); - this.viewCells.forEach(cell => { - cell.save(); - }); + if (this.notebookViewModel !== undefined && this.notebookViewModel.equal(model) && this.webview !== null) { + return; + } - if (this.webview) { - this.webview?.clearInsets(); - this.webview?.clearPreloadsCache(); - } else { - this.webview = new BackLayerWebView(this.webviewService, this.notebookService, this, this.environmentSerice); - this.list?.rowsContainer.insertAdjacentElement('afterbegin', this.webview!.element); - } + this.detachModel(); + await this.attachModel(input, model); + } - this.model = model; - this.localStore.add(this.model.onDidChangeCells((e) => { - this.updateViewCells(e); - })); + private detachModel() { + this.localStore.clear(); + this.notebookViewModel?.dispose(); + this.notebookViewModel = undefined; + this.webview?.clearInsets(); + this.webview?.clearPreloadsCache(); + } - let viewState = this.loadTextEditorViewState(input); - this.webview.updateRendererPreloads(this.model!.notebook.renderers); - this.viewType = input.viewType; - this.viewCells = await Promise.all(this.model!.notebook!.cells.map(async cell => { - const isEditing = viewState && viewState.editingCells[cell.handle]; - const viewCell = this.instantiationService.createInstance(CellViewModel, input.viewType!, this.model!.notebook!.handle, cell, !!isEditing); - this.localStore.add(viewCell); - return viewCell; - })); + private async attachModel(input: NotebookEditorInput, model: NotebookEditorModel) { + if (!this.webview) { + this.webview = new BackLayerWebView(this.webviewService, this.notebookService, this, this.environmentSerice); + this.list?.rowsContainer.insertAdjacentElement('afterbegin', this.webview!.element); + } - const updateScrollPosition = () => { - const scrollTop = this.list?.scrollTop || 0; - const scrollHeight = this.list?.scrollHeight || 0; - this.webview!.element.style.height = `${scrollHeight}px`; - let updateItems: { cell: CellViewModel, output: IOutput, cellTop: number }[] = []; - - // const date = new Date(); - if (this.webview?.insetMapping) { - this.webview?.insetMapping.forEach((value, key) => { - let cell = value.cell; - let index = this.model!.getNotebook().cells.indexOf(cell.cell); - let cellTop = this.list?.getAbsoluteTop(index) || 0; - if (this.webview!.shouldUpdateInset(cell, key, cellTop)) { - updateItems.push({ - cell: cell, - output: key, - cellTop: cellTop - }); - } - }); + this.notebookViewModel = this.instantiationService.createInstance(NotebookViewModel, input.viewType!, model); + const viewState = this.loadTextEditorViewState(input); + await this.notebookViewModel.initialize(viewState); - if (updateItems.length) { - // console.log('----- did scroll ---- ', date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds()); - this.webview?.updateViewScrollTop(-scrollTop, updateItems); - } - } - }; - this.localStore.add(this.list!.onWillScroll(e => { - // const date = new Date(); - // console.log('----- will scroll ---- ', date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds()); - this.webview!.updateViewScrollTop(-e.scrollTop, []); - })); - this.localStore.add(this.list!.onDidChangeContentHeight(() => updateScrollPosition())); - this.localStore.add(this.list!.onFocusChange((e) => { - if (e.elements.length > 0) { - this.notebookService.updateNotebookActiveCell(input.viewType!, input.resource!, e.elements[0].cell.handle); + this.localStore.add(this.notebookViewModel.onDidChangeCells((e) => { + this.updateViewCells(e); + })); + + this.webview?.updateRendererPreloads(this.notebookViewModel.renderers); + + this.localStore.add(this.list!.onWillScroll(e => { + this.webview!.updateViewScrollTop(-e.scrollTop, []); + })); + + this.localStore.add(this.list!.onDidChangeContentHeight(() => { + const scrollTop = this.list?.scrollTop || 0; + const scrollHeight = this.list?.scrollHeight || 0; + this.webview!.element.style.height = `${scrollHeight}px`; + let updateItems: { cell: CellViewModel, output: IOutput, cellTop: number }[] = []; + + if (this.webview?.insetMapping) { + this.webview?.insetMapping.forEach((value, key) => { + let cell = value.cell; + let index = this.notebookViewModel!.getViewCellIndex(cell); + let cellTop = this.list?.getAbsoluteTop(index) || 0; + if (this.webview!.shouldUpdateInset(cell, key, cellTop)) { + updateItems.push({ + cell: cell, + output: key, + cellTop: cellTop + }); } - })); + }); - this.list?.splice(0, this.list?.length); - this.list?.splice(0, 0, this.viewCells); - this.list?.layout(); - }); + if (updateItems.length) { + this.webview?.updateViewScrollTop(-scrollTop, updateItems); + } + } + })); + + this.localStore.add(this.list!.onFocusChange((e) => { + if (e.elements.length > 0) { + this.notebookService.updateNotebookActiveCell(input.viewType!, input.resource!, e.elements[0].cell.handle); + } + })); + + this.list?.splice(0, this.list?.length); + this.list?.splice(0, 0, this.notebookViewModel!.viewCells); + this.list?.layout(); } private saveTextEditorViewState(input: NotebookEditorInput): void { if (this.group) { let state: { [key: number]: boolean } = {}; - this.viewCells.filter(cell => cell.isEditing).forEach(cell => state[cell.cell.handle] = true); + this.notebookViewModel!.viewCells.filter(cell => cell.isEditing).forEach(cell => state[cell.cell.handle] = true); this.editorMemento.saveEditorState(this.group, input, { editingCells: state }); @@ -406,7 +389,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor, Noteb //#region Find Delegate startFind(value: string): CellFindMatch[] { let matches: CellFindMatch[] = []; - this.viewCells.forEach(cell => { + this.notebookViewModel!.viewCells.forEach(cell => { let cellMatches = cell.startFind(value); matches.push(...cellMatches); }); @@ -419,7 +402,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor, Noteb focusNext(match: CellFindMatch) { let cell = match.cell; - let index = this.viewCells.indexOf(cell); + let index = this.notebookViewModel!.viewCells.indexOf(cell); this.list?.reveal(index); } @@ -437,7 +420,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor, Noteb //#region Cell operations layoutNotebookCell(cell: CellViewModel, height: number) { let relayout = (cell: CellViewModel, height: number) => { - let index = this.model!.getNotebook().cells.indexOf(cell.cell); + let index = this.notebookViewModel!.getViewCellIndex(cell); if (index >= 0) { this.list?.updateElementHeight(index, height); } @@ -445,38 +428,28 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor, Noteb DOM.scheduleAtNextAnimationFrame(() => { relayout(cell, height); - // this.relayoutDisposable = null; }); } updateViewCells(splices: NotebookCellsSplice[]) { - let update = () => { + DOM.scheduleAtNextAnimationFrame(() => { splices.reverse().forEach((diff) => { this.list?.splice(diff[0], diff[1], diff[2].map(cell => { - return this.instantiationService.createInstance(CellViewModel, this.viewType!, this.model!.notebook!.handle, cell, false); + return this.instantiationService.createInstance(CellViewModel, this.viewType!, this.notebookViewModel!.handle, cell, false); })); }); - }; - - DOM.scheduleAtNextAnimationFrame(() => { - update(); }); } - async insertEmptyNotebookCell(listIndex: number | undefined, cell: CellViewModel, type: 'code' | 'markdown', direction: 'above' | 'below'): Promise { - let newLanguages = this.model!.notebook!.languages; - let language = 'markdown'; - if (newLanguages && newLanguages.length) { - language = newLanguages[0]; - } - - let index = listIndex ? listIndex : this.model!.getNotebook().cells.indexOf(cell.cell); + async insertEmptyNotebookCell(cell: CellViewModel, type: 'code' | 'markdown', direction: 'above' | 'below'): Promise { + const newLanguages = this.notebookViewModel!.languages; + const language = newLanguages && newLanguages.length ? newLanguages[0] : 'markdown'; + const index = this.notebookViewModel!.getViewCellIndex(cell); const insertIndex = direction === 'above' ? index : index + 1; + const newModeCell = await this.notebookService.createNotebookCell(this.viewType!, this.notebookViewModel!.uri, insertIndex, language, type); + const newCell = this.instantiationService.createInstance(CellViewModel, this.viewType!, this.notebookViewModel!.handle, newModeCell!, false); - let newModeCell = await this.notebookService.createNotebookCell(this.viewType!, this.model!.notebook!.uri, insertIndex, language, type); - let newCell = this.instantiationService.createInstance(CellViewModel, this.viewType!, this.model!.notebook!.handle, newModeCell!, false); - - this.viewCells!.splice(insertIndex, 0, newCell); + this.notebookViewModel!.insertCell(insertIndex, newCell); this.list?.splice(insertIndex, 0, [newCell]); this.list?.setFocus([insertIndex]); @@ -489,11 +462,18 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor, Noteb }); } - editNotebookCell(listIndex: number | undefined, cell: CellViewModel): void { + async deleteNotebookCell(cell: CellViewModel): Promise { + const index = this.notebookViewModel!.getViewCellIndex(cell); + await this.notebookService.deleteNotebookCell(this.viewType!, this.notebookViewModel!.uri, index); + this.notebookViewModel!.deleteCell(index); + this.list?.splice(index, 1); + } + + editNotebookCell(cell: CellViewModel): void { cell.isEditing = true; } - saveNotebookCell(listIndex: number | undefined, cell: CellViewModel): void { + saveNotebookCell(cell: CellViewModel): void { cell.isEditing = false; } @@ -508,7 +488,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor, Noteb } focusNotebookCell(cell: CellViewModel, focusEditor: boolean) { - let index = this.model!.getNotebook().cells.indexOf(cell.cell); + const index = this.notebookViewModel!.getViewCellIndex(cell); if (focusEditor) { @@ -525,15 +505,6 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor, Noteb this.list?.focusView(); } - async deleteNotebookCell(listIndex: number | undefined, cell: CellViewModel): Promise { - let index = this.model!.getNotebook().cells.indexOf(cell.cell); - - // await this.notebookService.createNotebookCell(this.viewType!, this.model!.notebook!.uri, insertIndex, language, type); - await this.notebookService.deleteNotebookCell(this.viewType!, this.model!.notebook!.uri, index); - this.viewCells!.splice(index, 1); - this.list?.splice(index, 1); - } - //#endregion //#region MISC @@ -555,15 +526,15 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor, Noteb return; } - let preloads = this.model!.notebook!.renderers; + let preloads = this.notebookViewModel!.renderers; if (!this.webview!.insetMapping.has(output)) { - let index = this.model!.getNotebook().cells.indexOf(cell.cell); + let index = this.notebookViewModel!.getViewCellIndex(cell); let cellTop = this.list?.getAbsoluteTop(index) || 0; this.webview!.createInset(cell, output, cellTop, offset, shadowContent, preloads); } else { - let index = this.model!.getNotebook().cells.indexOf(cell.cell); + let index = this.notebookViewModel!.getViewCellIndex(cell); let cellTop = this.list?.getAbsoluteTop(index) || 0; let scrollTop = this.list?.scrollTop || 0; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookViewModel.ts b/src/vs/workbench/contrib/notebook/browser/notebookViewModel.ts new file mode 100644 index 0000000000000000000000000000000000000000..b903aad9dec9962bfbcd6f03ba53a051e90a8a61 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookViewModel.ts @@ -0,0 +1,119 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; +import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/renderers/cellViewModel'; +import { DisposableStore, Disposable } from 'vs/base/common/lifecycle'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { Emitter, Event } from 'vs/base/common/event'; +import { NotebookCellsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellFindMatch } from 'vs/workbench/contrib/notebook/browser/notebookFindWidget'; + +export interface INotebookEditorViewState { + editingCells: { [key: number]: boolean }; +} + +export class NotebookViewModel extends Disposable { + private _localStore: DisposableStore = this._register(new DisposableStore()); + private _viewCells: CellViewModel[] = []; + + get viewCells() { + return this._viewCells; + } + + get notebookDocument() { + return this._model.notebook; + } + + get renderers() { + return this._model.notebook!.renderers; + } + + get handle() { + return this._model.notebook.handle; + } + + get languages() { + return this._model.notebook.languages; + } + + get uri() { + return this._model.notebook.uri; + } + + private readonly _onDidChangeCells = new Emitter(); + get onDidChangeCells(): Event { return this._onDidChangeCells.event; } + + constructor( + public viewType: string, + private _model: NotebookEditorModel, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + + this._register(this._model.onDidChangeCells(e => this._onDidChangeCells.fire(e))); + } + + initialize(viewState: INotebookEditorViewState | undefined) { + this._viewCells = this._model!.notebook!.cells.map(cell => { + const isEditing = viewState && viewState.editingCells[cell.handle]; + const viewCell = this.instantiationService.createInstance(CellViewModel, this.viewType, this._model!.notebook!.handle, cell, !!isEditing); + this._localStore.add(viewCell); + return viewCell; + }); + + return; + } + + isDirty() { + return this._model.isDirty(); + } + + hide() { + this.viewCells.forEach(cell => { + if (cell.getText() !== '') { + cell.isEditing = false; + } + }); + } + + getViewCellIndex(cell: CellViewModel) { + return this.viewCells.indexOf(cell); + } + + find(value: string): CellFindMatch[] { + let matches: CellFindMatch[] = []; + this.viewCells.forEach(cell => { + let cellMatches = cell.startFind(value); + matches.push(...cellMatches); + }); + + return matches; + } + + insertCell(index: number, newCell: CellViewModel) { + this.viewCells!.splice(index, 0, newCell); + this._model.insertCell(newCell.cell, index); + } + + deleteCell(index: number) { + let viewCell = this.viewCells[index]; + this.viewCells.splice(index, 1); + this._model.deleteCell(viewCell.cell); + } + + equal(model: NotebookEditorModel) { + return this._model === model; + } + + dispose() { + this._localStore.clear(); + this._viewCells.forEach(cell => { + cell.save(); + }); + + super.dispose(); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/renderers/cellRenderer.ts index 95cedf60e8ff03fb9fa91654fa339a26acbf70a7..034533c5784310fc05b3f758b945b8f7c71f25eb 100644 --- a/src/vs/workbench/contrib/notebook/browser/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/renderers/cellRenderer.ts @@ -89,7 +89,7 @@ class AbstractCellRenderer { undefined, true, async () => { - await this.notebookEditor.insertEmptyNotebookCell(listIndex, element, 'code', 'above'); + await this.notebookEditor.insertEmptyNotebookCell(element, 'code', 'above'); } ); actions.push(insertAbove); @@ -100,7 +100,7 @@ class AbstractCellRenderer { undefined, true, async () => { - await this.notebookEditor.insertEmptyNotebookCell(listIndex, element, 'code', 'below'); + await this.notebookEditor.insertEmptyNotebookCell(element, 'code', 'below'); } ); actions.push(insertBelow); @@ -111,7 +111,7 @@ class AbstractCellRenderer { undefined, true, async () => { - await this.notebookEditor.insertEmptyNotebookCell(listIndex, element, 'markdown', 'above'); + await this.notebookEditor.insertEmptyNotebookCell(element, 'markdown', 'above'); } ); actions.push(insertMarkdownAbove); @@ -122,7 +122,7 @@ class AbstractCellRenderer { undefined, true, async () => { - await this.notebookEditor.insertEmptyNotebookCell(listIndex, element, 'markdown', 'below'); + await this.notebookEditor.insertEmptyNotebookCell(element, 'markdown', 'below'); } ); actions.push(insertMarkdownBelow); @@ -134,7 +134,7 @@ class AbstractCellRenderer { undefined, true, async () => { - this.notebookEditor.editNotebookCell(listIndex, element); + this.notebookEditor.editNotebookCell(element); } ); @@ -146,7 +146,7 @@ class AbstractCellRenderer { undefined, true, async () => { - this.notebookEditor.saveNotebookCell(listIndex, element); + this.notebookEditor.saveNotebookCell(element); } ); @@ -159,7 +159,7 @@ class AbstractCellRenderer { undefined, true, async () => { - this.notebookEditor.deleteNotebookCell(listIndex, element); + this.notebookEditor.deleteNotebookCell(element); } ); diff --git a/src/vs/workbench/contrib/notebook/browser/renderers/codeCell.ts b/src/vs/workbench/contrib/notebook/browser/renderers/codeCell.ts index 22874bfa369a42583379aa27cd3961a7f541360c..a8f101890f3022e0f1ed9c0d74fc7b5fec7f87c5 100644 --- a/src/vs/workbench/contrib/notebook/browser/renderers/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/renderers/codeCell.ts @@ -104,7 +104,7 @@ export class CodeCell extends Disposable { this._register(templateData.editor!.onDidContentSizeChange((e) => { if (e.contentHeightChanged) { - if (viewCell.editorHeight !== e.contentHeight) { + if (this.viewCell.editorHeight !== e.contentHeight) { templateData.editor?.layout( { width: e.contentWidth, @@ -112,13 +112,13 @@ export class CodeCell extends Disposable { } ); - viewCell.editorHeight = e.contentHeight; + this.viewCell.editorHeight = e.contentHeight; - if (viewCell.outputs.length) { - let outputHeight = viewCell.getOutputTotalHeight(); - notebookEditor.layoutNotebookCell(viewCell, viewCell.editorHeight + 32 + outputHeight); + if (this.viewCell.outputs.length) { + let outputHeight = this.viewCell.getOutputTotalHeight(); + notebookEditor.layoutNotebookCell(this.viewCell, viewCell.editorHeight + 32 + outputHeight); } else { - notebookEditor.layoutNotebookCell(viewCell, viewCell.editorHeight + 32); + notebookEditor.layoutNotebookCell(this.viewCell, viewCell.editorHeight + 32); } } diff --git a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..f5e3efe30ab96495bc954d01abd5ca09a9fa96a2 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts @@ -0,0 +1,165 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { PieceTreeTextBufferFactory } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; +import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/notebookViewModel'; +import { generateCellPath, ICell, INotebook, IOutput, NotebookCellOutputsSplice, NotebookCellsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/renderers/cellViewModel'; + +class MockCell implements ICell { + uri: URI; + private _onDidChangeOutputs = new Emitter(); + onDidChangeOutputs: Event = this._onDidChangeOutputs.event; + private _isDirty: boolean = false; + private _outputs: IOutput[]; + get outputs(): IOutput[] { + return this._outputs; + } + + get isDirty() { + return this._isDirty; + } + + set isDirty(newState: boolean) { + this._isDirty = newState; + + } + + constructor( + public viewType: string, + public handle: number, + public source: string[], + public language: string, + public cell_type: 'markdown' | 'code', + outputs: IOutput[] + ) { + this._outputs = outputs; + this.uri = URI.from({ + scheme: 'vscode-notebook', + authority: viewType, + path: generateCellPath(cell_type, handle), + query: '' + }); + } + + resolveTextBufferFactory(): PieceTreeTextBufferFactory { + throw new Error('Method not implemented.'); + } +} + +class MockNotebook extends Disposable implements INotebook { + private readonly _onDidChangeCells = new Emitter(); + get onDidChangeCells(): Event { return this._onDidChangeCells.event; } + private _onDidChangeDirtyState = new Emitter(); + onDidChangeDirtyState: Event = this._onDidChangeDirtyState.event; + private readonly _onWillDispose: Emitter = this._register(new Emitter()); + readonly onWillDispose: Event = this._onWillDispose.event; + cells: MockCell[]; + activeCell: MockCell | undefined; + languages: string[] = []; + renderers = new Set(); + + + constructor( + public handle: number, + public viewType: string, + public uri: URI + ) { + super(); + + this.cells = []; + } + + save(): Promise { + throw new Error('Method not implemented.'); + } +} + +suite('NotebookViewModel', () => { + const instantiationService = new TestInstantiationService(); + + const createCellViewModel = (viewType: string, notebookHandle: number, cellhandle: number, source: string[], language: string, cell_type: 'markdown' | 'code', outputs: IOutput[]) => { + const mockCell = new MockCell(viewType, cellhandle, source, language, cell_type, outputs); + return instantiationService.createInstance(CellViewModel, viewType, notebookHandle, mockCell, false); + }; + + const withNotebookDocument = (cells: [string[], string, 'markdown' | 'code', IOutput[]][], callback: (viewModel: NotebookViewModel) => void) => { + const viewType = 'notebook'; + const notebook = new MockNotebook(0, viewType, URI.parse('test')); + notebook.cells = cells.map((cell, index) => { + return new MockCell(viewType, index, cell[0], cell[1], cell[2], cell[3]); + }); + const model = new NotebookEditorModel(notebook); + const viewModel = new NotebookViewModel(viewType, model, instantiationService); + viewModel.initialize(undefined); + + callback(viewModel); + + viewModel.dispose(); + return; + }; + + test('ctor', function () { + const notebook = new MockNotebook(0, 'notebook', URI.parse('test')); + const model = new NotebookEditorModel(notebook); + const viewModel = new NotebookViewModel('notebook', model, instantiationService); + assert.equal(viewModel.viewType, 'notebook'); + }); + + test('insert/delete', function () { + withNotebookDocument( + [ + [['var a = 1;'], 'javascript', 'code', []], + [['var b = 2;'], 'javascript', 'code', []] + ], + (viewModel) => { + const cell = createCellViewModel(viewModel.viewType, viewModel.handle, 0, ['var c = 3;'], 'javascript', 'code', []); + viewModel.insertCell(1, cell); + assert.equal(viewModel.viewCells.length, 3); + assert.equal(viewModel.notebookDocument.cells.length, 3); + assert.equal(viewModel.getViewCellIndex(cell), 1); + + viewModel.deleteCell(1); + assert.equal(viewModel.viewCells.length, 2); + assert.equal(viewModel.notebookDocument.cells.length, 2); + assert.equal(viewModel.getViewCellIndex(cell), -1); + } + ); + }); + + test('index', function () { + withNotebookDocument( + [ + [['var a = 1;'], 'javascript', 'code', []], + [['var b = 2;'], 'javascript', 'code', []] + ], + (viewModel) => { + const firstViewCell = viewModel.viewCells[0]; + const lastViewCell = viewModel.viewCells[viewModel.viewCells.length - 1]; + + const insertIndex = viewModel.getViewCellIndex(firstViewCell) + 1; + const cell = createCellViewModel(viewModel.viewType, viewModel.handle, 3, ['var c = 3;'], 'javascript', 'code', []); + viewModel.insertCell(insertIndex, cell); + + const addedCellIndex = viewModel.getViewCellIndex(cell); + viewModel.deleteCell(addedCellIndex); + + const secondInsertIndex = viewModel.getViewCellIndex(lastViewCell) + 1; + const cell2 = createCellViewModel(viewModel.viewType, viewModel.handle, 4, ['var d = 4;'], 'javascript', 'code', []); + viewModel.insertCell(secondInsertIndex, cell2); + + assert.equal(viewModel.viewCells.length, 3); + assert.equal(viewModel.notebookDocument.cells.length, 3); + assert.equal(viewModel.getViewCellIndex(cell2), 2); + } + ); + }); +});