diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index 7e9022063e1..ef92249a326 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -14,9 +14,10 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { ILogService } from 'vs/platform/log/common/log'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; -import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, CellEditType, CellKind, DisplayOrderKey, ICellEditOperation, ICellRange, IEditor, INotebookDocumentFilter, NotebookCellMetadata, NotebookCellOutputsSplice, NotebookDocumentMetadata, NOTEBOOK_DISPLAY_ORDER, TransientMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, CellEditType, CellKind, DisplayOrderKey, ICellEditOperation, ICellRange, IEditor, IMainCellDto, INotebookDocumentFilter, NotebookCellMetadata, NotebookCellOutputsSplice, NotebookCellsChangeType, NotebookDocumentMetadata, NOTEBOOK_DISPLAY_ORDER, TransientMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IMainNotebookController, INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ExtHostContext, ExtHostNotebookShape, IExtHostContext, INotebookCellStatusBarEntryDto, INotebookDocumentsAndEditorsDelta, INotebookModelAddedData, MainContext, MainThreadNotebookShape, NotebookEditorRevealType, NotebookExtensionDescription } from '../common/extHost.protocol'; @@ -59,7 +60,7 @@ class DocumentAndEditorState { const apiEditors = []; for (let id in after.textEditors) { const editor = after.textEditors.get(id)!; - apiEditors.push({ id, documentUri: editor.uri!, selections: editor!.textModel!.selections, visibleRanges: editor.visibleRanges }); + apiEditors.push({ id, documentUri: editor.uri!, selections: editor!.getSelectionHandles(), visibleRanges: editor.visibleRanges }); } return { @@ -73,7 +74,7 @@ class DocumentAndEditorState { const addedAPIEditors = editorDelta.added.map(add => ({ id: add.getId(), documentUri: add.uri!, - selections: add.textModel!.selections || [], + selections: add.getSelectionHandles(), visibleRanges: add.visibleRanges })); @@ -231,7 +232,12 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo if (!this._editorEventListenersMapping.has(editor.getId())) { const disposableStore = new DisposableStore(); disposableStore.add(editor.onDidChangeVisibleRanges(() => { - this._proxy.$acceptEditorPropertiesChanged(editor.getId(), { visibleRanges: { ranges: editor.visibleRanges } }); + this._proxy.$acceptEditorPropertiesChanged(editor.getId(), { visibleRanges: { ranges: editor.visibleRanges }, selections: null }); + })); + + disposableStore.add(editor.onDidChangeSelection(() => { + const selectionHandles = editor.getSelectionHandles(); + this._proxy.$acceptEditorPropertiesChanged(editor.getId(), { visibleRanges: null, selections: { selections: selectionHandles } }); })); this._editorEventListenersMapping.set(editor.getId(), disposableStore); @@ -256,21 +262,49 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo notebookEditorAddedHandler(editor); }); - const notebookDocumentAddedHandler = (textModel: NotebookTextModel) => { - if (this._documentEventListenersMapping.has(textModel.uri)) { - return; - } - const disposableStore = new DisposableStore(); - disposableStore.add(textModel.onDidModelChangeProxy(e => { - this._proxy.$acceptModelChanged(textModel!.uri, e, textModel!.isDirty); - this._proxy.$acceptDocumentPropertiesChanged(textModel.uri, { selections: { selections: textModel!.selections }, metadata: null }); - })); - disposableStore.add(textModel.onDidSelectionChange(e => { - const selectionsChange = e ? { selections: e } : null; - this._proxy.$acceptDocumentPropertiesChanged(textModel.uri, { selections: selectionsChange, metadata: null }); - })); + const cellToDto = (cell: NotebookCellTextModel): IMainCellDto => { + return { + handle: cell.handle, + uri: cell.uri, + source: cell.textBuffer.getLinesContent(), + eol: cell.textBuffer.getEOL(), + language: cell.language, + cellKind: cell.cellKind, + outputs: cell.outputs, + metadata: cell.metadata + }; + }; - this._documentEventListenersMapping.set(textModel.uri, disposableStore); + + const notebookDocumentAddedHandler = (textModel: NotebookTextModel) => { + if (!this._editorEventListenersMapping.has(textModel.uri.toString())) { + const disposableStore = new DisposableStore(); + disposableStore.add(textModel!.onDidChangeContent(e => { + const data = + e.kind === NotebookCellsChangeType.ModelChange || e.kind === NotebookCellsChangeType.Initialize + ? { + kind: e.kind, + versionId: e.versionId, + changes: e.changes.map(diff => [diff[0], diff[1], diff[2].map(cell => cellToDto(cell as NotebookCellTextModel))] as [number, number, IMainCellDto[]]) + } + : ( + e.kind === NotebookCellsChangeType.Move + ? { + kind: e.kind, + index: e.index, + length: e.length, + newIdx: e.newIdx, + versionId: e.versionId, + cells: e.cells.map(cell => cellToDto(cell as NotebookCellTextModel)) + } + : e + ); + this._proxy.$acceptModelChanged(textModel.uri, data, textModel.isDirty); + this._proxy.$acceptDocumentPropertiesChanged(textModel.uri, { metadata: null }); + })); + + this._editorEventListenersMapping.set(textModel!.uri.toString(), disposableStore); + } }; this._notebookService.listNotebookDocuments().forEach(notebookDocumentAddedHandler); @@ -432,7 +466,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo textModel.insertTemplateCell(mainCell); } - this._proxy.$acceptDocumentPropertiesChanged(textModel.uri, { selections: null, metadata: textModel.metadata }); + this._proxy.$acceptDocumentPropertiesChanged(textModel.uri, { metadata: textModel.metadata }); return; }, resolveNotebookEditor: async (viewType: string, uri: URI, editorId: string) => { @@ -542,7 +576,16 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo async $updateNotebookCellMetadata(viewType: string, resource: UriComponents, handle: number, metadata: NotebookCellMetadata): Promise { this.logService.debug('MainThreadNotebooks#updateNotebookCellMetadata', resource.path, handle, metadata); const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); - textModel?.changeCellMetadata(handle, metadata, true); + if (!textModel) { + return; + } + + const index = textModel.cells.findIndex(cell => cell.handle === handle); + if (index < 0) { + return; + } + + textModel.applyEdit(textModel.versionId, [{ editType: CellEditType.Metadata, index, metadata }], true); } async $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[]): Promise { @@ -551,7 +594,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo if (textModel) { this._notebookService.transformSpliceOutputs(textModel, splices); - textModel.spliceNotebookCellOutputs(cellHandle, splices); + textModel._spliceNotebookCellOutputs(cellHandle, splices); } } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 8621b58cd6d..2f9b6700921 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -51,7 +51,7 @@ import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; import { TunnelOptions } from 'vs/platform/remote/common/tunnel'; import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, InternalTimelineOptions } from 'vs/workbench/contrib/timeline/common/timeline'; import { revive } from 'vs/base/common/marshalling'; -import { IProcessedOutput, INotebookDisplayOrder, NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEvent, NotebookDataDto, IMainCellDto, INotebookDocumentFilter, INotebookKernelInfoDto2, TransientMetadata, INotebookCellStatusBarEntry, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IProcessedOutput, INotebookDisplayOrder, NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEventDto, NotebookDataDto, IMainCellDto, INotebookDocumentFilter, INotebookKernelInfoDto2, TransientMetadata, INotebookCellStatusBarEntry, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { Dto } from 'vs/base/common/types'; import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; @@ -1640,11 +1640,11 @@ export interface INotebookVisibleRangesEvent { export interface INotebookEditorPropertiesChangeData { visibleRanges: INotebookVisibleRangesEvent | null; + selections: INotebookSelectionChangeEvent | null; } export interface INotebookDocumentPropertiesChangeData { metadata: NotebookDocumentMetadata | null; - selections: INotebookSelectionChangeEvent | null; } export interface INotebookModelAddedData { @@ -1686,7 +1686,7 @@ export interface ExtHostNotebookShape { $acceptDisplayOrder(displayOrder: INotebookDisplayOrder): void; $acceptNotebookActiveKernelChange(event: { uri: UriComponents, providerHandle: number | undefined, kernelId: string | undefined }): void; $onDidReceiveMessage(editorId: string, rendererId: string | undefined, message: unknown): void; - $acceptModelChanged(uriComponents: UriComponents, event: NotebookCellsChangedEvent, isDirty: boolean): void; + $acceptModelChanged(uriComponents: UriComponents, event: NotebookCellsChangedEventDto, isDirty: boolean): void; $acceptModelSaved(uriComponents: UriComponents): void; $acceptEditorPropertiesChanged(id: string, data: INotebookEditorPropertiesChangeData): void; $acceptDocumentPropertiesChanged(uriComponents: UriComponents, data: INotebookDocumentPropertiesChangeData): void; diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 6998c475a7e..03e7cedad80 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -22,7 +22,7 @@ import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePa import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; -import { addIdToOutput, CellEditType, CellOutputKind, CellStatusbarAlignment, CellUri, diff, ICellEditOperation, ICellReplaceEdit, IMainCellDto, INotebookCellStatusBarEntry, INotebookDisplayOrder, INotebookEditData, INotebookKernelInfoDto2, IProcessedOutput, NotebookCellMetadata, NotebookCellsChangedEvent, NotebookCellsChangeType, NotebookCellsSplice2, NotebookDataDto, notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { addIdToOutput, CellEditType, CellOutputKind, CellStatusbarAlignment, CellUri, diff, ICellEditOperation, ICellReplaceEdit, IMainCellDto, INotebookCellStatusBarEntry, INotebookDisplayOrder, INotebookEditData, INotebookKernelInfoDto2, IProcessedOutput, NotebookCellMetadata, NotebookCellsChangedEventDto, NotebookCellsChangeType, NotebookCellsSplice2, NotebookDataDto, notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import * as vscode from 'vscode'; import { Cache } from './cache'; import { ResourceMap } from 'vs/base/common/map'; @@ -212,8 +212,7 @@ export class ExtHostNotebookDocument extends Disposable { private _cellDisposableMapping = new Map(); private _notebook: vscode.NotebookDocument | undefined; - - private _metadata: Required = notebookDocumentMetadataDefaults; + private _metadata: Required; private _metadataChangeListener: IDisposable; private _versionId = 0; private _isDirty: boolean = false; @@ -229,13 +228,14 @@ export class ExtHostNotebookDocument extends Disposable { private readonly _documentsAndEditors: ExtHostDocumentsAndEditors, private readonly _emitter: INotebookEventEmitter, private readonly _viewType: string, + metadata: Required, public readonly uri: URI, public readonly renderingHandler: ExtHostNotebookOutputRenderingHandler, private readonly _storagePath: URI | undefined ) { super(); - const observableMetadata = getObservable(notebookDocumentMetadataDefaults); + const observableMetadata = getObservable(metadata); this._metadata = observableMetadata.proxy; this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { this._tryUpdateMetadata(); @@ -313,7 +313,7 @@ export class ExtHostNotebookDocument extends Disposable { this._backup = undefined; } - acceptModelChanged(event: NotebookCellsChangedEvent, isDirty: boolean): void { + acceptModelChanged(event: NotebookCellsChangedEventDto, isDirty: boolean): void { this._versionId = event.versionId; this._isDirty = isDirty; if (event.kind === NotebookCellsChangeType.Initialize) { @@ -324,13 +324,9 @@ export class ExtHostNotebookDocument extends Disposable { this._moveCell(event.index, event.newIdx); } else if (event.kind === NotebookCellsChangeType.Output) { this._setCellOutputs(event.index, event.outputs); - } else if (event.kind === NotebookCellsChangeType.CellClearOutput) { - this._clearCellOutputs(event.index); - } else if (event.kind === NotebookCellsChangeType.CellsClearOutput) { - this._clearAllCellOutputs(); } else if (event.kind === NotebookCellsChangeType.ChangeLanguage) { this._changeCellLanguage(event.index, event.language); - } else if (event.kind === NotebookCellsChangeType.ChangeMetadata) { + } else if (event.kind === NotebookCellsChangeType.ChangeCellMetadata) { this._changeCellMetadata(event.index, event.metadata); } } @@ -421,25 +417,6 @@ export class ExtHostNotebookDocument extends Disposable { this._emitter.emitCellOutputsChange({ document: this.notebookDocument, cells: [cell.cell] }); } - private _clearCellOutputs(index: number): void { - const cell = this._cells[index].cell; - cell.outputs = []; - const event: vscode.NotebookCellOutputsChangeEvent = { document: this.notebookDocument, cells: [cell] }; - this._emitter.emitCellOutputsChange(event); - } - - private _clearAllCellOutputs(): void { - const modifedCells: vscode.NotebookCell[] = []; - this._cells.forEach(({ cell }) => { - if (cell.outputs.length !== 0) { - cell.outputs = []; - modifedCells.push(cell); - } - }); - const event: vscode.NotebookCellOutputsChangeEvent = { document: this.notebookDocument, cells: modifedCells }; - this._emitter.emitCellOutputsChange(event); - } - private _changeCellLanguage(index: number, language: string): void { const cell = this._cells[index]; const event: vscode.NotebookCellLanguageChangeEvent = { document: this.notebookDocument, cell: cell.cell, language }; @@ -1247,7 +1224,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN this._webviewComm.get(editorId)?.onDidReceiveMessage(forRendererType, message); } - $acceptModelChanged(uriComponents: UriComponents, event: NotebookCellsChangedEvent, isDirty: boolean): void { + $acceptModelChanged(uriComponents: UriComponents, event: NotebookCellsChangedEventDto, isDirty: boolean): void { const document = this._documents.get(URI.revive(uriComponents)); if (document) { document.acceptModelChanged(event, isDirty); @@ -1283,15 +1260,6 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN visibleRanges: editor.editor.visibleRanges }); } - } - - $acceptDocumentPropertiesChanged(uriComponents: UriComponents, data: INotebookDocumentPropertiesChangeData): void { - this.logService.debug('ExtHostNotebook#$acceptDocumentPropertiesChanged', uriComponents.path, data); - const editor = this._getEditorFromURI(uriComponents); - - if (!editor) { - return; - } if (data.selections) { if (data.selections.selections.length) { @@ -1306,7 +1274,15 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN selection: editor.editor.selection }); } + } + $acceptDocumentPropertiesChanged(uriComponents: UriComponents, data: INotebookDocumentPropertiesChangeData): void { + this.logService.debug('ExtHostNotebook#$acceptDocumentPropertiesChanged', uriComponents.path, data); + const editor = this._getEditorFromURI(uriComponents); + + if (!editor) { + return; + } if (data.metadata) { editor.editor.notebookData.notebookDocument.metadata = { @@ -1400,15 +1376,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN emitCellMetadataChange(event: vscode.NotebookCellMetadataChangeEvent): void { that._onDidChangeCellMetadata.fire(event); } - }, viewType, uri, this, storageRoot); - - - if (modelData.metadata) { - document.notebookDocument.metadata = { - ...notebookDocumentMetadataDefaults, - ...modelData.metadata - }; - } + }, viewType, { ...notebookDocumentMetadataDefaults, ...modelData.metadata }, uri, this, storageRoot); document.acceptModelChanged({ kind: NotebookCellsChangeType.Initialize, diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index e1bff9ff928..4feefecc161 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -20,7 +20,7 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; import { BaseCellRenderTemplate, CellEditState, CellFocusMode, ICellViewModel, INotebookEditor, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_OUTPUT_FOCUSED, EXPAND_CELL_CONTENT_COMMAND_ID, NOTEBOOK_CELL_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { CellKind, CellUri, NotebookCellRunState, NOTEBOOK_EDITOR_CURSOR_BOUNDARY } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellEditType, CellKind, CellUri, NotebookCellMetadata, NotebookCellRunState, NOTEBOOK_EDITOR_CURSOR_BOUNDARY } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -1290,14 +1290,25 @@ registerAction2(class extends NotebookCellAction { return; } - editor.viewModel.notebookDocument.clearCellOutput(context.cell.handle); + const cell = context.cell; + const index = editor.viewModel.notebookDocument.cells.indexOf(cell.model); + + if (index < 0) { + return; + } + + editor.viewModel.notebookDocument.applyEdit(editor.viewModel.notebookDocument.versionId, [{ editType: CellEditType.Output, index, outputs: [] }], true); + if (context.cell.metadata && context.cell.metadata?.runState !== NotebookCellRunState.Running) { - context.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(context.cell.handle, { - runState: NotebookCellRunState.Idle, - runStartTime: undefined, - lastRunDuration: undefined, - statusMessage: undefined - }); + context.notebookEditor.viewModel!.notebookDocument.applyEdit(context.notebookEditor.viewModel!.notebookDocument.versionId, [{ + editType: CellEditType.Metadata, index, metadata: { + ...context.cell.metadata, + runState: NotebookCellRunState.Idle, + runStartTime: undefined, + lastRunDuration: undefined, + statusMessage: undefined + } + }], true); } } }); @@ -1423,7 +1434,10 @@ registerAction2(class extends NotebookAction { return; } - editor.viewModel.notebookDocument.clearAllCellOutputs(); + editor.viewModel.notebookDocument.applyEdit(editor.viewModel.notebookDocument.versionId, + editor.viewModel.notebookDocument.cells.map((cell, index) => ({ + editType: CellEditType.Output, index, outputs: [] + })), true); } }); @@ -1543,7 +1557,27 @@ registerAction2(class extends NotebookCellAction { } }); -registerAction2(class extends NotebookCellAction { +abstract class ChangeNotebookCellMetadataAction extends NotebookCellAction { + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { + const cell = context.cell; + const textModel = context.notebookEditor.viewModel?.notebookDocument; + if (!textModel) { + return; + } + + const index = textModel.cells.indexOf(cell.model); + + if (index < 0) { + return; + } + + textModel.applyEdit(textModel.versionId, [{ editType: CellEditType.Metadata, index, metadata: { ...context.cell.metadata, ...this.getMetadataDelta() } }], true); + } + + abstract getMetadataDelta(): NotebookCellMetadata; +} + +registerAction2(class extends ChangeNotebookCellMetadataAction { constructor() { super({ id: COLLAPSE_CELL_INPUT_COMMAND_ID, @@ -1561,12 +1595,12 @@ registerAction2(class extends NotebookCellAction { }); } - async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { - context.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(context.cell.handle, { inputCollapsed: true }); + getMetadataDelta(): NotebookCellMetadata { + return { inputCollapsed: true }; } }); -registerAction2(class extends NotebookCellAction { +registerAction2(class extends ChangeNotebookCellMetadataAction { constructor() { super({ id: EXPAND_CELL_CONTENT_COMMAND_ID, @@ -1584,12 +1618,12 @@ registerAction2(class extends NotebookCellAction { }); } - async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { - context.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(context.cell.handle, { inputCollapsed: false }); + getMetadataDelta(): NotebookCellMetadata { + return { inputCollapsed: false }; } }); -registerAction2(class extends NotebookCellAction { +registerAction2(class extends ChangeNotebookCellMetadataAction { constructor() { super({ id: COLLAPSE_CELL_OUTPUT_COMMAND_ID, @@ -1607,12 +1641,12 @@ registerAction2(class extends NotebookCellAction { }); } - async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { - context.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(context.cell.handle, { outputCollapsed: true }); + getMetadataDelta(): NotebookCellMetadata { + return { outputCollapsed: true }; } }); -registerAction2(class extends NotebookCellAction { +registerAction2(class extends ChangeNotebookCellMetadataAction { constructor() { super({ id: EXPAND_CELL_OUTPUT_COMMAND_ID, @@ -1630,8 +1664,8 @@ registerAction2(class extends NotebookCellAction { }); } - async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { - context.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(context.cell.handle, { outputCollapsed: false }); + getMetadataDelta(): NotebookCellMetadata { + return { outputCollapsed: false }; } }); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/scm/scm.ts b/src/vs/workbench/contrib/notebook/browser/contrib/scm/scm.ts index 3b3601c8d7f..4c431ef0a48 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/scm/scm.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/scm/scm.ts @@ -43,11 +43,7 @@ export class SCMController extends Disposable implements INotebookEditorContribu this.update(); if (this._notebookEditor.textModel) { - this._localDisposable.add(this._notebookEditor.textModel.onDidChangeContent(() => { - this.update(); - })); - - this._localDisposable.add(this._notebookEditor.textModel.onDidChangeCells(() => { + this._localDisposable.add(this._notebookEditor.textModel.onDidChangeContent((e) => { this.update(); })); } diff --git a/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts index 6eace8686e1..327c3ee0721 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts @@ -17,7 +17,7 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { format } from 'vs/base/common/jsonFormatter'; import { applyEdits } from 'vs/base/common/jsonEdit'; -import { CellUri, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellEditType, CellUri, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { hash } from 'vs/base/common/hash'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; @@ -451,7 +451,15 @@ abstract class AbstractCellRenderer extends Disposable { if (newLangauge !== undefined && newLangauge !== this.cell.modified!.language) { this.notebookEditor.textModel!.changeCellLanguage(this.cell.modified!.handle, newLangauge); } - this.notebookEditor.textModel!.changeCellMetadata(this.cell.modified!.handle, result, false); + const index = this.notebookEditor.textModel!.cells.indexOf(this.cell.modified!); + + if (index < 0) { + return; + } + + this.notebookEditor.textModel!.applyEdit(this.notebookEditor.textModel!.versionId, [ + { editType: CellEditType.Metadata, index, metadata: result } + ], true); } catch { } } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index d6cfd69e03f..d0772635f8f 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -233,6 +233,7 @@ export interface INotebookEditor extends IEditor { getDomNode(): HTMLElement; getOverflowContainerDomNode(): HTMLElement; getInnerWebview(): Webview | undefined; + getSelectionHandles(): number[]; /** * Focus the notebook editor cell list diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 932fbeed8c8..c3041f339cd 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -215,6 +215,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._cursorNavigationMode = v; } + private readonly _onDidChangeSelection = this._register(new Emitter()); + get onDidChangeSelection(): Event { return this._onDidChangeSelection.event; } + private readonly _onDidChangeVisibleRanges = this._register(new Emitter()); onDidChangeVisibleRanges: Event = this._onDidChangeVisibleRanges.event; @@ -268,6 +271,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return this._uuid; } + getSelectionHandles(): number[] { + return this.viewModel?.selectionHandles || []; + } + hasModel() { return !!this._notebookViewModel; } @@ -933,6 +940,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } } + this._localStore.add(this.viewModel.onDidChangeSelection(() => { + this._onDidChangeSelection.fire(); + })); + this._localStore.add(this._list!.onWillScroll(e => { this._onWillScroll.fire(e); if (!this._webviewResolved) { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index 0e5f4b4bda5..fcffc8ee0fd 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -48,7 +48,7 @@ import { StatefulMarkdownCell } from 'vs/workbench/contrib/notebook/browser/view import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { CellKind, NotebookCellMetadata, NotebookCellRunState, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellEditType, CellKind, NotebookCellMetadata, NotebookCellRunState, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { createAndFillInActionBarActionsWithVerticalSeparators, VerticalSeparator, VerticalSeparatorViewItem } from './cellActionView'; const $ = DOM.$; @@ -332,10 +332,21 @@ abstract class AbstractCellRenderer { return; } + const textModel = this.notebookEditor.viewModel!.notebookDocument; + const index = textModel.cells.indexOf(templateData.currentRenderedCell.model); + + if (index < 0) { + return; + } + if (templateData.currentRenderedCell.metadata?.inputCollapsed) { - this.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(templateData.currentRenderedCell.handle, { inputCollapsed: false }); + textModel.applyEdit(textModel.versionId, [ + { editType: CellEditType.Metadata, index, metadata: { ...templateData.currentRenderedCell.metadata, inputCollapsed: false } } + ], true); } else if (templateData.currentRenderedCell.metadata?.outputCollapsed) { - this.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(templateData.currentRenderedCell.handle, { outputCollapsed: false }); + textModel.applyEdit(textModel.versionId, [ + { editType: CellEditType.Metadata, index, metadata: { ...templateData.currentRenderedCell.metadata, outputCollapsed: false } } + ], true); } })); } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts index 709c41859b2..aac728c93f2 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts @@ -52,12 +52,10 @@ export class JoinCellEdit implements IResourceUndoRedoElement { const cell = this.editingDelegate.createCellViewModel(this._deletedRawCell); if (this.direction === 'above') { - this.editingDelegate.insertCell(this.index, this._deletedRawCell); - this.editingDelegate.emitSelections([cell.handle]); + this.editingDelegate.insertCell(this.index, this._deletedRawCell, [cell.handle]); cell.focusMode = CellFocusMode.Editor; } else { - this.editingDelegate.insertCell(this.index, cell.model); - this.editingDelegate.emitSelections([this.cell.handle]); + this.editingDelegate.insertCell(this.index, cell.model, [this.cell.handle]); this.cell.focusMode = CellFocusMode.Editor; } } @@ -72,8 +70,7 @@ export class JoinCellEdit implements IResourceUndoRedoElement { { range: this.inverseRange, text: this.insertContent } ]); - this.editingDelegate.deleteCell(this.index); - this.editingDelegate.emitSelections([this.cell.handle]); + this.editingDelegate.deleteCell(this.index, [this.cell.handle]); this.cell.focusMode = CellFocusMode.Editor; } } @@ -110,10 +107,9 @@ export class SplitCellEdit implements IResourceUndoRedoElement { this.cell.setSelections(this.selections); for (let j = 1; j < this.cellContents.length; j++) { - this.editingDelegate.deleteCell(this.index + 1); + this.editingDelegate.deleteCell(this.index + 1, j === this.cellContents.length - 1 ? [this.cell.handle] : undefined); } - this.editingDelegate.emitSelections([this.cell.handle]); this.cell.focusMode = CellFocusMode.Editor; } @@ -134,7 +130,6 @@ export class SplitCellEdit implements IResourceUndoRedoElement { } if (lastCell) { - this.editingDelegate.emitSelections([lastCell.handle]); lastCell.focusMode = CellFocusMode.Editor; } } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts index 9be338a7cec..4553fdd0412 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts @@ -23,7 +23,7 @@ import { NotebookEventDispatcher, NotebookMetadataChangedEvent } from 'vs/workbe import { CellFoldingState, EditorFoldingStateDelegate } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { CellKind, NotebookCellMetadata, INotebookSearchOptions, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, NotebookCellMetadata, INotebookSearchOptions, ICellRange, NotebookCellsChangeType, ICell, NotebookCellTextModelSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { FoldingRegions } from 'vs/editor/contrib/folding/foldingRanges'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { MarkdownRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/mdRenderer'; @@ -214,7 +214,6 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD } this._selections = selections; - this._notebook.selections = selections; this._onDidChangeSelection.fire(); } @@ -248,8 +247,8 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD this.id = '$notebookViewModel' + MODEL_ID; this._instanceId = strings.singleLetterHash(MODEL_ID); - this._register(this._notebook.onDidChangeCells(e => { - const diffs = e.splices.map(splice => { + const compute = (changes: NotebookCellTextModelSplice[], synchronous: boolean) => { + const diffs = changes.map(splice => { return [splice[0], splice[1], splice[2].map(cell => { return createCellViewModel(this._instantiationService, this, cell as NotebookCellTextModel); })] as [number, number, CellViewModel[]]; @@ -272,7 +271,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD }); this._onDidChangeViewCells.fire({ - synchronous: e.synchronous, + synchronous: synchronous, splices: diffs }); @@ -303,17 +302,31 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD } this.selectionHandles = endSelectionHandles; + }; + + this._register(this._notebook.onDidChangeContent(e => { + let changes: NotebookCellTextModelSplice[] = []; + + if (e.kind === NotebookCellsChangeType.ModelChange || e.kind === NotebookCellsChangeType.Initialize) { + changes = e.changes; + compute(changes, e.synchronous); + return; + } else if (e.kind === NotebookCellsChangeType.Move) { + compute([[e.index, e.length, []]], e.synchronous); + compute([[e.newIdx, 0, e.cells]], e.synchronous); + } else { + return; + } })); - this._register(this._notebook.onDidChangeMetadata(e => { - this.eventDispatcher.emit([new NotebookMetadataChangedEvent(e)]); - })); + this._register(this._notebook.onDidChangeContent(e => { + if (e.kind === NotebookCellsChangeType.ChangeDocumentMetadata) { + this.eventDispatcher.emit([new NotebookMetadataChangedEvent(this._notebook.metadata)]); + } - this._register(this._notebook.emitSelections(selections => { - // text model emit selection change (for example, undo/redo) - // we should update the selection handle wisely - // TODO@rebornix, if the editor is note selected, undo/redo should not change the focused element selection - this.updateSelectionsFromEdits(selections); + if (e.endSelections) { + this.updateSelectionsFromEdits(e.endSelections); + } })); this._register(this.eventDispatcher.onDidChangeLayout((e) => { @@ -607,14 +620,15 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD createCell(index: number, source: string, language: string, type: CellKind, metadata: NotebookCellMetadata | undefined, synchronous: boolean, pushUndoStop: boolean = true, previouslyFocused: ICellViewModel[] = []) { const beforeSelections = previouslyFocused.map(e => e.handle); - this._notebook.createCell2(index, source, language, type, metadata, synchronous, pushUndoStop, beforeSelections, undefined); - // TODO@rebornix, rely on createCell to be sync + const cell = this._notebook.createCellTextModel(source, language, type, [], metadata); + this._notebook.insertCell(index, cell, synchronous, pushUndoStop, beforeSelections, undefined); + // TODO, rely on createCell to be sync return this.viewCells[index]; } insertCell(index: number, cell: NotebookCellTextModel, synchronous: boolean, pushUndoStop: boolean = true): CellViewModel { - this._notebook.insertCell2(index, cell, synchronous, pushUndoStop); - // TODO@rebornix, rely on createCell to be sync // this will trigger it to synchronous update + this._notebook.insertCell(index, cell, synchronous, pushUndoStop, undefined, undefined); + // TODO, rely on createCell to be sync // this will trigger it to synchronous update return this._viewCells[index]; } @@ -637,7 +651,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD } } - this._notebook.deleteCell2(index, synchronous, pushUndoStop, this.selectionHandles, endSelections); + this._notebook.deleteCell(index, synchronous, pushUndoStop, this.selectionHandles, endSelections); } /** @@ -654,7 +668,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD return false; } - this._notebook.moveCellToIdx2(index, length, newIdx, synchronous, pushedToUndoStack, undefined, [viewCell.handle]); + this._notebook.moveCellToIdx(index, length, newIdx, synchronous, pushedToUndoStack, undefined, [viewCell.handle]); return true; } @@ -757,9 +771,6 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD }, deleteCell: (index: number) => { this.deleteCell(index, true, false); - }, - emitSelections: (selections: number[]) => { - this.updateSelectionsFromEdits(selections); } } )); @@ -828,7 +839,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD insertContent, cell, { - insertCell: (index: number, cell: NotebookCellTextModel) => { + insertCell: (index: number, cell: NotebookCellTextModel, endSelections?: number[]) => { this.insertCell(index, cell, true, false); }, deleteCell: (index: number) => { @@ -836,9 +847,6 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD }, createCellViewModel: (cell: NotebookCellTextModel) => { return createCellViewModel(this._instantiationService, this, cell); - }, - emitSelections: (selections: number[]) => { - this.updateSelectionsFromEdits(selections); } }) ); @@ -882,7 +890,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD insertContent, below, { - insertCell: (index: number, cell: NotebookCellTextModel) => { + insertCell: (index: number, cell: NotebookCellTextModel, endSelections?: number[]) => { this.insertCell(index, cell, true, false); }, deleteCell: (index: number) => { @@ -890,9 +898,6 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD }, createCellViewModel: (cell: NotebookCellTextModel) => { return createCellViewModel(this._instantiationService, this, cell); - }, - 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 index 5cde6b3bfaa..9a9c8c2a3d6 100644 --- a/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts +++ b/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts @@ -12,11 +12,10 @@ import { NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/noteb * It should not modify Undo/Redo stack */ export interface ITextCellEditingDelegate { - insertCell?(index: number, cell: NotebookCellTextModel): void; - deleteCell?(index: number): void; + insertCell?(index: number, cell: NotebookCellTextModel, endSelections?: number[]): void; + deleteCell?(index: number, endSelections?: number[]): void; moveCell?(fromIndex: number, length: number, toIndex: number, beforeSelections: number[] | undefined, endSelections: number[] | undefined): void; updateCellMetadata?(index: number, newMetadata: NotebookCellMetadata): void; - emitSelections(selections: number[]): void; } @@ -38,20 +37,14 @@ export class InsertCellEdit implements IResourceUndoRedoElement { throw new Error('Notebook Delete Cell not implemented for Undo/Redo'); } - this.editingDelegate.deleteCell(this.insertIndex); - if (this.beforedSelections) { - this.editingDelegate.emitSelections(this.beforedSelections); - } + this.editingDelegate.deleteCell(this.insertIndex, this.beforedSelections); } redo(): void { 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); - } + this.editingDelegate.insertCell(this.insertIndex, this.cell, this.endSelections); } } @@ -77,10 +70,7 @@ export class DeleteCellEdit implements IResourceUndoRedoElement { 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); - } + this.editingDelegate.insertCell(this.insertIndex, this._cell, this.beforedSelections); } redo(): void { @@ -88,10 +78,7 @@ export class DeleteCellEdit implements IResourceUndoRedoElement { throw new Error('Notebook Delete Cell not implemented for Undo/Redo'); } - this.editingDelegate.deleteCell(this.insertIndex); - if (this.endSelections) { - this.editingDelegate.emitSelections(this.endSelections); - } + this.editingDelegate.deleteCell(this.insertIndex, this.endSelections); } } @@ -116,9 +103,6 @@ export class MoveCellEdit implements IResourceUndoRedoElement { } this.editingDelegate.moveCell(this.toIndex, this.length, this.fromIndex, this.endSelections, this.beforedSelections); - if (this.beforedSelections) { - this.editingDelegate.emitSelections(this.beforedSelections); - } } redo(): void { @@ -127,9 +111,6 @@ export class MoveCellEdit implements IResourceUndoRedoElement { } this.editingDelegate.moveCell(this.fromIndex, this.length, this.toIndex, this.beforedSelections, this.endSelections); - if (this.endSelections) { - this.editingDelegate.emitSelections(this.endSelections); - } } } @@ -152,17 +133,13 @@ export class SpliceCellsEdit implements IResourceUndoRedoElement { this.diffs.forEach(diff => { for (let i = 0; i < diff[2].length; i++) { - this.editingDelegate.deleteCell!(diff[0]); + this.editingDelegate.deleteCell!(diff[0], this.beforeHandles); } diff[1].reverse().forEach(cell => { - this.editingDelegate.insertCell!(diff[0], cell); + this.editingDelegate.insertCell!(diff[0], cell, this.beforeHandles); }); }); - - if (this.beforeHandles) { - this.editingDelegate.emitSelections(this.beforeHandles); - } } redo(): void { @@ -172,17 +149,13 @@ export class SpliceCellsEdit implements IResourceUndoRedoElement { this.diffs.reverse().forEach(diff => { for (let i = 0; i < diff[1].length; i++) { - this.editingDelegate.deleteCell!(diff[0]); + this.editingDelegate.deleteCell!(diff[0], this.endHandles); } diff[2].reverse().forEach(cell => { - this.editingDelegate.insertCell!(diff[0], cell); + this.editingDelegate.insertCell!(diff[0], cell, this.endHandles); }); }); - - if (this.endHandles) { - this.editingDelegate.emitSelections(this.endHandles); - } } } diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index 9b1b09e1675..5537f5d9f15 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -8,7 +8,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; 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, NotebookCellsChangedEvent, CellKind, IProcessedOutput, notebookDocumentMetadataDefaults, diff, NotebookCellsChangeType, ICellDto2, IMainCellDto, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookTextModel, NotebookCellOutputsSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, CellEditType, CellUri, CellKind, IProcessedOutput, notebookDocumentMetadataDefaults, diff, NotebookCellsChangeType, ICellDto2, TransientOptions, NotebookTextModelChangedEvent } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ITextSnapshot } from 'vs/editor/common/model'; import { IUndoRedoService, UndoRedoElementType, IUndoRedoElement, IResourceUndoRedoElement } from 'vs/platform/undoRedo/common/undoRedo'; import { InsertCellEdit, DeleteCellEdit, MoveCellEdit, SpliceCellsEdit, CellMetadataEdit } from 'vs/workbench/contrib/notebook/common/model/cellEdit'; @@ -101,17 +101,44 @@ export class NotebookOperationManager { } } +class DelayedEmitter { + constructor( + private readonly _onDidChangeContent: Emitter, + private readonly _increaseVersion: () => void, + private readonly _textModel: NotebookTextModel + + ) { + + } + + emit(data: { + triggerDirty: { value: boolean } | undefined, + modelContentChange: { value: NotebookTextModelChangedEvent } | undefined, + }) { + this._increaseVersion(); + + if (data.triggerDirty) { + this._textModel.setDirty(data.triggerDirty.value); + } + + if (data.modelContentChange) { + this._onDidChangeContent.fire( + { + ...data.modelContentChange.value, + versionId: this._textModel.versionId + } + ); + } + } +} + export class NotebookTextModel extends Disposable implements INotebookTextModel { private readonly _onWillDispose: Emitter = this._register(new Emitter()); - private readonly _onDidChangeCells = this._register(new Emitter<{ synchronous: boolean, splices: NotebookCellTextModelSplice[] }>()); - private readonly _onDidModelChangeProxy = this._register(new Emitter()); - private readonly _onDidChangeContent = this._register(new Emitter()); + private readonly _onDidChangeContent = this._register(new Emitter()); private readonly _onDidChangeMetadata = this._register(new Emitter()); readonly onWillDispose: Event = this._onWillDispose.event; - readonly onDidChangeCells = this._onDidChangeCells.event; - readonly onDidModelChangeProxy = this._onDidModelChangeProxy.event; readonly onDidChangeContent = this._onDidChangeContent.event; readonly onDidChangeMetadata = this._onDidChangeMetadata.event; @@ -148,24 +175,14 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel readonly onDidSelectionChange = this._onDidSelectionChangeProxy.event; - private _selections: number[] = []; - - get selections() { - return this._selections; - } - - set selections(selections: number[]) { - this._selections = selections; - this._onDidSelectionChangeProxy.fire(this._selections); - } - - //#endregion + private _operationManager: NotebookOperationManager; + private _eventEmitter: DelayedEmitter; private _dirty = false; protected readonly _onDidChangeDirty = this._register(new Emitter()); readonly onDidChangeDirty = this._onDidChangeDirty.event; - private readonly _operationManager: NotebookOperationManager; + constructor( readonly viewType: string, @@ -179,6 +196,11 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._cells = []; this._operationManager = new NotebookOperationManager(this._undoService, uri); + this._eventEmitter = new DelayedEmitter( + this._onDidChangeContent, + () => { this._increaseVersionId(); }, + this + ); } dispose() { @@ -236,7 +258,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel const dirtyStateListener = mainCells[i].onDidChangeContent(() => { this.setDirty(true); this._increaseVersionId(); - this._onDidChangeContent.fire(); + this._onDidChangeContent.fire({ kind: NotebookCellsChangeType.ChangeCellContent, versionId: this.versionId, synchronous: true }); }); this._cellListeners.set(mainCells[i].handle, dirtyStateListener); @@ -250,14 +272,21 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._operationManager.pushStackElement(label); } + pushEditOperations(modelVersionId: number, rawEdits: ICellEditOperation[], synchronous: boolean) { + // this._eventEmitter.beginDeferredEmit(); + this.pushStackElement('edit'); + // every edit should push a pending element into `edit` undo stack element. + this.applyEdit(modelVersionId, rawEdits, synchronous); + this.pushStackElement('edit'); + // this._eventEmitter.endDeferredEmit(); + + } + applyEdit(modelVersionId: number, rawEdits: ICellEditOperation[], synchronous: boolean): boolean { if (modelVersionId !== this._versionId) { return false; } - const oldViewCells = this._cells.slice(0); - const oldMap = new Map(this._mapping); - const edits = rawEdits.map((edit, index) => { return { edit, @@ -271,65 +300,34 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel for (const { edit } of edits) { switch (edit.editType) { case CellEditType.Replace: - this._replaceCells(edit.index, edit.count, edit.cells); + this._replaceCells(edit.index, edit.count, edit.cells, synchronous); break; case CellEditType.Output: //TODO@joh,@rebornix no event, no undo stop (?) - this._assertCellIndex(edit.index); + this._assertIndex(edit.index); const cell = this._cells[edit.index]; - this.spliceNotebookCellOutputs(cell.handle, [[0, cell.outputs.length, edit.outputs]]); + // TODO@rebornix, we should do diff first + this._spliceNotebookCellOutputs(cell.handle, [[0, cell.outputs.length, edit.outputs]]); break; case CellEditType.Metadata: - this._assertCellIndex(edit.index); - this.deltaCellMetadata(this._cells[edit.index].handle, edit.metadata); + this._assertIndex(edit.index); + this._changeCellMetadata(this._cells[edit.index].handle, edit.metadata, true); break; } } - const diffs = diff(oldViewCells, this._cells, cell => { - return oldMap.has(cell.handle); - }).map(diff => { - return [diff.start, diff.deleteCount, diff.toInsert] as [number, number, NotebookCellTextModel[]]; - }); - - this._onDidModelChangeProxy.fire({ - kind: NotebookCellsChangeType.ModelChange, - versionId: this._versionId, - changes: diffs.map(diff => [diff[0], diff[1], diff[2].map(cell => ({ - handle: cell.handle, - uri: cell.uri, - source: cell.textBuffer.getLinesContent(), - eol: cell.textBuffer.getEOL(), - language: cell.language, - cellKind: cell.cellKind, - outputs: cell.outputs, - metadata: cell.metadata - }))] as [number, number, IMainCellDto[]]) - }); - - 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._operationManager.pushEditOperation(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; } - private _replaceCells(index: number, count: number, cellDtos: ICellDto2[]): void { + private _replaceCells(index: number, count: number, cellDtos: ICellDto2[], synchronous: boolean): void { if (count === 0 && cellDtos.length === 0) { return; } - this._isUntitled = false; + this._isUntitled = false; //TODO@rebornix fishy? + const oldViewCells = this._cells.slice(0); + const oldMap = new Map(this._mapping); // prepare remove for (let i = index; i < index + count; i++) { @@ -348,9 +346,10 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._modelService ); const dirtyStateListener = cell.onDidChangeContent(() => { - this.setDirty(true); - this._increaseVersionId(); - this._onDidChangeContent.fire(); + this._eventEmitter.emit({ + triggerDirty: { value: true }, + modelContentChange: { value: { kind: NotebookCellsChangeType.ChangeCellContent, versionId: this.versionId, synchronous: true } } + }); }); this._cellListeners.set(cell.handle, dirtyStateListener); this._mapping.set(cell.handle, cell); @@ -359,9 +358,35 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel // make change this._cells.splice(index, count, ...cells); - this.setDirty(true); - this._increaseVersionId(); - this._onDidChangeContent.fire(); + const diffs = diff(oldViewCells, this._cells, cell => { + return oldMap.has(cell.handle); + }).map(diff => { + return [diff.start, diff.deleteCount, diff.toInsert] as [number, number, NotebookCellTextModel[]]; + }); + + 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._operationManager.pushEditOperation(new SpliceCellsEdit(this.uri, undoDiff, { + insertCell: (index, cell, endSelections?: number[]) => { this._insertNewCell(index, [cell], true, endSelections); }, + deleteCell: (index, endSelections?: number[]) => { this._removeCell(index, 1, true, endSelections); }, + }, undefined, undefined)); + + // should be deferred + this._eventEmitter.emit({ + triggerDirty: { value: true }, + modelContentChange: { + value: { + kind: NotebookCellsChangeType.ModelChange, + versionId: this._versionId, + changes: diffs, + synchronous + } + } + }); } handleEdit(label: string | undefined, undo: () => void, redo: () => void): void { @@ -404,7 +429,10 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel updateNotebookMetadata(metadata: NotebookDocumentMetadata) { this.metadata = metadata; - this._onDidChangeMetadata.fire(this.metadata); + this._eventEmitter.emit({ + triggerDirty: { value: true }, + modelContentChange: { value: { kind: NotebookCellsChangeType.ChangeDocumentMetadata, versionId: this.versionId, metadata: this.metadata, synchronous: true } } + }); } insertTemplateCell(cell: NotebookCellTextModel) { @@ -418,82 +446,83 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel const dirtyStateListener = cell.onDidChangeContent(() => { this._isUntitled = false; - this.setDirty(true); - this._increaseVersionId(); - this._onDidChangeContent.fire(); + this._eventEmitter.emit({ + triggerDirty: { value: true }, + modelContentChange: { value: { kind: NotebookCellsChangeType.ChangeCellContent, versionId: this.versionId, synchronous: true } } + }); }); this._cellListeners.set(cell.handle, dirtyStateListener); this.setDirty(false); - this._onDidChangeContent.fire(); - this._onDidModelChangeProxy.fire({ + // this._eventEmitter.emit({ + // triggerDirty: undefined, + // modelContentChange: { value: { + // kind: NotebookCellsChangeType.ModelChange, + // versionId: this._versionId, changes: + // [[ + // 0, + // 0, + // [cell] + // ]], + // synchronous: true + // } } + // }); + + this.setDirty(false); + // this._onDidChangeContent.fire(NotebookCellsChangeType.ModelChange); + + this._onDidChangeContent.fire({ kind: NotebookCellsChangeType.ModelChange, versionId: this._versionId, changes: [[ 0, 0, - [{ - handle: cell.handle, - uri: cell.uri, - source: cell.textBuffer.getLinesContent(), - eol: cell.textBuffer.getEOL(), - language: cell.language, - cellKind: cell.cellKind, - outputs: cell.outputs, - metadata: cell.metadata - }] - ]] + [cell] + ]], + synchronous: true }); return; } - insertNewCell(index: number, cells: NotebookCellTextModel[], emitToExtHost: boolean = true): void { + private _insertNewCell(index: number, cells: NotebookCellTextModel[], synchronous: boolean, endSelections?: number[]): void { this._isUntitled = false; for (let i = 0; i < cells.length; i++) { this._mapping.set(cells[i].handle, cells[i]); const dirtyStateListener = cells[i].onDidChangeContent(() => { - this.setDirty(true); - this._increaseVersionId(); - this._onDidChangeContent.fire(); + this._eventEmitter.emit({ + triggerDirty: { value: true }, + modelContentChange: { value: { kind: NotebookCellsChangeType.ChangeCellContent, versionId: this.versionId, synchronous: true } } + }); }); this._cellListeners.set(cells[i].handle, dirtyStateListener); } this._cells.splice(index, 0, ...cells); - this.setDirty(true); - this._onDidChangeContent.fire(); - - this._increaseVersionId(); - - if (emitToExtHost) { - this._onDidModelChangeProxy.fire({ - kind: NotebookCellsChangeType.ModelChange, - versionId: this._versionId, changes: - [[ - index, - 0, - cells.map(cell => ({ - handle: cell.handle, - uri: cell.uri, - source: cell.textBuffer.getLinesContent(), - eol: cell.textBuffer.getEOL(), - language: cell.language, - cellKind: cell.cellKind, - outputs: cell.outputs, - metadata: cell.metadata - })) - ]] - }); - } + this._eventEmitter.emit({ + triggerDirty: { value: true }, + modelContentChange: { + value: { + kind: NotebookCellsChangeType.ModelChange, + versionId: this._versionId, changes: + [[ + index, + 0, + cells + ]], + synchronous, + endSelections: endSelections + } + } + }); return; } - removeCell(index: number, count: number, emitToExtHost: boolean = true) { + private _removeCell(index: number, count: number, synchronous: boolean, endSelections?: number[]) { this._isUntitled = false; for (let i = index; i < index + count; i++) { @@ -502,80 +531,10 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._cellListeners.delete(cell.handle); } this._cells.splice(index, count); - this.setDirty(true); - this._onDidChangeContent.fire(); - - this._increaseVersionId(); - if (emitToExtHost) { - this._onDidModelChangeProxy.fire({ kind: NotebookCellsChangeType.ModelChange, versionId: this._versionId, changes: [[index, count, []]] }); - } - } - - moveCellToIdx(index: number, length: number, newIdx: number, emitToExtHost: boolean = true) { - this._assertCellIndex(index); - this._assertCellIndex(newIdx); - - const cells = this._cells.splice(index, length); - this._cells.splice(newIdx, 0, ...cells); - this.setDirty(true); - this._onDidChangeContent.fire(); - - this._increaseVersionId(); - - if (emitToExtHost) { - this._onDidModelChangeProxy.fire({ kind: NotebookCellsChangeType.Move, versionId: this._versionId, index, newIdx }); - } - } - - private _assertCellIndex(index: number): void { - if (index < 0 || index >= this._cells.length) { - throw new Error(`model index out of range ${index}`); - } - } - - // TODO@rebornix should this trigger content change event? - spliceNotebookCellOutputs(cellHandle: number, splices: NotebookCellOutputsSplice[]): void { - const cell = this._mapping.get(cellHandle); - if (cell) { - - cell.spliceNotebookCellOutputs(splices); - - if (!this.transientOptions.transientOutputs) { - this._increaseVersionId(); - this.setDirty(true); - this._onDidChangeContent.fire(); - } - - this._onDidModelChangeProxy.fire({ - kind: NotebookCellsChangeType.Output, - versionId: this.versionId, - index: this._cells.indexOf(cell), - outputs: cell.outputs ?? [] - }); - } - - } - - clearCellOutput(handle: number) { - const cell = this._mapping.get(handle); - if (cell) { - cell.spliceNotebookCellOutputs([ - [0, cell.outputs.length, []] - ]); - - this._increaseVersionId(); - this._onDidModelChangeProxy.fire({ kind: NotebookCellsChangeType.CellClearOutput, versionId: this._versionId, index: this._cells.indexOf(cell) }); - } - } - - changeCellLanguage(handle: number, languageId: string) { - const cell = this._mapping.get(handle); - if (cell && cell.language !== languageId) { - cell.language = languageId; - - this._increaseVersionId(); - this._onDidModelChangeProxy.fire({ kind: NotebookCellsChangeType.ChangeLanguage, versionId: this._versionId, index: this._cells.indexOf(cell), language: languageId }); - } + this._eventEmitter.emit({ + triggerDirty: { value: true }, + modelContentChange: { value: { kind: NotebookCellsChangeType.ModelChange, versionId: this._versionId, changes: [[index, count, []]], synchronous, endSelections } } + }); } private _isCellMetadataChanged(a: NotebookCellMetadata, b: NotebookCellMetadata) { @@ -593,7 +552,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return false; } - changeCellMetadata(handle: number, metadata: NotebookCellMetadata, pushUndoStop: boolean) { + private _changeCellMetadata(handle: number, metadata: NotebookCellMetadata, pushUndoStop: boolean) { const cell = this._cells.find(cell => cell.handle === handle); if (!cell) { @@ -611,128 +570,109 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel if (!cell) { return; } - this.changeCellMetadata(cell.handle, newMetadata, false); - }, - emitSelections: this._emitSelectionsDelegate.bind(this) + this._changeCellMetadata(cell.handle, newMetadata, false); + } })); } + // should be deferred cell.metadata = metadata; - this.setDirty(true); - this._onDidChangeContent.fire(); } else { cell.metadata = metadata; } - this._increaseVersionId(); - this._onDidModelChangeProxy.fire({ kind: NotebookCellsChangeType.ChangeMetadata, versionId: this._versionId, index: this._cells.indexOf(cell), metadata: cell.metadata }); - } - - deltaCellMetadata(handle: number, newMetadata: NotebookCellMetadata) { - const cell = this._mapping.get(handle); - if (cell) { - this.changeCellMetadata(handle, { - ...cell.metadata, - ...newMetadata - }, true); - } - } - - clearAllCellOutputs() { - this._cells.forEach(cell => { - cell.spliceNotebookCellOutputs([ - [0, cell.outputs.length, []] - ]); + this._eventEmitter.emit({ + triggerDirty: { value: triggerDirtyChange }, + modelContentChange: { + value: { kind: NotebookCellsChangeType.ChangeCellMetadata, versionId: this._versionId, index: this._cells.indexOf(cell), metadata: cell.metadata, synchronous: true } + } }); - this._increaseVersionId(); - this._onDidModelChangeProxy.fire({ kind: NotebookCellsChangeType.CellsClearOutput, versionId: this._versionId }); + } + + // TODO@rebornix, once adopted the new Edit API in ext host, the method should be private. + _spliceNotebookCellOutputs(cellHandle: number, splices: NotebookCellOutputsSplice[]): void { + const cell = this._mapping.get(cellHandle); + if (cell) { + cell.spliceNotebookCellOutputs(splices); + + this._eventEmitter.emit({ + triggerDirty: { value: !this.transientOptions.transientOutputs }, + modelContentChange: { + value: { + kind: NotebookCellsChangeType.Output, + versionId: this.versionId, + index: this._cells.indexOf(cell), + outputs: cell.outputs ?? [], + transient: this.transientOptions.transientOutputs, + synchronous: true + } + } + }); + } + + } + + private _assertIndex(index: number) { + if (index < 0 || index >= this._cells.length) { + throw new Error(`model index out of range ${index}`); + } } //#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]]] }); + changeCellLanguage(handle: number, languageId: string) { + const cell = this._mapping.get(handle); + if (cell && cell.language !== languageId) { + cell.language = languageId; + + this._eventEmitter.emit({ + triggerDirty: undefined, + modelContentChange: { value: { kind: NotebookCellsChangeType.ChangeLanguage, versionId: this._versionId, index: this._cells.indexOf(cell), language: languageId, synchronous: true } } + }); + } } - 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, 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); - + insertCell(index: number, cell: NotebookCellTextModel, synchronous: boolean, pushUndoStop: boolean, beforeSelections: number[] | undefined, endSelections: number[] | undefined): void { if (pushUndoStop) { this._operationManager.pushEditOperation(new InsertCellEdit(this.uri, index, cell, { - insertCell: this._insertCellDelegate.bind(this), - deleteCell: this._deleteCellDelegate.bind(this), - emitSelections: this._emitSelectionsDelegate.bind(this) + insertCell: (index, cell, endSelections) => { this._insertNewCell(index, [cell], true, endSelections); }, + deleteCell: (index, endSelections) => { this._removeCell(index, 1, true, endSelections); }, }, beforeSelections, endSelections)); } - - this.insertNewCell(index, [cell]); - - this._onDidChangeCells.fire({ synchronous, splices: [[index, 0, [cell]]] }); - - if (endSelections) { - this._emitSelections.fire(endSelections); - } - return cell; + this._insertNewCell(index, [cell], synchronous, endSelections); } - insertCell2(index: number, cell: NotebookCellTextModel, synchronous: boolean, pushUndoStop: boolean): void { - if (pushUndoStop) { - this._operationManager.pushEditOperation(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) { + deleteCell(index: number, synchronous: boolean, pushUndoStop: boolean, beforeSelections: number[] | undefined, endSelections: number[] | undefined) { const cell = this._cells[index]; if (pushUndoStop) { this._operationManager.pushEditOperation(new DeleteCellEdit(this.uri, index, cell, { - insertCell: this._insertCellDelegate.bind(this), - deleteCell: this._deleteCellDelegate.bind(this), - emitSelections: this._emitSelectionsDelegate.bind(this) + insertCell: (index, cell, endSelections) => { this._insertNewCell(index, [cell], true, endSelections); }, + deleteCell: (index, endSelections) => { this._removeCell(index, 1, true, endSelections); }, }, beforeSelections, endSelections)); } - this.removeCell(index, 1); - this._onDidChangeCells.fire({ synchronous: synchronous, splices: [[index, 1, []]] }); - if (endSelections) { - this._emitSelections.fire(endSelections); - } + this._removeCell(index, 1, synchronous, endSelections); } - moveCellToIdx2(index: number, length: number, newIdx: number, synchronous: boolean, pushedToUndoStack: boolean, beforeSelections: number[] | undefined, endSelections: number[] | undefined): boolean { - const cells = this._cells.slice(index, index + length); + moveCellToIdx(index: number, length: number, newIdx: number, synchronous: boolean, pushedToUndoStack: boolean, beforeSelections: number[] | undefined, endSelections: number[] | undefined): boolean { if (pushedToUndoStack) { this._operationManager.pushEditOperation(new MoveCellEdit(this.uri, index, length, newIdx, { moveCell: (fromIndex: number, length: number, toIndex: number, beforeSelections: number[] | undefined, endSelections: number[] | undefined) => { - this.moveCellToIdx2(fromIndex, length, toIndex, true, false, beforeSelections, endSelections); + this.moveCellToIdx(fromIndex, length, toIndex, true, false, beforeSelections, endSelections); }, - emitSelections: this._emitSelectionsDelegate.bind(this) }, beforeSelections, endSelections)); } - this.moveCellToIdx(index, length, 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, length, []]] }); - this._onDidChangeCells.fire({ synchronous: synchronous, splices: [[newIdx, 0, cells]] }); - if (endSelections) { - this._emitSelections.fire(endSelections); + this._assertIndex(index); + this._assertIndex(newIdx); + + { + const cells = this._cells.splice(index, length); + this._cells.splice(newIdx, 0, ...cells); + this._eventEmitter.emit({ + triggerDirty: { value: true }, + modelContentChange: { value: { kind: NotebookCellsChangeType.Move, versionId: this._versionId, index, length, newIdx, cells, synchronous, endSelections } } + }); } return true; @@ -756,11 +696,9 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel 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); + const cell = this.createCellTextModel(newLinesContents[j], language, kind, [], undefined); + this.insertCell(insertIndex, cell, true, false, undefined, j === newLinesContents.length - 1 ? endSelections : undefined); + newCells.push(cell); } } //#endregion diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 7a9ffe3d680..75b8abd0a88 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -276,8 +276,6 @@ export interface INotebookTextModel { readonly versionId: number; languages: string[]; readonly cells: readonly ICell[]; - onDidChangeCells: Event<{ synchronous: boolean, splices: NotebookCellTextModelSplice[] }>; - onDidChangeContent: Event; onWillDispose(listener: () => void): IDisposable; } @@ -311,10 +309,10 @@ export type IRenderOutput = IRenderNoOutput | IInsetRenderOutput; export const outputHasDynamicHeight = (o: IRenderOutput) => o.type !== RenderOutputType.Extension && o.hasDynamicHeight; -export type NotebookCellTextModelSplice = [ +export type NotebookCellTextModelSplice = [ number /* start */, number, - ICell[] + T[] ]; export type NotebookCellOutputsSplice = [ @@ -347,26 +345,35 @@ export enum NotebookCellsChangeType { CellsClearOutput = 4, ChangeLanguage = 5, Initialize = 6, - ChangeMetadata = 7, + ChangeCellMetadata = 7, Output = 8, + ChangeCellContent = 9, + ChangeDocumentMetadata = 10 } -export interface NotebookCellsInitializeEvent { +export interface NotebookCellsInitializeEvent { readonly kind: NotebookCellsChangeType.Initialize; - readonly changes: NotebookCellsSplice2[]; + readonly changes: NotebookCellTextModelSplice[]; readonly versionId: number; } -export interface NotebookCellsModelChangedEvent { +export interface NotebookCellContentChangeEvent { + readonly kind: NotebookCellsChangeType.ChangeCellContent; + readonly versionId: number; +} + +export interface NotebookCellsModelChangedEvent { readonly kind: NotebookCellsChangeType.ModelChange; - readonly changes: NotebookCellsSplice2[]; + readonly changes: NotebookCellTextModelSplice[]; readonly versionId: number; } -export interface NotebookCellsModelMoveEvent { +export interface NotebookCellsModelMoveEvent { readonly kind: NotebookCellsChangeType.Move; readonly index: number; + readonly length: number; readonly newIdx: number; + readonly cells: T[]; readonly versionId: number; } @@ -375,17 +382,7 @@ export interface NotebookOutputChangedEvent { readonly index: number; readonly versionId: number; readonly outputs: IProcessedOutput[]; -} - -export interface NotebookCellClearOutputEvent { - readonly kind: NotebookCellsChangeType.CellClearOutput; - readonly index: number; - readonly versionId: number; -} - -export interface NotebookCellsClearOutputEvent { - readonly kind: NotebookCellsChangeType.CellsClearOutput; - readonly versionId: number; + readonly transient: boolean; } export interface NotebookCellsChangeLanguageEvent { @@ -396,13 +393,20 @@ export interface NotebookCellsChangeLanguageEvent { } export interface NotebookCellsChangeMetadataEvent { - readonly kind: NotebookCellsChangeType.ChangeMetadata; + readonly kind: NotebookCellsChangeType.ChangeCellMetadata; readonly versionId: number; readonly index: number; readonly metadata: NotebookCellMetadata | undefined; } -export type NotebookCellsChangedEvent = NotebookCellsInitializeEvent | NotebookCellsModelChangedEvent | NotebookCellsModelMoveEvent | NotebookOutputChangedEvent | NotebookCellClearOutputEvent | NotebookCellsClearOutputEvent | NotebookCellsChangeLanguageEvent | NotebookCellsChangeMetadataEvent; +export interface NotebookDocumentChangeMetadataEvent { + readonly kind: NotebookCellsChangeType.ChangeDocumentMetadata; + readonly versionId: number; + readonly metadata: NotebookDocumentMetadata | undefined; +} + +export type NotebookCellsChangedEventDto = NotebookCellsInitializeEvent | NotebookDocumentChangeMetadataEvent | NotebookCellContentChangeEvent | NotebookCellsModelChangedEvent | NotebookCellsModelMoveEvent | NotebookOutputChangedEvent | NotebookCellsChangeLanguageEvent | NotebookCellsChangeMetadataEvent; +export type NotebookTextModelChangedEvent = (NotebookCellsInitializeEvent | NotebookDocumentChangeMetadataEvent | NotebookCellContentChangeEvent | NotebookCellsModelChangedEvent | NotebookCellsModelMoveEvent | NotebookOutputChangedEvent | NotebookCellsChangeLanguageEvent | NotebookCellsChangeMetadataEvent) & { synchronous: boolean; endSelections?: number[] }; export const enum CellEditType { Replace = 1, @@ -687,6 +691,8 @@ export interface IEditor extends editorCommon.ICompositeCodeEditor { readonly onDidChangeModel: Event; readonly onDidFocusEditorWidget: Event; readonly onDidChangeVisibleRanges: Event; + readonly onDidChangeSelection: Event; + getSelectionHandles(): number[]; isNotebookEditor: boolean; visibleRanges: ICellRange[]; uri?: URI; diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index 9e3a0c944a4..bc6ee9afeb8 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { EditorModel, IRevertOptions } from 'vs/workbench/common/editor'; import { Emitter, Event } from 'vs/base/common/event'; -import { INotebookEditorModel, NotebookDocumentBackupData } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookEditorModel, NotebookCellsChangeType, NotebookDocumentBackupData } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { URI } from 'vs/base/common/uri'; @@ -145,9 +145,12 @@ export class NotebookEditorModel extends EditorModel implements INotebookEditorM this._register(this._notebook); - this._register(this._notebook.onDidChangeContent(() => { - this._onDidChangeContent.fire(); + this._register(this._notebook.onDidChangeContent(e => { + if (e.kind !== NotebookCellsChangeType.Initialize) { + this._onDidChangeContent.fire(); + } })); + this._register(this._notebook.onDidChangeDirty(() => { this._onDidChangeDirty.fire(); })); diff --git a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts index 5e2037d1a14..92eae8a9b4a 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts @@ -69,18 +69,18 @@ suite('NotebookViewModel', () => { ['//c', 'javascript', CellKind.Code, [], { editable: true }], ], (editor, viewModel) => { - viewModel.moveCellToIdx(0, 1, 0, false); + viewModel.moveCellToIdx(0, 1, 0, true); // no-op assert.equal(viewModel.viewCells[0].getText(), '//a'); assert.equal(viewModel.viewCells[1].getText(), '//b'); - viewModel.moveCellToIdx(0, 1, 1, false); + viewModel.moveCellToIdx(0, 1, 1, true); // b, a, c assert.equal(viewModel.viewCells[0].getText(), '//b'); assert.equal(viewModel.viewCells[1].getText(), '//a'); assert.equal(viewModel.viewCells[2].getText(), '//c'); - viewModel.moveCellToIdx(0, 1, 2, false); + viewModel.moveCellToIdx(0, 1, 2, true); // a, c, b assert.equal(viewModel.viewCells[0].getText(), '//a'); assert.equal(viewModel.viewCells[1].getText(), '//c'); @@ -100,12 +100,12 @@ suite('NotebookViewModel', () => { ['//c', 'javascript', CellKind.Code, [], { editable: true }], ], (editor, viewModel) => { - viewModel.moveCellToIdx(1, 1, 0, false); + viewModel.moveCellToIdx(1, 1, 0, true); // b, a, c assert.equal(viewModel.viewCells[0].getText(), '//b'); assert.equal(viewModel.viewCells[1].getText(), '//a'); - viewModel.moveCellToIdx(2, 1, 0, false); + viewModel.moveCellToIdx(2, 1, 0, true); // c, b, a assert.equal(viewModel.viewCells[0].getText(), '//c'); assert.equal(viewModel.viewCells[1].getText(), '//b'); diff --git a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts index adb0abc97d0..31eab292f31 100644 --- a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts @@ -66,6 +66,9 @@ export class TestNotebookEditor implements INotebookEditor { constructor( ) { } + getSelectionHandles(): number[] { + return []; + } setOptions(options: NotebookEditorOptions | undefined): Promise { @@ -82,6 +85,7 @@ export class TestNotebookEditor implements INotebookEditor { onDidScroll = new Emitter().event; onWillDispose = new Emitter().event; onDidChangeVisibleRanges: Event = new Emitter().event; + onDidChangeSelection: Event = new Emitter().event; visibleRanges: ICellRange[] = []; uri?: URI | undefined; textModel?: NotebookTextModel | undefined; @@ -333,7 +337,7 @@ export class NotebookEditorTestModel extends EditorModel implements INotebookEdi ) { super(); - if (_notebook && _notebook.onDidChangeCells) { + if (_notebook && _notebook.onDidChangeContent) { this._register(_notebook.onDidChangeContent(() => { this._dirty = true; this._onDidChangeDirty.fire();