diff --git a/extensions/vscode-notebook-tests/src/notebook.test.ts b/extensions/vscode-notebook-tests/src/notebook.test.ts index e07bb1a91258..a48052541e93 100644 --- a/extensions/vscode-notebook-tests/src/notebook.test.ts +++ b/extensions/vscode-notebook-tests/src/notebook.test.ts @@ -56,7 +56,32 @@ async function splitEditor() { await once; } -suite('API tests', () => { +suite('Notebook API tests', () => { + // test.only('crash', async function () { + // for (let i = 0; i < 200; i++) { + // let resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + // await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + // await vscode.commands.executeCommand('workbench.action.revertAndCloseActiveEditor'); + + // resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './empty.vsctestnb')); + // await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + // await vscode.commands.executeCommand('workbench.action.revertAndCloseActiveEditor'); + // } + // }); + + // test.only('crash', async function () { + // for (let i = 0; i < 200; i++) { + // let resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + // await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + // await vscode.commands.executeCommand('workbench.action.files.save'); + // await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + // resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './empty.vsctestnb')); + // await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + // await vscode.commands.executeCommand('workbench.action.files.save'); + // await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + // } + // }); + test('document open/close event', async function () { const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); const firstDocumentOpen = getEventOncePromise(vscode.notebook.onDidOpenNotebookDocument); @@ -222,6 +247,13 @@ suite('API tests', () => { await vscode.commands.executeCommand('workbench.action.files.save'); await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + const firstEditor = vscode.notebook.activeNotebookEditor; + assert.equal(firstEditor?.document.cells.length, 1); + + await vscode.commands.executeCommand('workbench.action.files.save'); + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); }); test('notebook editor active/visible', async function () { @@ -290,7 +322,7 @@ suite('API tests', () => { assert.equal(cellChangeEventRet.changes[0].items[0], vscode.notebook.activeNotebookEditor!.document.cells[1]); await vscode.commands.executeCommand('workbench.action.files.save'); - await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); }); test('initialzation should not emit cell change events.', async function () { @@ -766,6 +798,9 @@ suite('metadata', () => { assert.equal(vscode.notebook.activeNotebookEditor!.document.metadata.custom!['testMetadata'] as boolean, false); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.metadata.custom!['testCellMetadata'] as number, 123); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.language, 'typescript'); + + await vscode.commands.executeCommand('workbench.action.files.saveAll'); + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); }); @@ -781,6 +816,9 @@ suite('metadata', () => { const activeCell = vscode.notebook.activeNotebookEditor!.selection; assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.indexOf(activeCell!), 1); assert.equal(activeCell?.metadata.custom!['testCellMetadata'] as number, 123); + + await vscode.commands.executeCommand('workbench.action.files.saveAll'); + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); }); }); @@ -808,7 +846,7 @@ suite('regression', () => { await vscode.commands.executeCommand('vscode.openWith', resource, 'default'); assert.equal(vscode.window.activeTextEditor?.document.uri.path, resource.path); - await vscode.commands.executeCommand('workbench.action.files.saveAll'); + await vscode.commands.executeCommand('workbench.action.revertAndCloseActiveEditor'); await vscode.commands.executeCommand('workbench.action.closeAllEditors'); }); @@ -824,7 +862,7 @@ suite('regression', () => { assert.notEqual(vscode.notebook.activeNotebookEditor, undefined, 'notebook first'); assert.notEqual(vscode.window.activeTextEditor, undefined); - // await vscode.commands.executeCommand('workbench.action.files.saveAll'); + await vscode.commands.executeCommand('workbench.action.revertAndCloseActiveEditor'); await vscode.commands.executeCommand('workbench.action.closeAllEditors'); }); diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 57f43f68be6c..5bb65b9adf55 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1762,7 +1762,7 @@ declare module 'vscode' { resolveNotebook(document: NotebookDocument, webview: NotebookCommunication): Promise; saveNotebook(document: NotebookDocument, cancellation: CancellationToken): Promise; saveNotebookAs(targetResource: Uri, document: NotebookDocument, cancellation: CancellationToken): Promise; - readonly onDidChangeNotebook: Event; + readonly onDidChangeNotebook: Event; backupNotebook(document: NotebookDocument, context: NotebookDocumentBackupContext, cancellation: CancellationToken): Promise; kernel?: NotebookKernel; diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index e14517d01652..6ea53df612d5 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; +import * as DOM from 'vs/base/browser/dom'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IExtHostContext, ExtHostNotebookShape, ExtHostContext, INotebookDocumentsAndEditorsDelta, INotebookModelAddedData } from '../common/extHost.protocol'; import { Disposable, IDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; @@ -40,7 +41,7 @@ export class MainThreadNotebookDocument extends Disposable { ) { super(); - this._textModel = new NotebookTextModel(handle, viewType, supportBackup, uri); + this._textModel = new NotebookTextModel(handle, viewType, supportBackup, uri, undoRedoService); this._register(this._textModel.onDidModelChangeProxy(e => { this._proxy.$acceptModelChanged(this.uri, e); this._proxy.$acceptEditorPropertiesChanged(uri, { selections: { selections: this._textModel.selections }, metadata: null }); @@ -51,9 +52,18 @@ export class MainThreadNotebookDocument extends Disposable { })); } - async applyEdit(modelVersionId: number, edits: ICellEditOperation[], emitToExtHost: boolean): Promise { + async applyEdit(modelVersionId: number, edits: ICellEditOperation[], emitToExtHost: boolean, synchronous: boolean): Promise { await this.notebookService.transformEditsOutputs(this.textModel, edits); - return this._textModel.$applyEdit(modelVersionId, edits); + if (synchronous) { + return this._textModel.$applyEdit(modelVersionId, edits, emitToExtHost, synchronous); + } else { + return new Promise(resolve => { + this._register(DOM.scheduleAtNextAnimationFrame(() => { + const ret = this._textModel.$applyEdit(modelVersionId, edits, emitToExtHost, true); + resolve(ret); + })); + }); + } } async spliceNotebookCellOutputs(cellHandle: number, splices: NotebookCellOutputsSplice[]) { @@ -528,7 +538,7 @@ export class MainThreadNotebookController implements IMainNotebookController { await mainthreadNotebook.applyEdit(mainthreadNotebook.textModel.versionId, [ { editType: CellEditType.Delete, count: mainthreadNotebook.textModel.cells.length, index: 0 }, { editType: CellEditType.Insert, index: 0, cells: data.cells } - ], true); + ], true, false); } return mainthreadNotebook.textModel; } @@ -548,7 +558,7 @@ export class MainThreadNotebookController implements IMainNotebookController { index: 0, cells: backup.cells || [] } - ], false); + ], false, true); // create document in ext host with cells data await this._mainThreadNotebook.addNotebookDocument({ @@ -625,7 +635,7 @@ export class MainThreadNotebookController implements IMainNotebookController { let mainthreadNotebook = this._mapping.get(URI.from(resource).toString()); if (mainthreadNotebook) { - return await mainthreadNotebook.applyEdit(modelVersionId, edits, true); + return await mainthreadNotebook.applyEdit(modelVersionId, edits, true, true); } return false; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index cbb12ffcb21e..983b7ee46d69 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -674,7 +674,7 @@ async function moveCell(context: INotebookCellActionContext, direction: 'up' | ' if (result) { // move cell command only works when the cell container has focus - await context.notebookEditor.focusNotebookCell(context.cell, 'container'); + await context.notebookEditor.focusNotebookCell(result, 'container'); } } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 070883566b53..c516338caa6f 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -210,17 +210,17 @@ export interface INotebookEditor extends IEditor { /** * Move a cell up one spot */ - moveCellUp(cell: ICellViewModel): Promise; + moveCellUp(cell: ICellViewModel): Promise; /** * Move a cell down one spot */ - moveCellDown(cell: ICellViewModel): Promise; + moveCellDown(cell: ICellViewModel): Promise; /** * Move a cell above or below another cell */ - moveCell(cell: ICellViewModel, relativeToCell: ICellViewModel, direction: 'above' | 'below'): Promise; + moveCell(cell: ICellViewModel, relativeToCell: ICellViewModel, direction: 'above' | 'below'): Promise; /** * Focus the container of a cell (the monaco editor inside is not focused). @@ -371,6 +371,7 @@ export interface INotebookEditor extends IEditor { } export interface INotebookCellList { + isDisposed: boolean readonly contextKeyService: IContextKeyService; elementAt(position: number): ICellViewModel | undefined; elementHeight(element: ICellViewModel): number; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 560aadb5affe..d17b20c7efc0 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -6,7 +6,7 @@ import * as DOM from 'vs/base/browser/dom'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; -import { MutableDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -30,7 +30,7 @@ export class NotebookEditor extends BaseEditor { static readonly ID: string = 'workbench.editor.notebook'; private readonly _editorMemento: IEditorMemento; - private readonly _groupListener = this._register(new MutableDisposable()); + private readonly _groupListener = this._register(new DisposableStore()); private readonly _widgetDisposableStore: DisposableStore = new DisposableStore(); private _widget: IBorrowValue = { value: undefined }; private _rootElement!: HTMLElement; @@ -49,13 +49,13 @@ export class NotebookEditor extends BaseEditor { @IInstantiationService private readonly instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @IEditorService private readonly _editorService: IEditorService, - @IEditorGroupsService editorGroupService: IEditorGroupsService, + @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, @IEditorDropService private readonly _editorDropService: IEditorDropService, @INotificationService private readonly _notificationService: INotificationService, @INotebookEditorWidgetService private readonly _notebookWidgetService: INotebookEditorWidgetService, ) { super(NotebookEditor.ID, telemetryService, themeService, storageService); - this._editorMemento = this.getEditorMemento(editorGroupService, NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY); + this._editorMemento = this.getEditorMemento(_editorGroupService, NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY); } set viewModel(newModel: NotebookViewModel | undefined) { @@ -100,7 +100,14 @@ export class NotebookEditor extends BaseEditor { setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { super.setEditorVisible(visible, group); - this._groupListener.value = group?.onWillCloseEditor(e => this._saveEditorViewState(e.editor)); + if (group) { + this._groupListener.add(group.onWillCloseEditor(e => this._saveEditorViewState(e.editor))); + this._groupListener.add(group.onDidGroupChange(() => { + if (this._editorGroupService.activeGroup !== group) { + this._widget?.value?.updateEditorFocus(); + } + })); + } if (!visible) { this._saveEditorViewState(this.input); @@ -197,6 +204,10 @@ export class NotebookEditor extends BaseEditor { private _saveEditorViewState(input: IEditorInput | undefined): void { if (this.group && this._widget.value && input instanceof NotebookEditorInput) { + if (this._widget.value.isDisposed) { + return; + } + const state = this._widget.value.getEditorViewState(); this._editorMemento.saveEditorState(this.group, input.resource, state); } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 8757ae8b75e2..baa5b0969613 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -105,10 +105,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return this._isDisposed; } - private readonly _onDidChangeModel = new Emitter(); + private readonly _onDidChangeModel = this._register(new Emitter()); readonly onDidChangeModel: Event = this._onDidChangeModel.event; - private readonly _onDidFocusEditorWidget = new Emitter(); + private readonly _onDidFocusEditorWidget = this._register(new Emitter()); readonly onDidFocusEditorWidget = this._onDidFocusEditorWidget.event; set viewModel(newModel: NotebookViewModel | undefined) { @@ -129,7 +129,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } private _activeKernel: INotebookKernelInfo | undefined = undefined; - private readonly _onDidChangeKernel = new Emitter(); + private readonly _onDidChangeKernel = this._register(new Emitter()); readonly onDidChangeKernel: Event = this._onDidChangeKernel.event; get activeKernel() { @@ -217,7 +217,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor updateEditorFocus() { // Note - focus going to the webview will fire 'blur', but the webview element will be // a descendent of the notebook editor root. - this._editorFocus?.set(DOM.isAncestor(document.activeElement, this._overlayContainer)); + const focused = DOM.isAncestor(document.activeElement, this._overlayContainer); + this._editorFocus?.set(focused); + this._notebookViewModel?.setFocus(focused); } hasFocus() { @@ -700,14 +702,13 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor const focus = this._list.getFocus()[0]; if (typeof focus === 'number') { const element = this._notebookViewModel!.viewCells[focus]; - const itemDOM = this._list?.domElementOfElement(element!); - let editorFocused = false; - if (document.activeElement && itemDOM && itemDOM.contains(document.activeElement)) { - editorFocused = true; - } + if (element) { + const itemDOM = this._list?.domElementOfElement(element); + let editorFocused = !!(document.activeElement && itemDOM && itemDOM.contains(document.activeElement)); - state.editorFocused = editorFocused; - state.focus = focus; + state.editorFocused = editorFocused; + state.focus = focus; + } } } @@ -906,7 +907,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor (direction === 'above' ? index : nextIndex) : index; const newCell = this._notebookViewModel!.createCell(insertIndex, initialText.split(/\r?\n/g), language, type, cell?.metadata, true); - return newCell; + return newCell as CellViewModel; } async splitNotebookCell(cell: ICellViewModel): Promise { @@ -946,41 +947,41 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return true; } - async moveCellDown(cell: ICellViewModel): Promise { + async moveCellDown(cell: ICellViewModel): Promise { if (!this._notebookViewModel!.metadata.editable) { - return false; + return null; } const index = this._notebookViewModel!.getCellIndex(cell); if (index === this._notebookViewModel!.length - 1) { - return false; + return null; } const newIdx = index + 1; return this._moveCellToIndex(index, newIdx); } - async moveCellUp(cell: ICellViewModel): Promise { + async moveCellUp(cell: ICellViewModel): Promise { if (!this._notebookViewModel!.metadata.editable) { - return false; + return null; } const index = this._notebookViewModel!.getCellIndex(cell); if (index === 0) { - return false; + return null; } const newIdx = index - 1; return this._moveCellToIndex(index, newIdx); } - async moveCell(cell: ICellViewModel, relativeToCell: ICellViewModel, direction: 'above' | 'below'): Promise { + async moveCell(cell: ICellViewModel, relativeToCell: ICellViewModel, direction: 'above' | 'below'): Promise { if (!this._notebookViewModel!.metadata.editable) { - return false; + return null; } if (cell === relativeToCell) { - return false; + return null; } const originalIdx = this._notebookViewModel!.getCellIndex(cell); @@ -994,23 +995,24 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return this._moveCellToIndex(originalIdx, newIdx); } - private async _moveCellToIndex(index: number, newIdx: number): Promise { + private async _moveCellToIndex(index: number, newIdx: number): Promise { if (index === newIdx) { - return false; + return null; } if (!this._notebookViewModel!.moveCellToIdx(index, newIdx, true)) { throw new Error('Notebook Editor move cell, index out of range'); } - let r: (val: boolean) => void; + let r: (val: ICellViewModel | null) => void; DOM.scheduleAtNextAnimationFrame(() => { if (this._isDisposed) { - r(false); + r(null); } - this._list?.revealElementInView(this._notebookViewModel!.viewCells[newIdx]); - r(true); + const viewCell = this._notebookViewModel!.viewCells[newIdx]; + this._list?.revealElementInView(viewCell); + r(viewCell); }); return new Promise(resolve => { r = resolve; }); @@ -1262,6 +1264,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor dispose() { this._isDisposed = true; + // dispose webview first + this._webview?.dispose(); + this.notebookService.removeNotebookEditor(this); const keys = Object.keys(this._contributions); for (let i = 0, len = keys.length; i < len; i++) { @@ -1271,7 +1276,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._localStore.clear(); this._list?.dispose(); - this._webview?.dispose(); this._overlayContainer.remove(); this.viewModel?.dispose(); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetService.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetService.ts index 1ae1d6808796..2369eee6a024 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetService.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetService.ts @@ -92,8 +92,9 @@ class NotebookEditorWidgetService implements INotebookEditorWidgetService { private _disposeWidget(widget: NotebookEditorWidget): void { widget.onWillHide(); - widget.getDomNode().remove(); + const domNode = widget.getDomNode(); widget.dispose(); + domNode.remove(); } private _freeWidget(input: NotebookEditorInput, source: IEditorGroup, target: IEditorGroup): void { diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index 1093257a694e..5d0acd82ce32 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -45,6 +45,12 @@ export class NotebookCellList extends WorkbenchList implements ID private _hiddenRangeIds: string[] = []; private hiddenRangesPrefixSum: PrefixSumComputer | null = null; + private _isDisposed = false; + + get isDisposed() { + return this._isDisposed; + } + constructor( private listUser: string, container: HTMLElement, @@ -162,6 +168,10 @@ export class NotebookCellList extends WorkbenchList implements ID attachViewModel(model: NotebookViewModel) { this._viewModel = model; this._viewModelStore.add(model.onDidChangeViewCells((e) => { + if (this._isDisposed) { + return; + } + const currentRanges = this._hiddenRangeIds.map(id => this._viewModel!.getTrackedRange(id)).filter(range => range !== null) as ICellRange[]; const newVisibleViewCells: CellViewModel[] = getVisibleCells(this._viewModel!.viewCells as CellViewModel[], currentRanges); @@ -197,7 +207,11 @@ export class NotebookCellList extends WorkbenchList implements ID deletedOutputs.forEach(output => this._onDidRemoveOutput.fire(output)); }); } else { - DOM.scheduleAtNextAnimationFrame(() => { + this._viewModelStore.add(DOM.scheduleAtNextAnimationFrame(() => { + if (this._isDisposed) { + return; + } + viewDiffs.reverse().forEach((diff) => { const hideOutputs: IProcessedOutput[] = []; const deletedOutputs: IProcessedOutput[] = []; @@ -216,7 +230,7 @@ export class NotebookCellList extends WorkbenchList implements ID hideOutputs.forEach(output => this._onDidHideOutput.fire(output)); deletedOutputs.forEach(output => this._onDidRemoveOutput.fire(output)); }); - }); + })); } })); @@ -490,7 +504,7 @@ export class NotebookCellList extends WorkbenchList implements ID domElementOfElement(element: ICellViewModel): HTMLElement | null { const index = this._getViewIndexUpperBound(element); - if (index !== undefined) { + if (index !== undefined && index >= 0) { return this.view.domElement(index); } @@ -518,7 +532,7 @@ export class NotebookCellList extends WorkbenchList implements ID updateElementHeight2(element: ICellViewModel, size: number): void { const index = this._getViewIndexUpperBound(element); - if (index === undefined) { + if (index === undefined || index < 0 || index >= this.length) { return; } @@ -874,6 +888,7 @@ export class NotebookCellList extends WorkbenchList implements ID } dispose() { + this._isDisposed = true; this._viewModelStore.dispose(); this._localDisposableStore.dispose(); super.dispose(); diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts index 4cfc80e8ef11..360dce577041 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts @@ -61,7 +61,12 @@ class RichRenderer implements IOutputTransformContribution { let mimeTypesMessage = mimeTypes.join(', '); - contentNode.innerText = `No renderer could be found for output. It has the following MIME types: ${mimeTypesMessage}`; + if (preferredMimeType) { + contentNode.innerText = `No renderer could be found for MIME type: ${preferredMimeType}`; + } else { + contentNode.innerText = `No renderer could be found for output. It has the following MIME types: ${mimeTypesMessage}`; + } + container.appendChild(contentNode); return { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 220e3d823f35..aa3e65bda099 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -340,11 +340,19 @@ ${loaderJs} } async initialize(content: string) { + if (!document.body.contains(this.element)) { + throw new Error('Element is already detached from the DOM tree'); + } + this.webview = this._createInset(this.webviewService, content); this.webview.mountTo(this.element); this._register(this.webview); this._register(this.webview.onDidClickLink(link => { + if (this._disposed) { + return; + } + if (!link) { return; } @@ -356,6 +364,10 @@ ${loaderJs} })); this._register(this.webview.onDidReload(() => { + if (this._disposed) { + return; + } + this.preloadsCache.clear(); for (const [output, inset] of this.insetMapping.entries()) { this.updateRendererPreloads(inset.preloads); @@ -364,6 +376,10 @@ ${loaderJs} })); this._register(this.webview.onMessage((data: FromWebviewMessage) => { + if (this._disposed) { + return; + } + if (data.__vscode_notebook_message) { if (data.type === 'dimension') { let height = data.data.height; @@ -739,6 +755,10 @@ ${loaderJs} } private _sendMessageToWebview(message: ToWebviewMessage) { + if (this._disposed) { + return; + } + this.webview.postMessage(message); } @@ -748,6 +768,7 @@ ${loaderJs} dispose() { this._disposed = true; + this.webview.dispose(); super.dispose(); } } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index 666a56cc00d8..81abf3422817 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -90,14 +90,17 @@ export abstract class BaseCellViewModel extends Disposable { options: model.IModelDeltaDecoration; }>(); private _lastDecorationId: number = 0; - protected _textModel?: model.ITextModel; get textModel(): model.ITextModel | undefined { - return this._textModel; + return this.model.textModel; + } + + set textModel(m: model.ITextModel | undefined) { + this.model.textModel = m; } hasModel(): this is IEditableCellViewModel { - return !!this._textModel; + return !!this.model.textModel; } private _dragging: boolean = false; @@ -127,7 +130,7 @@ export abstract class BaseCellViewModel extends Disposable { abstract onDeselect(): void; assertTextModelAttached(): boolean { - if (this._textModel && this._textEditor && this._textEditor.getModel() === this._textModel) { + if (this.textModel && this._textEditor && this._textEditor.getModel() === this.textModel) { return true; } @@ -148,7 +151,7 @@ export abstract class BaseCellViewModel extends Disposable { } this._textEditor = editor; - this._textModel = this._textEditor.getModel() || undefined; + this.textModel = this._textEditor.getModel() || undefined; if (this._editorViewStates) { this._restoreViewState(this._editorViewStates); @@ -183,7 +186,7 @@ export abstract class BaseCellViewModel extends Disposable { }); this._textEditor = undefined; - this._textModel = undefined; + this.textModel = undefined; this._cursorChangeListener?.dispose(); this._cursorChangeListener = null; this._onDidChangeEditorAttachState.fire(); @@ -315,7 +318,7 @@ export abstract class BaseCellViewModel extends Disposable { } const firstViewLineTop = this._textEditor.getTopForPosition(1, 1); - const lastViewLineTop = this._textEditor.getTopForPosition(this._textModel!.getLineCount(), this._textModel!.getLineLength(this._textModel!.getLineCount())); + const lastViewLineTop = this._textEditor.getTopForPosition(this.textModel!.getLineCount(), this.textModel!.getLineLength(this.textModel!.getLineCount())); const selectionTop = this._textEditor.getTopForPosition(selection.startLineNumber, selection.startColumn); if (selectionTop === lastViewLineTop) { @@ -343,7 +346,7 @@ export abstract class BaseCellViewModel extends Disposable { let cellMatches: model.FindMatch[] = []; if (this.assertTextModelAttached()) { - cellMatches = this._textModel!.findMatches(value, false, false, false, null, false); + cellMatches = this.textModel!.findMatches(value, false, false, false, null, false); } else { const lineCount = this.textBuffer.getLineCount(); const fullRange = new Range(1, 1, lineCount, this.textBuffer.getLineLength(lineCount) + 1); diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts index 030188a6a8fa..709c41859b2d 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts @@ -5,181 +5,24 @@ import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; -import { ICell, CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IResourceUndoRedoElement, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo'; import { URI } from 'vs/base/common/uri'; import { BaseCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel'; -import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { CellFocusMode } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { ITextCellEditingDelegate } from 'vs/workbench/contrib/notebook/common/model/cellEdit'; -/** - * It should not modify Undo/Redo stack - */ -export interface ICellEditingDelegate { - insertCell?(index: number, viewCell: BaseCellViewModel): void; - deleteCell?(index: number): void; - moveCell?(fromIndex: number, toIndex: number): void; - createCellViewModel?(cell: ICell): BaseCellViewModel; + +export interface IViewCellEditingDelegate extends ITextCellEditingDelegate { + createCellViewModel?(cell: NotebookCellTextModel): BaseCellViewModel; createCell?(index: number, source: string | string[], language: string, type: CellKind): BaseCellViewModel; - setSelections(selections: number[]): void; -} - -export class InsertCellEdit implements IResourceUndoRedoElement { - type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; - label: string = 'Insert Cell'; - constructor( - public resource: URI, - private insertIndex: number, - private cell: BaseCellViewModel, - private editingDelegate: ICellEditingDelegate, - private beforedSelections: number[], - private endSelections: number[] - ) { - } - - undo(): void | Promise { - if (!this.editingDelegate.deleteCell) { - throw new Error('Notebook Delete Cell not implemented for Undo/Redo'); - } - - this.editingDelegate.deleteCell(this.insertIndex); - this.editingDelegate.setSelections(this.beforedSelections); - } - redo(): void | Promise { - if (!this.editingDelegate.insertCell) { - throw new Error('Notebook Insert Cell not implemented for Undo/Redo'); - } - - this.editingDelegate.insertCell(this.insertIndex, this.cell); - this.editingDelegate.setSelections(this.endSelections); - } -} - -export class DeleteCellEdit implements IResourceUndoRedoElement { - type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; - label: string = 'Delete Cell'; - - private _rawCell: ICell; - constructor( - public resource: URI, - private insertIndex: number, - cell: BaseCellViewModel, - private editingDelegate: ICellEditingDelegate, - private beforedSelections: number[], - private endSelections: number[] - ) { - this._rawCell = cell.model; - - // save inmem text to `ICell` - // no needed any more as the text buffer is transfered to `raw_cell` - // this._rawCell.source = [cell.getText()]; - } - - undo(): void | Promise { - if (!this.editingDelegate.insertCell || !this.editingDelegate.createCellViewModel) { - throw new Error('Notebook Insert Cell not implemented for Undo/Redo'); - } - - const cell = this.editingDelegate.createCellViewModel(this._rawCell); - this.editingDelegate.insertCell(this.insertIndex, cell); - this.editingDelegate.setSelections(this.beforedSelections); - } - - redo(): void | Promise { - if (!this.editingDelegate.deleteCell) { - throw new Error('Notebook Delete Cell not implemented for Undo/Redo'); - } - - this.editingDelegate.deleteCell(this.insertIndex); - this.editingDelegate.setSelections(this.endSelections); - } -} - -export class MoveCellEdit implements IResourceUndoRedoElement { - type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; - label: string = 'Delete Cell'; - - constructor( - public resource: URI, - private fromIndex: number, - private toIndex: number, - private editingDelegate: ICellEditingDelegate, - private beforedSelections: number[], - private endSelections: number[] - ) { - } - - undo(): void | Promise { - if (!this.editingDelegate.moveCell) { - throw new Error('Notebook Move Cell not implemented for Undo/Redo'); - } - - this.editingDelegate.moveCell(this.toIndex, this.fromIndex); - this.editingDelegate.setSelections(this.beforedSelections); - } - - redo(): void | Promise { - if (!this.editingDelegate.moveCell) { - throw new Error('Notebook Move Cell not implemented for Undo/Redo'); - } - - this.editingDelegate.moveCell(this.fromIndex, this.toIndex); - this.editingDelegate.setSelections(this.endSelections); - } -} - -export class SpliceCellsEdit implements IResourceUndoRedoElement { - type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; - label: string = 'Insert Cell'; - constructor( - public resource: URI, - private diffs: [number, CellViewModel[], CellViewModel[]][], - private editingDelegate: ICellEditingDelegate, - private beforeHandles: number[], - private endHandles: number[] - ) { - } - - undo(): void | Promise { - if (!this.editingDelegate.deleteCell || !this.editingDelegate.insertCell) { - throw new Error('Notebook Insert/Delete Cell not implemented for Undo/Redo'); - } - - this.diffs.forEach(diff => { - for (let i = 0; i < diff[2].length; i++) { - this.editingDelegate.deleteCell!(diff[0]); - } - - diff[1].reverse().forEach(cell => { - this.editingDelegate.insertCell!(diff[0], cell); - }); - }); - this.editingDelegate.setSelections(this.beforeHandles); - } - - redo(): void | Promise { - if (!this.editingDelegate.deleteCell || !this.editingDelegate.insertCell) { - throw new Error('Notebook Insert/Delete Cell not implemented for Undo/Redo'); - } - - this.diffs.reverse().forEach(diff => { - for (let i = 0; i < diff[1].length; i++) { - this.editingDelegate.deleteCell!(diff[0]); - } - - diff[2].reverse().forEach(cell => { - this.editingDelegate.insertCell!(diff[0], cell); - }); - }); - - this.editingDelegate.setSelections(this.endHandles); - } } export class JoinCellEdit implements IResourceUndoRedoElement { type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; label: string = 'Join Cell'; - private _deletedRawCell: ICell; + private _deletedRawCell: NotebookCellTextModel; constructor( public resource: URI, private index: number, @@ -189,7 +32,7 @@ export class JoinCellEdit implements IResourceUndoRedoElement { private inverseRange: Range, private insertContent: string, private removedCell: BaseCellViewModel, - private editingDelegate: ICellEditingDelegate, + private editingDelegate: IViewCellEditingDelegate, ) { this._deletedRawCell = this.removedCell.model; } @@ -209,12 +52,12 @@ export class JoinCellEdit implements IResourceUndoRedoElement { const cell = this.editingDelegate.createCellViewModel(this._deletedRawCell); if (this.direction === 'above') { - this.editingDelegate.insertCell(this.index, cell); - this.editingDelegate.setSelections([cell.handle]); + this.editingDelegate.insertCell(this.index, this._deletedRawCell); + this.editingDelegate.emitSelections([cell.handle]); cell.focusMode = CellFocusMode.Editor; } else { - this.editingDelegate.insertCell(this.index, cell); - this.editingDelegate.setSelections([this.cell.handle]); + this.editingDelegate.insertCell(this.index, cell.model); + this.editingDelegate.emitSelections([this.cell.handle]); this.cell.focusMode = CellFocusMode.Editor; } } @@ -230,7 +73,7 @@ export class JoinCellEdit implements IResourceUndoRedoElement { ]); this.editingDelegate.deleteCell(this.index); - this.editingDelegate.setSelections([this.cell.handle]); + this.editingDelegate.emitSelections([this.cell.handle]); this.cell.focusMode = CellFocusMode.Editor; } } @@ -247,13 +90,13 @@ export class SplitCellEdit implements IResourceUndoRedoElement { private cellContents: string[], private language: string, private cellKind: CellKind, - private editingDelegate: ICellEditingDelegate + private editingDelegate: IViewCellEditingDelegate ) { } async undo(): Promise { - if (!this.editingDelegate.deleteCell || !this.editingDelegate.createCellViewModel) { + if (!this.editingDelegate.deleteCell) { throw new Error('Notebook Delete Cell not implemented for Undo/Redo'); } @@ -270,12 +113,12 @@ export class SplitCellEdit implements IResourceUndoRedoElement { this.editingDelegate.deleteCell(this.index + 1); } - this.editingDelegate.setSelections([this.cell.handle]); + this.editingDelegate.emitSelections([this.cell.handle]); this.cell.focusMode = CellFocusMode.Editor; } async redo(): Promise { - if (!this.editingDelegate.insertCell || !this.editingDelegate.createCell) { + if (!this.editingDelegate.createCell) { throw new Error('Notebook Insert Cell not implemented for Undo/Redo'); } @@ -291,7 +134,7 @@ export class SplitCellEdit implements IResourceUndoRedoElement { } if (lastCell) { - this.editingDelegate.setSelections([lastCell.handle]); + this.editingDelegate.emitSelections([lastCell.handle]); lastCell.focusMode = CellFocusMode.Editor; } } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts index 83dc3487a73f..76b317332c77 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts @@ -173,17 +173,17 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod * Text model is used for editing. */ async resolveTextModel(): Promise { - if (!this._textModel) { + if (!this.textModel) { const ref = await this._modelService.createModelReference(this.model.uri); - this._textModel = ref.object.textEditorModel; + this.textModel = ref.object.textEditorModel; this._register(ref); - this._register(this._textModel.onDidChangeContent(() => { + this._register(this.textModel.onDidChangeContent(() => { this.editState = CellEditState.Editing; this._onDidChangeState.fire({ contentChanged: true }); })); } - return this._textModel; + return this.textModel; } onDeselect() { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts index 70f4e0008942..3ea2c23acacd 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts @@ -147,16 +147,16 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie } async resolveTextModel(): Promise { - if (!this._textModel) { + if (!this.textModel) { const ref = await this._modelService.createModelReference(this.model.uri); - this._textModel = ref.object.textEditorModel; + this.textModel = ref.object.textEditorModel; this._register(ref); - this._register(this._textModel.onDidChangeContent(() => { + this._register(this.textModel.onDidChangeContent(() => { this._html = null; this._onDidChangeState.fire({ contentChanged: true }); })); } - return this._textModel; + return this.textModel; } onDeselect() { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts index 07e4e899dd85..7b63095557b7 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts @@ -19,7 +19,6 @@ import { WorkspaceTextEdit } from 'vs/editor/common/modes'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { CellEditState, CellFindMatch, ICellRange, ICellViewModel, NotebookLayoutInfo, IEditableCellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { DeleteCellEdit, InsertCellEdit, MoveCellEdit, SpliceCellsEdit, JoinCellEdit, SplitCellEdit } from 'vs/workbench/contrib/notebook/browser/viewModel/cellEdit'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { NotebookEventDispatcher, NotebookMetadataChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { CellFoldingState, EditorFoldingStateDelegate } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; @@ -31,6 +30,8 @@ import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/no import { MarkdownRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/mdRenderer'; import { dirname } from 'vs/base/common/resources'; import { IPosition, Position } from 'vs/editor/common/core/position'; +import { SplitCellEdit, JoinCellEdit } from 'vs/workbench/contrib/notebook/browser/viewModel/cellEdit'; +import { BaseCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel'; import { PieceTreeTextBuffer } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer'; export interface INotebookEditorViewState { @@ -199,7 +200,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD return this._notebook.metadata; } - private readonly _onDidChangeViewCells = new Emitter(); + private readonly _onDidChangeViewCells = this._register(new Emitter()); get onDidChangeViewCells(): Event { return this._onDidChangeViewCells.event; } private _lastNotebookEditResource: URI[] = []; @@ -215,7 +216,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD return this._layoutInfo; } - private readonly _onDidChangeSelection = new Emitter(); + private readonly _onDidChangeSelection = this._register(new Emitter()); get onDidChangeSelection(): Event { return this._onDidChangeSelection.event; } private _selections: number[] = []; @@ -242,6 +243,11 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD public readonly id: string; private _foldingRanges: FoldingRegions | null = null; private _hiddenRanges: ICellRange[] = []; + private _focused: boolean = true; + + get focused() { + return this._focused; + } constructor( public viewType: string, @@ -259,23 +265,20 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD this._instanceId = strings.singleLetterHash(MODEL_ID); this._register(this._notebook.onDidChangeCells(e => { - const diffs = e.map(splice => { + const diffs = e.splices.map(splice => { return [splice[0], splice[1], splice[2].map(cell => { return createCellViewModel(this._instantiationService, this, cell as NotebookCellTextModel); })] as [number, number, CellViewModel[]]; }); - const undoDiff = diffs.map(diff => { - const deletedCells = this.viewCells.slice(diff[0], diff[0] + diff[1]); - - return [diff[0], deletedCells, diff[2]] as [number, CellViewModel[], CellViewModel[]]; - }); - diffs.reverse().forEach(diff => { const deletedCells = this._viewCells.splice(diff[0], diff[1], ...diff[2]); + this._decorationsTree.acceptReplace(diff[0], diff[1], diff[2].length, true); deletedCells.forEach(cell => { this._handleToViewCellMapping.delete(cell.handle); + // dispsoe the cell to release ref to the cell text document + cell.dispose(); }); diff[2].forEach(cell => { @@ -285,7 +288,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD }); this._onDidChangeViewCells.fire({ - synchronous: true, + synchronous: e.synchronous, splices: diffs }); @@ -315,12 +318,6 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD } } - this._undoService.pushElement(new SpliceCellsEdit(this.uri, undoDiff, { - insertCell: this._insertCellDelegate.bind(this), - deleteCell: this._deleteCellDelegate.bind(this), - setSelections: this._setSelectionsDelegate.bind(this) - }, this.selectionHandles, endSelectionHandles)); - this.selectionHandles = endSelectionHandles; })); @@ -328,6 +325,13 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD this.eventDispatcher.emit([new NotebookMetadataChangedEvent(e)]); })); + this._register(this._notebook.emitSelections(selections => { + // text model emit selection change (for example, undo/redo) + // we should update the selection handle wisely + // TODO, if the editor is note selected, undo/redo should not change the focused element selection + this.updateSelectionsFromEdits(selections); + })); + this._register(this.eventDispatcher.onDidChangeLayout((e) => { this._layoutInfo = e.value; @@ -353,6 +357,16 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD }); } + setFocus(focused: boolean) { + this._focused = focused; + } + + updateSelectionsFromEdits(selections: number[]) { + if (this._focused) { + this.selectionHandles = selections; + } + } + getFoldingStartIndex(index: number): number { if (!this._foldingRanges) { return -1; @@ -582,87 +596,20 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD return result; } - private _createCellDelegate(index: number, source: string | string[], language: string, type: CellKind) { - const cell = this._notebook.createCellTextModel(source, language, type, [], undefined); - let newCell: CellViewModel = createCellViewModel(this._instantiationService, this, cell); - this._viewCells!.splice(index, 0, newCell); - this._handleToViewCellMapping.set(newCell.handle, newCell); - this._notebook.insertNewCell(index, [cell]); - this._localStore.add(newCell); - this._decorationsTree.acceptReplace(index, 0, 1, true); - this._onDidChangeViewCells.fire({ synchronous: true, splices: [[index, 0, [newCell]]] }); - return newCell; - } - - private _insertCellDelegate(insertIndex: number, insertCell: CellViewModel) { - this._viewCells!.splice(insertIndex, 0, insertCell); - this._handleToViewCellMapping.set(insertCell.handle, insertCell); - this._notebook.insertNewCell(insertIndex, [insertCell.model as NotebookCellTextModel]); - this._localStore.add(insertCell); - this._onDidChangeViewCells.fire({ synchronous: true, splices: [[insertIndex, 0, [insertCell]]] }); - } - - private _deleteCellDelegate(deleteIndex: number) { - const deleteCell = this._viewCells[deleteIndex]; - this._viewCells.splice(deleteIndex, 1); - this._handleToViewCellMapping.delete(deleteCell.handle); - - this._notebook.removeCell(deleteIndex, 1); - this._decorationsTree.acceptReplace(deleteIndex, 1, 0, true); - this._onDidChangeViewCells.fire({ synchronous: true, splices: [[deleteIndex, 1, []]] }); - } - - private _setSelectionsDelegate(selections: number[]) { - this.selectionHandles = selections; - } - createCell(index: number, source: string | string[], language: string, type: CellKind, metadata: NotebookCellMetadata | undefined, synchronous: boolean, pushUndoStop: boolean = true) { - const cell = this._notebook.createCellTextModel(source, language, type, [], metadata); - let newCell: CellViewModel = createCellViewModel(this._instantiationService, this, cell); - this._viewCells!.splice(index, 0, newCell); - this._handleToViewCellMapping.set(newCell.handle, newCell); - this._notebook.insertNewCell(index, [cell]); - this._localStore.add(newCell); - - if (pushUndoStop) { - this._undoService.pushElement(new InsertCellEdit(this.uri, index, newCell, { - insertCell: this._insertCellDelegate.bind(this), - deleteCell: this._deleteCellDelegate.bind(this), - setSelections: this._setSelectionsDelegate.bind(this) - }, this.selectionHandles, this.selectionHandles)); - } - - this._decorationsTree.acceptReplace(index, 0, 1, true); - this._onDidChangeViewCells.fire({ synchronous: synchronous, splices: [[index, 0, [newCell]]] }); - return newCell; + this._notebook.createCell2(index, source, language, type, metadata, synchronous, pushUndoStop, undefined, undefined); + // TODO, rely on createCell to be sync + return this.viewCells[index]; } - insertCell(index: number, cell: NotebookCellTextModel, synchronous: boolean): CellViewModel { - let newCell: CellViewModel = createCellViewModel(this._instantiationService, this, cell); - this._viewCells!.splice(index, 0, newCell); - this._handleToViewCellMapping.set(newCell.handle, newCell); - - this._notebook.insertNewCell(index, [newCell.model]); - this._localStore.add(newCell); - this._undoService.pushElement(new InsertCellEdit(this.uri, index, newCell, { - insertCell: this._insertCellDelegate.bind(this), - deleteCell: this._deleteCellDelegate.bind(this), - setSelections: this._setSelectionsDelegate.bind(this) - }, this.selectionHandles, this.selectionHandles)); - - this._decorationsTree.acceptReplace(index, 0, 1, true); - this._onDidChangeViewCells.fire({ synchronous: synchronous, splices: [[index, 0, [newCell]]] }); - return newCell; + insertCell(index: number, cell: NotebookCellTextModel, synchronous: boolean, pushUndoStop: boolean = true): CellViewModel { + this._notebook.insertCell2(index, cell, synchronous, pushUndoStop); + // TODO, rely on createCell to be sync // this will trigger it to synchronous update + return this._viewCells[index]; } deleteCell(index: number, synchronous: boolean, pushUndoStop: boolean = true) { const primarySelectionIndex = this.selectionHandles.length ? this._viewCells.indexOf(this.getCellByHandle(this.selectionHandles[0])!) : null; - - let viewCell = this._viewCells[index]; - this._viewCells.splice(index, 1); - this._handleToViewCellMapping.delete(viewCell.handle); - this._notebook.removeCell(index, 1); - let endSelections: number[] = []; if (this.selectionHandles.length) { const primarySelectionHandle = this.selectionHandles[0]; @@ -680,23 +627,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD } } - if (pushUndoStop) { - this._undoService.pushElement(new DeleteCellEdit(this.uri, index, viewCell, { - insertCell: this._insertCellDelegate.bind(this), - deleteCell: this._deleteCellDelegate.bind(this), - createCellViewModel: (cell: NotebookCellTextModel) => { - return createCellViewModel(this._instantiationService, this, cell); - }, - setSelections: this._setSelectionsDelegate.bind(this) - }, this.selectionHandles, endSelections)); - } - - this.selectionHandles = endSelections; - - this._decorationsTree.acceptReplace(index, 1, 0, true); - - this._onDidChangeViewCells.fire({ synchronous: synchronous, splices: [[index, 1, []]] }); - viewCell.dispose(); + this._notebook.deleteCell2(index, synchronous, pushUndoStop, this.selectionHandles, endSelections); } moveCellToIdx(index: number, newIdx: number, synchronous: boolean, pushedToUndoStack: boolean = true): boolean { @@ -705,24 +636,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD return false; } - this.viewCells.splice(index, 1); - this.viewCells!.splice(newIdx, 0, viewCell); - this._notebook.moveCellToIdx(index, newIdx); - - if (pushedToUndoStack) { - this._undoService.pushElement(new MoveCellEdit(this.uri, index, newIdx, { - moveCell: (fromIndex: number, toIndex: number) => { - this.moveCellToIdx(fromIndex, toIndex, true, false); - }, - setSelections: this._setSelectionsDelegate.bind(this) - }, this.selectionHandles, this.selectionHandles)); - } - - this.selectionHandles = this.selectionHandles; - - this._onDidChangeViewCells.fire({ synchronous: synchronous, splices: [[index, 1, []]] }); - this._onDidChangeViewCells.fire({ synchronous: synchronous, splices: [[newIdx, 0, [viewCell]]] }); - + this._notebook.moveCellToIdx2(index, newIdx, synchronous, pushedToUndoStack, undefined, [viewCell.handle]); return true; } @@ -806,23 +720,10 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD let newLinesContents = this._computeCellLinesContents(cell, splitPoints); if (newLinesContents) { - const editorSelections = cell.getSelections(); - // update the contents of the first cell - cell.textModel.applyEdits([ - { range: cell.textModel.getFullModelRange(), text: newLinesContents[0] } - ], false); - - // create new cells based on the new text models - const language = cell.model.language; + this._notebook.splitNotebookCell(index, newLinesContents, this.selectionHandles); + const language = cell.language; const kind = cell.cellKind; - let insertIndex = this.getCellIndex(cell) + 1; - const newCells = []; - for (let j = 1; j < newLinesContents.length; j++, insertIndex++) { - newCells.push(this.createCell(insertIndex, newLinesContents[j], language, kind, undefined, true, false)); - } - - this.selectionHandles = [cell.handle]; this._undoService.pushElement(new SplitCellEdit( this.uri, @@ -833,16 +734,17 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD language, kind, { - insertCell: this._insertCellDelegate.bind(this), - deleteCell: this._deleteCellDelegate.bind(this), - createCellViewModel: (cell: NotebookCellTextModel) => { - return createCellViewModel(this._instantiationService, this, cell); + createCell: (index: number, source: string | string[], language: string, type: CellKind) => { + return this.createCell(index, source, language, type, undefined, true, false) as BaseCellViewModel; }, - createCell: this._createCellDelegate.bind(this), - setSelections: this._setSelectionsDelegate.bind(this) + deleteCell: (index: number) => { + this.deleteCell(index, true, false); + }, + emitSelections: (selections: number[]) => { + this.updateSelectionsFromEdits(selections); + } } )); - return newCells; } } @@ -908,12 +810,18 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD insertContent, cell, { - insertCell: this._insertCellDelegate.bind(this), - deleteCell: this._deleteCellDelegate.bind(this), + insertCell: (index: number, cell: NotebookCellTextModel) => { + this.insertCell(index, cell, true, false); + }, + deleteCell: (index: number) => { + this.deleteCell(index, true, false); + }, createCellViewModel: (cell: NotebookCellTextModel) => { return createCellViewModel(this._instantiationService, this, cell); }, - setSelections: this._setSelectionsDelegate.bind(this) + emitSelections: (selections: number[]) => { + this.updateSelectionsFromEdits(selections); + } }) ); @@ -956,12 +864,18 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD insertContent, below, { - insertCell: this._insertCellDelegate.bind(this), - deleteCell: this._deleteCellDelegate.bind(this), + insertCell: (index: number, cell: NotebookCellTextModel) => { + this.insertCell(index, cell, true, false); + }, + deleteCell: (index: number) => { + this.deleteCell(index, true, false); + }, createCellViewModel: (cell: NotebookCellTextModel) => { return createCellViewModel(this._instantiationService, this, cell); }, - setSelections: this._setSelectionsDelegate.bind(this) + emitSelections: (selections: number[]) => { + this.updateSelectionsFromEdits(selections); + } }) ); diff --git a/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts b/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts new file mode 100644 index 000000000000..2bf2293f03cc --- /dev/null +++ b/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts @@ -0,0 +1,184 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IResourceUndoRedoElement, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo'; +import { URI } from 'vs/base/common/uri'; +import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; + +/** + * It should not modify Undo/Redo stack + */ +export interface ITextCellEditingDelegate { + insertCell?(index: number, cell: NotebookCellTextModel): void; + deleteCell?(index: number): void; + moveCell?(fromIndex: number, toIndex: number, beforeSelections: number[] | undefined, endSelections: number[] | undefined): void; + emitSelections(selections: number[]): void; +} + + +export class InsertCellEdit implements IResourceUndoRedoElement { + type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; + label: string = 'Insert Cell'; + constructor( + public resource: URI, + private insertIndex: number, + private cell: NotebookCellTextModel, + private editingDelegate: ITextCellEditingDelegate, + private beforedSelections: number[] | undefined, + private endSelections: number[] | undefined + ) { + } + + undo(): void | Promise { + if (!this.editingDelegate.deleteCell) { + throw new Error('Notebook Delete Cell not implemented for Undo/Redo'); + } + + this.editingDelegate.deleteCell(this.insertIndex); + if (this.beforedSelections) { + this.editingDelegate.emitSelections(this.beforedSelections); + } + } + redo(): void | Promise { + if (!this.editingDelegate.insertCell) { + throw new Error('Notebook Insert Cell not implemented for Undo/Redo'); + } + + this.editingDelegate.insertCell(this.insertIndex, this.cell); + if (this.endSelections) { + this.editingDelegate.emitSelections(this.endSelections); + } + } +} + +export class DeleteCellEdit implements IResourceUndoRedoElement { + type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; + label: string = 'Delete Cell'; + constructor( + public resource: URI, + private insertIndex: number, + private _cell: NotebookCellTextModel, + private editingDelegate: ITextCellEditingDelegate, + private beforedSelections: number[] | undefined, + private endSelections: number[] | undefined + ) { + + // save inmem text to `ICell` + // no needed any more as the text buffer is transfered to `raw_cell` + // this._rawCell.source = [cell.getText()]; + } + + undo(): void | Promise { + if (!this.editingDelegate.insertCell) { + throw new Error('Notebook Insert Cell not implemented for Undo/Redo'); + } + + this.editingDelegate.insertCell(this.insertIndex, this._cell); + if (this.beforedSelections) { + this.editingDelegate.emitSelections(this.beforedSelections); + } + } + + redo(): void | Promise { + if (!this.editingDelegate.deleteCell) { + throw new Error('Notebook Delete Cell not implemented for Undo/Redo'); + } + + this.editingDelegate.deleteCell(this.insertIndex); + if (this.endSelections) { + this.editingDelegate.emitSelections(this.endSelections); + } + } +} + +export class MoveCellEdit implements IResourceUndoRedoElement { + type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; + label: string = 'Delete Cell'; + + constructor( + public resource: URI, + private fromIndex: number, + private toIndex: number, + private editingDelegate: ITextCellEditingDelegate, + private beforedSelections: number[] | undefined, + private endSelections: number[] | undefined + ) { + } + + undo(): void | Promise { + if (!this.editingDelegate.moveCell) { + throw new Error('Notebook Move Cell not implemented for Undo/Redo'); + } + + this.editingDelegate.moveCell(this.toIndex, this.fromIndex, this.endSelections, this.beforedSelections); + if (this.beforedSelections) { + this.editingDelegate.emitSelections(this.beforedSelections); + } + } + + redo(): void | Promise { + if (!this.editingDelegate.moveCell) { + throw new Error('Notebook Move Cell not implemented for Undo/Redo'); + } + + this.editingDelegate.moveCell(this.fromIndex, this.toIndex, this.beforedSelections, this.endSelections); + if (this.endSelections) { + this.editingDelegate.emitSelections(this.endSelections); + } + } +} + +export class SpliceCellsEdit implements IResourceUndoRedoElement { + type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; + label: string = 'Insert Cell'; + constructor( + public resource: URI, + private diffs: [number, NotebookCellTextModel[], NotebookCellTextModel[]][], + private editingDelegate: ITextCellEditingDelegate, + private beforeHandles: number[] | undefined, + private endHandles: number[] | undefined + ) { + } + + undo(): void | Promise { + if (!this.editingDelegate.deleteCell || !this.editingDelegate.insertCell) { + throw new Error('Notebook Insert/Delete Cell not implemented for Undo/Redo'); + } + + this.diffs.forEach(diff => { + for (let i = 0; i < diff[2].length; i++) { + this.editingDelegate.deleteCell!(diff[0]); + } + + diff[1].reverse().forEach(cell => { + this.editingDelegate.insertCell!(diff[0], cell); + }); + }); + + if (this.beforeHandles) { + this.editingDelegate.emitSelections(this.beforeHandles); + } + } + + redo(): void | Promise { + if (!this.editingDelegate.deleteCell || !this.editingDelegate.insertCell) { + throw new Error('Notebook Insert/Delete Cell not implemented for Undo/Redo'); + } + + this.diffs.reverse().forEach(diff => { + for (let i = 0; i < diff[1].length; i++) { + this.editingDelegate.deleteCell!(diff[0]); + } + + diff[2].reverse().forEach(cell => { + this.editingDelegate.insertCell!(diff[0], cell); + }); + }); + + if (this.endHandles) { + this.editingDelegate.emitSelections(this.endHandles); + } + } +} diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts index d9f2e3d35599..ccab2b085c3b 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { ICell, IProcessedOutput, NotebookCellOutputsSplice, CellKind, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ICell, IProcessedOutput, NotebookCellOutputsSplice, CellKind, NotebookCellMetadata, NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; import { URI } from 'vs/base/common/uri'; import * as model from 'vs/editor/common/model'; @@ -69,6 +69,16 @@ export class NotebookCellTextModel extends Disposable implements ICell { return this._textBuffer; } + private _textModel?: model.ITextModel; + + get textModel(): model.ITextModel | undefined { + return this._textModel; + } + + set textModel(m: model.ITextModel | undefined) { + this._textModel = m; + } + constructor( readonly uri: URI, public handle: number, @@ -109,4 +119,24 @@ export class NotebookCellTextModel extends Disposable implements ICell { this._onDidChangeOutputs.fire(splices); } + + getEvaluatedMetadata(documentMetadata: NotebookDocumentMetadata): NotebookCellMetadata { + const editable = this.metadata?.editable ?? + documentMetadata.cellEditable; + + const runnable = this.metadata?.runnable ?? + documentMetadata.cellRunnable; + + const hasExecutionOrder = this.metadata?.hasExecutionOrder ?? + documentMetadata.cellHasExecutionOrder; + + return { + ...(this.metadata || {}), + ...{ + editable, + runnable, + hasExecutionOrder + } + }; + } } diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index 368d919e6d42..35e7c3a59dc2 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -9,6 +9,8 @@ import { URI } from 'vs/base/common/uri'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { INotebookTextModel, NotebookCellOutputsSplice, NotebookCellTextModelSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, CellEditType, CellUri, ICellInsertEdit, NotebookCellsChangedEvent, CellKind, IProcessedOutput, notebookDocumentMetadataDefaults, diff, ICellDeleteEdit, NotebookCellsChangeType, ICellDto2, IMainCellDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ITextSnapshot } from 'vs/editor/common/model'; +import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; +import { InsertCellEdit, DeleteCellEdit, MoveCellEdit, SpliceCellsEdit } from 'vs/workbench/contrib/notebook/common/model/cellEdit'; function compareRangesUsingEnds(a: [number, number], b: [number, number]): number { if (a[1] === b[1]) { @@ -68,15 +70,17 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel private readonly _onWillDispose: Emitter = this._register(new Emitter()); readonly onWillDispose: Event = this._onWillDispose.event; - private readonly _onDidChangeCells = new Emitter(); - get onDidChangeCells(): Event { return this._onDidChangeCells.event; } - private _onDidModelChangeProxy = new Emitter(); + private readonly _onDidChangeCells = this._register(new Emitter<{ synchronous: boolean, splices: NotebookCellTextModelSplice[] }>()); + get onDidChangeCells() { return this._onDidChangeCells.event; } + private readonly _emitSelections = this._register(new Emitter()); + get emitSelections() { return this._emitSelections.event; } + private _onDidModelChangeProxy = this._register(new Emitter()); get onDidModelChangeProxy(): Event { return this._onDidModelChangeProxy.event; } - private _onDidSelectionChangeProxy = new Emitter(); + private _onDidSelectionChangeProxy = this._register(new Emitter()); get onDidSelectionChange(): Event { return this._onDidSelectionChangeProxy.event; } - private _onDidChangeContent = new Emitter(); + private _onDidChangeContent = this._register(new Emitter()); onDidChangeContent: Event = this._onDidChangeContent.event; - private _onDidChangeMetadata = new Emitter(); + private _onDidChangeMetadata = this._register(new Emitter()); onDidChangeMetadata: Event = this._onDidChangeMetadata.event; private _mapping: Map = new Map(); private _cellListeners: Map = new Map(); @@ -110,7 +114,8 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel public handle: number, public viewType: string, public supportBackup: boolean, - public uri: URI + public uri: URI, + private _undoService: IUndoRedoService ) { super(); this.cells = []; @@ -165,7 +170,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._increaseVersionId(); } - $applyEdit(modelVersionId: number, rawEdits: ICellEditOperation[], emitToExtHost: boolean = true): boolean { + $applyEdit(modelVersionId: number, rawEdits: ICellEditOperation[], emitToExtHost: boolean, synchronous: boolean): boolean { if (modelVersionId !== this._versionId) { return false; } @@ -242,7 +247,19 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel }); } - this._onDidChangeCells.fire(diffs); + const undoDiff = diffs.map(diff => { + const deletedCells = this.cells.slice(diff[0], diff[0] + diff[1]); + + return [diff[0], deletedCells, diff[2]] as [number, NotebookCellTextModel[], NotebookCellTextModel[]]; + }); + + this._undoService.pushElement(new SpliceCellsEdit(this.uri, undoDiff, { + insertCell: this._insertCellDelegate.bind(this), + deleteCell: this._deleteCellDelegate.bind(this), + emitSelections: this._emitSelectionsDelegate.bind(this) + }, undefined, undefined)); + + this._onDidChangeCells.fire({ synchronous: synchronous, splices: diffs }); return true; } @@ -446,6 +463,123 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._onDidModelChangeProxy.fire({ kind: NotebookCellsChangeType.CellsClearOutput, versionId: this._versionId }); } + //#region Notebook Text Model Edit API + + private _insertCellDelegate(insertIndex: number, insertCell: NotebookCellTextModel) { + this.insertNewCell(insertIndex, [insertCell]); + this._onDidChangeCells.fire({ synchronous: true, splices: [[insertIndex, 0, [insertCell]]] }); + } + + private _deleteCellDelegate(deleteIndex: number) { + this.removeCell(deleteIndex, 1); + this._onDidChangeCells.fire({ synchronous: true, splices: [[deleteIndex, 1, []]] }); + } + + private _emitSelectionsDelegate(selections: number[]) { + this._emitSelections.fire(selections); + } + + createCell2(index: number, source: string | string[], language: string, type: CellKind, metadata: NotebookCellMetadata | undefined, synchronous: boolean, pushUndoStop: boolean, beforeSelections: number[] | undefined, endSelections: number[] | undefined) { + const cell = this.createCellTextModel(source, language, type, [], metadata); + + if (pushUndoStop) { + this._undoService.pushElement(new InsertCellEdit(this.uri, index, cell, { + insertCell: this._insertCellDelegate.bind(this), + deleteCell: this._deleteCellDelegate.bind(this), + emitSelections: this._emitSelectionsDelegate.bind(this) + }, beforeSelections, endSelections)); + } + + + this.insertNewCell(index, [cell]); + + this._onDidChangeCells.fire({ synchronous, splices: [[index, 0, [cell]]] }); + + if (endSelections) { + this._emitSelections.fire(endSelections); + } + return cell; + } + + insertCell2(index: number, cell: NotebookCellTextModel, synchronous: boolean, pushUndoStop: boolean): void { + if (pushUndoStop) { + this._undoService.pushElement(new InsertCellEdit(this.uri, index, cell, { + insertCell: this._insertCellDelegate.bind(this), + deleteCell: this._deleteCellDelegate.bind(this), + emitSelections: this._emitSelectionsDelegate.bind(this) + }, undefined, undefined)); + } + + this.insertNewCell(index, [cell]); + this._onDidChangeCells.fire({ synchronous: synchronous, splices: [[index, 0, [cell]]] }); + } + + deleteCell2(index: number, synchronous: boolean, pushUndoStop: boolean, beforeSelections: number[] | undefined, endSelections: number[] | undefined) { + const cell = this.cells[index]; + if (pushUndoStop) { + this._undoService.pushElement(new DeleteCellEdit(this.uri, index, cell, { + insertCell: this._insertCellDelegate.bind(this), + deleteCell: this._deleteCellDelegate.bind(this), + emitSelections: this._emitSelectionsDelegate.bind(this) + }, beforeSelections, endSelections)); + } + + this.removeCell(index, 1); + this._onDidChangeCells.fire({ synchronous: synchronous, splices: [[index, 1, []]] }); + if (endSelections) { + this._emitSelections.fire(endSelections); + } + } + + moveCellToIdx2(index: number, newIdx: number, synchronous: boolean, pushedToUndoStack: boolean, beforeSelections: number[] | undefined, endSelections: number[] | undefined): boolean { + const cell = this.cells[index]; + if (pushedToUndoStack) { + this._undoService.pushElement(new MoveCellEdit(this.uri, index, newIdx, { + moveCell: (fromIndex: number, toIndex: number, beforeSelections: number[] | undefined, endSelections: number[] | undefined) => { + this.moveCellToIdx2(fromIndex, toIndex, true, false, beforeSelections, endSelections); + }, + emitSelections: this._emitSelectionsDelegate.bind(this) + }, beforeSelections, endSelections)); + } + + this.moveCellToIdx(index, newIdx); + // todo, we can't emit this change as it will create a new view model and that will hold + // a new reference to the document, thus + this._onDidChangeCells.fire({ synchronous: synchronous, splices: [[index, 1, []]] }); + this._onDidChangeCells.fire({ synchronous: synchronous, splices: [[newIdx, 0, [cell]]] }); + if (endSelections) { + this._emitSelections.fire(endSelections); + } + + return true; + } + + async splitNotebookCell(index: number, newLinesContents: string[], endSelections: number[]) { + const cell = this.cells[index]; + + if (!cell.textModel) { + return; + } + + cell.textModel.applyEdits([ + { range: cell.textModel.getFullModelRange(), text: newLinesContents[0] } + ], false); + + // create new cells based on the new text models + const language = cell.language; + const kind = cell.cellKind; + let insertIndex = index + 1; + const newCells = []; + for (let j = 1; j < newLinesContents.length; j++, insertIndex++) { + newCells.push(this.createCell2(insertIndex, newLinesContents[j], language, kind, undefined, true, false, undefined, undefined)); + } + + if (endSelections) { + this._emitSelections.fire(endSelections); + } + } + //#endregion + dispose() { this._onWillDispose.fire(); this._cellListeners.forEach(val => val.dispose()); diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index e6508afccf62..efaf1248a3d3 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -269,7 +269,7 @@ export interface INotebookTextModel { languages: string[]; cells: ICell[]; renderers: Set; - onDidChangeCells?: Event; + onDidChangeCells?: Event<{ synchronous: boolean, splices: NotebookCellTextModelSplice[] }>; onDidChangeContent: Event; onWillDispose(listener: () => void): IDisposable; } diff --git a/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts index 2bad51a304a4..84f55c863311 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts @@ -31,7 +31,7 @@ suite('NotebookTextModel', () => { textModel.$applyEdit(textModel.versionId, [ { editType: CellEditType.Insert, index: 1, cells: [new TestCell(viewModel.viewType, 5, ['var e = 5;'], 'javascript', CellKind.Code, [])] }, { editType: CellEditType.Insert, index: 3, cells: [new TestCell(viewModel.viewType, 6, ['var f = 6;'], 'javascript', CellKind.Code, [])] }, - ]); + ], true, true); assert.equal(textModel.cells.length, 6); @@ -56,7 +56,7 @@ suite('NotebookTextModel', () => { textModel.$applyEdit(textModel.versionId, [ { editType: CellEditType.Insert, index: 1, cells: [new TestCell(viewModel.viewType, 5, ['var e = 5;'], 'javascript', CellKind.Code, [])] }, { editType: CellEditType.Insert, index: 1, cells: [new TestCell(viewModel.viewType, 6, ['var f = 6;'], 'javascript', CellKind.Code, [])] }, - ]); + ], true, true); assert.equal(textModel.cells.length, 6); @@ -81,7 +81,7 @@ suite('NotebookTextModel', () => { textModel.$applyEdit(textModel.versionId, [ { editType: CellEditType.Delete, index: 1, count: 1 }, { editType: CellEditType.Delete, index: 3, count: 1 }, - ]); + ], true, true); assert.equal(textModel.cells[0].getValue(), 'var a = 1;'); assert.equal(textModel.cells[1].getValue(), 'var c = 3;'); @@ -104,7 +104,7 @@ suite('NotebookTextModel', () => { textModel.$applyEdit(textModel.versionId, [ { editType: CellEditType.Delete, index: 1, count: 1 }, { editType: CellEditType.Insert, index: 3, cells: [new TestCell(viewModel.viewType, 5, ['var e = 5;'], 'javascript', CellKind.Code, [])] }, - ]); + ], true, true); assert.equal(textModel.cells.length, 4); @@ -129,7 +129,7 @@ suite('NotebookTextModel', () => { textModel.$applyEdit(textModel.versionId, [ { editType: CellEditType.Delete, index: 1, count: 1 }, { editType: CellEditType.Insert, index: 1, cells: [new TestCell(viewModel.viewType, 5, ['var e = 5;'], 'javascript', CellKind.Code, [])] }, - ]); + ], true, true); assert.equal(textModel.cells.length, 4); assert.equal(textModel.cells[0].getValue(), 'var a = 1;'); diff --git a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts index 74af51dd4209..a5279405bc10 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts @@ -23,7 +23,7 @@ suite('NotebookViewModel', () => { instantiationService.spy(IUndoRedoService, 'pushElement'); test('ctor', function () { - const notebook = new NotebookTextModel(0, 'notebook', false, URI.parse('test')); + const notebook = new NotebookTextModel(0, 'notebook', false, URI.parse('test'), undoRedoService); const model = new NotebookEditorTestModel(notebook); const eventDispatcher = new NotebookEventDispatcher(); const viewModel = new NotebookViewModel('notebook', model.notebook, eventDispatcher, null, instantiationService, blukEditService, undoRedoService); diff --git a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts index cbd8b4f01217..8c12ebf06421 100644 --- a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts @@ -137,15 +137,15 @@ export class TestNotebookEditor implements INotebookEditor { throw new Error('Method not implemented.'); } - moveCellDown(cell: CellViewModel): Promise { + moveCellDown(cell: CellViewModel): Promise { throw new Error('Method not implemented.'); } - moveCellUp(cell: CellViewModel): Promise { + moveCellUp(cell: CellViewModel): Promise { throw new Error('Method not implemented.'); } - moveCell(cell: ICellViewModel, relativeToCell: ICellViewModel, direction: 'above' | 'below'): Promise { + moveCell(cell: ICellViewModel, relativeToCell: ICellViewModel, direction: 'above' | 'below'): Promise { throw new Error('Method not implemented.'); } @@ -320,7 +320,7 @@ export class NotebookEditorTestModel extends EditorModel implements INotebookEdi export function withTestNotebook(instantiationService: IInstantiationService, blukEditService: IBulkEditService, undoRedoService: IUndoRedoService, cells: [string[], string, CellKind, IProcessedOutput[], NotebookCellMetadata][], callback: (editor: TestNotebookEditor, viewModel: NotebookViewModel, textModel: NotebookTextModel) => void) { const viewType = 'notebook'; const editor = new TestNotebookEditor(); - const notebook = new NotebookTextModel(0, viewType, false, URI.parse('test')); + const notebook = new NotebookTextModel(0, viewType, false, URI.parse('test'), undoRedoService); notebook.cells = cells.map((cell, index) => { return new NotebookCellTextModel(notebook.uri, index, cell[0], cell[1], cell[2], cell[3], cell[4]); });