diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index e81026ca925..6cf1d4c474d 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -4,108 +4,20 @@ *--------------------------------------------------------------------------------------------*/ import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; -import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IExtHostContext, ExtHostNotebookShape, ExtHostContext } from '../common/extHost.protocol'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IExtHostContext, ExtHostNotebookShape, ExtHostContext, ICellDto } from '../common/extHost.protocol'; +import { Disposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; import { INotebookService, IMainNotebookController } from 'vs/workbench/contrib/notebook/browser/notebookService'; -import { Emitter, Event } from 'vs/base/common/event'; -import { ICell, IOutput, INotebook, INotebookMimeTypeSelector, NOTEBOOK_DISPLAY_ORDER, NotebookCellsSplice, NotebookCellOutputsSplice, CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ICell, INotebookTextModel, INotebookMimeTypeSelector, NOTEBOOK_DISPLAY_ORDER, NotebookCellsSplice, NotebookCellOutputsSplice, CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { PieceTreeTextBufferFactory, PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; +import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -export class MainThreadCell implements ICell { - private _onDidChangeOutputs = new Emitter(); - onDidChangeOutputs: Event = this._onDidChangeOutputs.event; +export class MainThreadNotebookDocument extends Disposable { + private _textModel: NotebookTextModel; - private _onDidChangeDirtyState = new Emitter(); - onDidChangeDirtyState: Event = this._onDidChangeDirtyState.event; - - private _outputs: IOutput[]; - - get outputs(): IOutput[] { - return this._outputs; - } - - private _isDirty: boolean = false; - - get isDirty() { - return this._isDirty; - } - - set isDirty(newState: boolean) { - this._isDirty = newState; - this._onDidChangeDirtyState.fire(newState); - } - - get source() { - return this._source; - } - - set source(newValue: string[]) { - this._source = newValue; - this._buffer = null; - } - - private _buffer: PieceTreeTextBufferFactory | null = null; - - constructor( - readonly uri: URI, - public handle: number, - private _source: string[], - public language: string, - public cellKind: CellKind, - outputs: IOutput[] - ) { - this._outputs = outputs; - } - - spliceNotebookCellOutputs(splices: NotebookCellOutputsSplice[]): void { - splices.reverse().forEach(splice => { - this.outputs.splice(splice[0], splice[1], ...splice[2]); - }); - - this._onDidChangeOutputs.fire(splices); - } - - save() { - this._isDirty = false; - } - - resolveTextBufferFactory(): PieceTreeTextBufferFactory { - if (this._buffer) { - return this._buffer; - } - - let builder = new PieceTreeTextBufferBuilder(); - builder.acceptChunk(this.source.join('\n')); - this._buffer = builder.finish(true); - return this._buffer; - } -} - -export class MainThreadNotebookDocument extends Disposable implements INotebook { - 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 _onDidChangeDirtyState = new Emitter(); - onDidChangeDirtyState: Event = this._onDidChangeDirtyState.event; - private _mapping: Map = new Map(); - private _cellListeners: Map = new Map(); - cells: MainThreadCell[]; - activeCell: MainThreadCell | undefined; - languages: string[] = []; - renderers = new Set(); - - private _isDirty: boolean = false; - - get isDirty() { - return this._isDirty; - } - - set isDirty(newState: boolean) { - this._isDirty = newState; - this._onDidChangeDirtyState.fire(newState); + get textModel() { + return this._textModel; } constructor( @@ -115,35 +27,13 @@ export class MainThreadNotebookDocument extends Disposable implements INotebook public uri: URI ) { super(); - this.cells = []; + this._textModel = new NotebookTextModel(handle, viewType, uri); } - updateLanguages(languages: string[]) { - this.languages = languages; - } - - updateRenderers(renderers: number[]) { - renderers.forEach(render => { - this.renderers.add(render); - }); - } - - updateActiveCell(handle: number) { - this.activeCell = this._mapping.get(handle); - } - - async createRawCell(viewType: string, uri: URI, index: number, language: string, type: CellKind): Promise { - let cell = await this._proxy.$createEmptyCell(viewType, uri, index, language, type); + async createRawCell(cell: ICellDto | undefined, index: number): Promise { if (cell) { - let mainCell = new MainThreadCell(URI.revive(cell.uri), cell.handle, cell.source, cell.language, cell.cellKind, cell.outputs); - this._mapping.set(cell.handle, mainCell); - this.cells.splice(index, 0, mainCell); - - let dirtyStateListener = mainCell.onDidChangeDirtyState((cellState) => { - this.isDirty = this.isDirty || cellState; - }); - - this._cellListeners.set(cell.handle, dirtyStateListener); + let mainCell = new NotebookCellTextModel(URI.revive(cell.uri), cell.handle, cell.source, cell.language, cell.cellKind, cell.outputs); + this._textModel.insertNewCell(index, mainCell); return mainCell; } @@ -153,55 +43,15 @@ export class MainThreadNotebookDocument extends Disposable implements INotebook async deleteCell(uri: URI, index: number): Promise { let deleteExtHostCell = await this._proxy.$deleteCell(this.viewType, uri, index); if (deleteExtHostCell) { - let cell = this.cells[index]; - this._cellListeners.get(cell.handle)?.dispose(); - this._cellListeners.delete(cell.handle); - this.cells.splice(index, 1); + this._textModel.removeCell(index); return true; } return false; } - async save(): Promise { - let ret = await this._proxy.$saveNotebook(this.viewType, this.uri); - - if (ret) { - this.cells.forEach((cell) => { - cell.save(); - }); - } - - return ret; - } - - spliceNotebookCells(splices: NotebookCellsSplice[]): void { - splices.reverse().forEach(splice => { - let cellDtos = splice[2]; - let newCells = cellDtos.map(cell => { - let mainCell = new MainThreadCell(URI.revive(cell.uri), cell.handle, cell.source, cell.language, cell.cellKind, cell.outputs || []); - this._mapping.set(cell.handle, mainCell); - let dirtyStateListener = mainCell.onDidChangeDirtyState((cellState) => { - this.isDirty = this.isDirty || cellState; - }); - this._cellListeners.set(cell.handle, dirtyStateListener); - return mainCell; - }); - - this.cells.splice(splice[0], splice[1], ...newCells); - }); - - this._onDidChangeCells.fire(splices); - } - - spliceNotebookCellOutputs(cellHandle: number, splices: NotebookCellOutputsSplice[]): void { - let cell = this._mapping.get(cellHandle); - cell?.spliceNotebookCellOutputs(splices); - } - dispose() { - this._onWillDispose.fire(); - this._cellListeners.forEach(val => val.dispose()); + this._textModel.dispose(); super.dispose(); } } @@ -313,18 +163,18 @@ export class MainThreadNotebookController implements IMainNotebookController { ) { } - async resolveNotebook(viewType: string, uri: URI): Promise { + async resolveNotebook(viewType: string, uri: URI): Promise { // TODO: resolve notebook should wait for all notebook document destory operations to finish. let mainthreadNotebook = this._mapping.get(URI.from(uri).toString()); if (mainthreadNotebook) { - return mainthreadNotebook; + return mainthreadNotebook.textModel; } let notebookHandle = await this._mainThreadNotebook.resolveNotebook(viewType, uri); if (notebookHandle !== undefined) { mainthreadNotebook = this._mapping.get(URI.from(uri).toString()); - return mainthreadNotebook; + return mainthreadNotebook?.textModel; } return undefined; @@ -332,14 +182,14 @@ export class MainThreadNotebookController implements IMainNotebookController { spliceNotebookCells(resource: UriComponents, splices: NotebookCellsSplice[], renderers: number[]): void { let mainthreadNotebook = this._mapping.get(URI.from(resource).toString()); - mainthreadNotebook?.updateRenderers(renderers); - mainthreadNotebook?.spliceNotebookCells(splices); + mainthreadNotebook?.textModel.updateRenderers(renderers); + mainthreadNotebook?.textModel.$spliceNotebookCells(splices); } spliceNotebookCellOutputs(resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): void { let mainthreadNotebook = this._mapping.get(URI.from(resource).toString()); - mainthreadNotebook?.updateRenderers(renderers); - mainthreadNotebook?.spliceNotebookCellOutputs(cellHandle, splices); + mainthreadNotebook?.textModel.updateRenderers(renderers); + mainthreadNotebook?.textModel.$spliceNotebookCellOutputs(cellHandle, splices); } async executeNotebook(viewType: string, uri: URI): Promise { @@ -354,22 +204,33 @@ export class MainThreadNotebookController implements IMainNotebookController { updateLanguages(resource: UriComponents, languages: string[]) { let document = this._mapping.get(URI.from(resource).toString()); - document?.updateLanguages(languages); + document?.textModel.updateLanguages(languages); } updateNotebookRenderers(resource: UriComponents, renderers: number[]): void { let document = this._mapping.get(URI.from(resource).toString()); - document?.updateRenderers(renderers); + document?.textModel.updateRenderers(renderers); } updateNotebookActiveCell(uri: URI, cellHandle: number): void { let mainthreadNotebook = this._mapping.get(URI.from(uri).toString()); - mainthreadNotebook?.updateActiveCell(cellHandle); + mainthreadNotebook?.textModel.updateActiveCell(cellHandle); + } + + async createRawCell2(uri: URI, index: number, language: string, type: CellKind): Promise { + let cell = await this._proxy.$createEmptyCell(this._viewType, uri, index, language, type); + if (cell) { + let mainCell = new NotebookCellTextModel(URI.revive(cell.uri), cell.handle, cell.source, cell.language, cell.cellKind, cell.outputs); + return mainCell; + } + + return undefined; } async createRawCell(uri: URI, index: number, language: string, type: CellKind): Promise { let mainthreadNotebook = this._mapping.get(URI.from(uri).toString()); - return mainthreadNotebook?.createRawCell(this._viewType, uri, index, language, type); + let cell = await this._proxy.$createEmptyCell(this._viewType, uri, index, language, type); + return mainthreadNotebook?.createRawCell(cell, index); } async deleteCell(uri: URI, index: number): Promise { @@ -385,12 +246,12 @@ export class MainThreadNotebookController implements IMainNotebookController { executeNotebookActiveCell(uri: URI): void { let mainthreadNotebook = this._mapping.get(URI.from(uri).toString()); - if (mainthreadNotebook && mainthreadNotebook.activeCell) { - this._proxy.$executeNotebook(this._viewType, uri, mainthreadNotebook.activeCell.handle); + if (mainthreadNotebook && mainthreadNotebook.textModel.activeCell) { + this._proxy.$executeNotebook(this._viewType, uri, mainthreadNotebook.textModel.activeCell.handle); } } - async destoryNotebookDocument(notebook: INotebook): Promise { + async destoryNotebookDocument(notebook: INotebookTextModel): Promise { let document = this._mapping.get(URI.from(notebook.uri).toString()); if (!document) { @@ -403,4 +264,8 @@ export class MainThreadNotebookController implements IMainNotebookController { this._mapping.delete(URI.from(notebook.uri).toString()); } } + + async save(uri: URI): Promise { + return this._proxy.$saveNotebook(this._viewType, uri); + } } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts index cb0bdf5f189..3f46e71b503 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts @@ -3,11 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EditorInput, EditorModel, IEditorInput, GroupIdentifier, ISaveOptions } from 'vs/workbench/common/editor'; +import { EditorInput, EditorModel, IEditorInput, GroupIdentifier, ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; import { Emitter, Event } from 'vs/base/common/event'; import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService'; -import { INotebook, ICell, NotebookCellsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ICell, NotebookCellsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { URI } from 'vs/base/common/uri'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; export class NotebookEditorModel extends EditorModel { private _dirty = false; @@ -24,16 +26,14 @@ export class NotebookEditorModel extends EditorModel { } constructor( - private _notebook: INotebook + private _notebook: NotebookTextModel ) { super(); if (_notebook && _notebook.onDidChangeCells) { - this._register(_notebook.onDidChangeDirtyState((newState) => { - if (this._dirty !== newState) { - this._dirty = newState; - this._onDidChangeDirty.fire(); - } + this._register(_notebook.onDidChangeContent(() => { + this._dirty = true; + this._onDidChangeDirty.fire(); })); this._register(_notebook.onDidChangeCells((e) => { this._onDidChangeCells.fire(e); @@ -45,7 +45,7 @@ export class NotebookEditorModel extends EditorModel { return this._dirty; } - getNotebook(): INotebook { + getNotebook(): NotebookTextModel { return this._notebook; } @@ -53,33 +53,28 @@ export class NotebookEditorModel extends EditorModel { let notebook = this.getNotebook(); if (notebook) { - notebook.cells.splice(index, 0, cell); + let mainCell = new NotebookCellTextModel(URI.revive(cell.uri), cell.handle, cell.source, cell.language, cell.cellKind, cell.outputs); + this.notebook.insertNewCell(index, mainCell); this._dirty = true; this._onDidChangeDirty.fire(); + } } - deleteCell(cell: ICell) { + deleteCell(index: number) { let notebook = this.getNotebook(); if (notebook) { - let index = notebook.cells.indexOf(cell); - notebook.cells.splice(index, 1); - this._dirty = true; - this._onDidChangeDirty.fire(); + this.notebook.removeCell(index); } } async save(): Promise { if (this._notebook) { - let ret = await this._notebook.save(); - - if (ret) { - this._dirty = false; - this._onDidChangeDirty.fire(); - // todo, flush all states - return true; - } + this._dirty = false; + this._onDidChangeDirty.fire(); + // todo, flush all states + return true; } return false; @@ -114,14 +109,21 @@ export class NotebookEditorInput extends EditorInput { async save(group: GroupIdentifier, options?: ISaveOptions): Promise { if (this.textModel) { - if (await this.textModel.save()) { - return this; - } + await this.notebookService.save(this.textModel.notebook.viewType, this.textModel.notebook.uri); + await this.textModel.save(); + return this; } return undefined; } + async revert(group: GroupIdentifier, options?: IRevertOptions): Promise { + if (this.textModel) { + // TODO@rebornix we need hashing + await this.textModel.save(); + } + } + async resolve(): Promise { if (!this.promise) { await this.notebookService.canResolve(this.viewType!); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookService.ts b/src/vs/workbench/contrib/notebook/browser/notebookService.ts index e840ac8bc53..adb57cac1f3 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookService.ts @@ -10,10 +10,12 @@ import { notebookProviderExtensionPoint, notebookRendererExtensionPoint } from ' import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; import { NotebookExtensionDescription } from 'vs/workbench/api/common/extHost.protocol'; import { Emitter, Event } from 'vs/base/common/event'; -import { INotebook, ICell, INotebookMimeTypeSelector, INotebookRendererInfo, CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookTextModel, ICell, INotebookMimeTypeSelector, INotebookRendererInfo, CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { NotebookOutputRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookOutputRenderer'; import { Iterable } from 'vs/base/common/iterator'; +import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; function MODEL_ID(resource: URI): string { return resource.toString(); @@ -22,13 +24,15 @@ function MODEL_ID(resource: URI): string { export const INotebookService = createDecorator('notebookService'); export interface IMainNotebookController { - resolveNotebook(viewType: string, uri: URI): Promise; + resolveNotebook(viewType: string, uri: URI): Promise; executeNotebook(viewType: string, uri: URI): Promise; updateNotebookActiveCell(uri: URI, cellHandle: number): void; createRawCell(uri: URI, index: number, language: string, type: CellKind): Promise; + createRawCell2(uri: URI, index: number, language: string, type: CellKind): Promise; deleteCell(uri: URI, index: number): Promise executeNotebookActiveCell(uri: URI): void; - destoryNotebookDocument(notebook: INotebook): Promise; + destoryNotebookDocument(notebook: INotebookTextModel): Promise; + save(uri: URI): Promise; } export interface INotebookService { @@ -40,7 +44,7 @@ export interface INotebookService { registerNotebookRenderer(handle: number, extensionData: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, preloads: URI[]): void; unregisterNotebookRenderer(handle: number): void; getRendererInfo(handle: number): INotebookRendererInfo | undefined; - resolveNotebook(viewType: string, uri: URI): Promise; + resolveNotebook(viewType: string, uri: URI): Promise; executeNotebook(viewType: string, uri: URI): Promise; executeNotebookActiveCell(viewType: string, uri: URI): Promise; getContributedNotebookProviders(resource: URI): readonly NotebookProviderInfo[]; @@ -48,8 +52,9 @@ export interface INotebookService { updateNotebookActiveCell(viewType: string, resource: URI, cellHandle: number): void; createNotebookCell(viewType: string, resource: URI, index: number, language: string, type: CellKind): Promise; deleteNotebookCell(viewType: string, resource: URI, index: number): Promise; - destoryNotebookDocument(viewType: string, notebook: INotebook): void; + destoryNotebookDocument(viewType: string, notebook: INotebookTextModel): void; updateActiveNotebookDocument(viewType: string, resource: URI): void; + save(viewType: string, resource: URI): Promise; } export class NotebookProviderInfoStore { @@ -105,8 +110,8 @@ class ModelData implements IDisposable { private readonly _modelEventListeners = new DisposableStore(); constructor( - public model: INotebook, - onWillDispose: (model: INotebook) => void + public model: NotebookTextModel, + onWillDispose: (model: INotebookTextModel) => void ) { this._modelEventListeners.add(model.onWillDispose(() => onWillDispose(model))); } @@ -215,7 +220,7 @@ export class NotebookService extends Disposable implements INotebookService { return; } - async resolveNotebook(viewType: string, uri: URI): Promise { + async resolveNotebook(viewType: string, uri: URI): Promise { const provider = this._notebookProviders.get(viewType); if (!provider) { return undefined; @@ -254,6 +259,16 @@ export class NotebookService extends Disposable implements INotebookService { return; } + async createNotebookCell2(viewType: string, resource: URI, index: number, language: string, type: CellKind): Promise { + let provider = this._notebookProviders.get(viewType); + + if (provider) { + return provider.controller.createRawCell2(resource, index, language, type); + } + + return; + } + async deleteNotebookCell(viewType: string, resource: URI, index: number): Promise { let provider = this._notebookProviders.get(viewType); @@ -299,7 +314,7 @@ export class NotebookService extends Disposable implements INotebookService { return ret; } - destoryNotebookDocument(viewType: string, notebook: INotebook): void { + destoryNotebookDocument(viewType: string, notebook: INotebookTextModel): void { let provider = this._notebookProviders.get(viewType); if (provider) { @@ -311,7 +326,17 @@ export class NotebookService extends Disposable implements INotebookService { this._onDidChangeActiveEditor.fire({ viewType, uri: resource }); } - private _onWillDispose(model: INotebook): void { + async save(viewType: string, resource: URI): Promise { + let provider = this._notebookProviders.get(viewType); + + if (provider) { + return provider.controller.save(resource); + } + + return false; + } + + private _onWillDispose(model: INotebookTextModel): void { let modelId = MODEL_ID(model.uri); let modelData = this._models[modelId]; diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel.ts index 26468a571f2..8007a2aade9 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel.ts @@ -243,8 +243,9 @@ export class CellViewModel extends Disposable implements ICellViewModel { this.cell.source = strs; this._html = null; } + save() { - if (this._textModel && !this._textModel.isDisposed() && (this.cell.isDirty || this.state === CellState.Editing)) { + if (this._textModel && !this._textModel.isDisposed() && this.state === CellState.Editing) { let cnt = this._textModel.getLineCount(); this.cell.source = this._textModel.getLinesContent().map((str, index) => str + (index !== cnt - 1 ? '\n' : '')); } @@ -276,7 +277,7 @@ export class CellViewModel extends Disposable implements ICellViewModel { this._buffer = this._textModel.getTextBuffer(); this._register(ref); this._register(this._textModel.onDidChangeContent(() => { - this.cell.isDirty = true; + this.cell.contentChange(); this._html = null; this._onDidChangeContent.fire(); })); diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts index 88e294c0181..209a7116bd8 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts @@ -143,7 +143,7 @@ export class NotebookViewModel extends Disposable { private _deleteCellDelegate(deleteIndex: number, cell: ICell) { this._viewCells.splice(deleteIndex, 1); - this._model.deleteCell(cell); + this._model.deleteCell(deleteIndex); this._onDidChangeViewCells.fire({ synchronous: true, splices: [[deleteIndex, 1, []]] }); } @@ -164,7 +164,7 @@ export class NotebookViewModel extends Disposable { deleteCell(index: number, synchronous: boolean) { let viewCell = this._viewCells[index]; this._viewCells.splice(index, 1); - this._model.deleteCell(viewCell.cell); + this._model.deleteCell(index); this.undoService.pushElement(new DeleteCellEdit(this.uri, index, viewCell, { insertCell: this._insertCellDelegate.bind(this), @@ -182,7 +182,7 @@ export class NotebookViewModel extends Disposable { } this.viewCells.splice(index, 1); - this._model.deleteCell(viewCell.cell); + this._model.deleteCell(index); this.viewCells!.splice(newIdx, 0, viewCell); this._model.insertCell(viewCell.cell, newIdx); diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts new file mode 100644 index 00000000000..e11f76aadb7 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { ICell, IOutput, NotebookCellOutputsSplice, CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { PieceTreeTextBufferFactory, PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; +import { URI } from 'vs/base/common/uri'; + +export class NotebookCellTextModel implements ICell { + private _onDidChangeOutputs = new Emitter(); + onDidChangeOutputs: Event = this._onDidChangeOutputs.event; + + private _onDidChangeContent = new Emitter(); + onDidChangeContent: Event = this._onDidChangeContent.event; + + private _outputs: IOutput[]; + + get outputs(): IOutput[] { + return this._outputs; + } + + get source() { + return this._source; + } + + set source(newValue: string[]) { + this._source = newValue; + this._buffer = null; + } + + private _buffer: PieceTreeTextBufferFactory | null = null; + + constructor( + readonly uri: URI, + public handle: number, + private _source: string[], + public language: string, + public cellKind: CellKind, + outputs: IOutput[] + ) { + this._outputs = outputs; + } + + contentChange() { + this._onDidChangeContent.fire(); + + } + + spliceNotebookCellOutputs(splices: NotebookCellOutputsSplice[]): void { + splices.reverse().forEach(splice => { + this.outputs.splice(splice[0], splice[1], ...splice[2]); + }); + + this._onDidChangeOutputs.fire(splices); + } + + resolveTextBufferFactory(): PieceTreeTextBufferFactory { + if (this._buffer) { + return this._buffer; + } + + let builder = new PieceTreeTextBufferBuilder(); + builder.acceptChunk(this.source.join('\n')); + this._buffer = builder.finish(true); + return this._buffer; + } +} diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts new file mode 100644 index 00000000000..c976c610eeb --- /dev/null +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -0,0 +1,101 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, 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, NotebookCellsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; + +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 _onDidChangeContent = new Emitter(); + onDidChangeContent: Event = this._onDidChangeContent.event; + private _mapping: Map = new Map(); + private _cellListeners: Map = new Map(); + cells: NotebookCellTextModel[]; + activeCell: NotebookCellTextModel | undefined; + languages: string[] = []; + renderers = new Set(); + + constructor( + public handle: number, + public viewType: string, + public uri: URI + ) { + super(); + this.cells = []; + } + + updateLanguages(languages: string[]) { + this.languages = languages; + } + + updateRenderers(renderers: number[]) { + renderers.forEach(render => { + this.renderers.add(render); + }); + } + + updateActiveCell(handle: number) { + this.activeCell = this._mapping.get(handle); + } + + insertNewCell(index: number, cell: NotebookCellTextModel): void { + this._mapping.set(cell.handle, cell); + this.cells.splice(index, 0, cell); + let dirtyStateListener = cell.onDidChangeContent(() => { + this._onDidChangeContent.fire(); + }); + + this._cellListeners.set(cell.handle, dirtyStateListener); + this._onDidChangeContent.fire(); + return; + } + + removeCell(index: number) { + let cell = this.cells[index]; + this._cellListeners.get(cell.handle)?.dispose(); + this._cellListeners.delete(cell.handle); + this.cells.splice(index, 1); + this._onDidChangeContent.fire(); + } + + + // TODO@rebornix should this trigger content change event? + $spliceNotebookCells(splices: NotebookCellsSplice[]): void { + splices.reverse().forEach(splice => { + let cellDtos = splice[2]; + let newCells = cellDtos.map(cell => { + let mainCell = new NotebookCellTextModel(URI.revive(cell.uri), cell.handle, cell.source, cell.language, cell.cellKind, cell.outputs || []); + this._mapping.set(cell.handle, mainCell); + let dirtyStateListener = mainCell.onDidChangeContent(() => { + this._onDidChangeContent.fire(); + }); + this._cellListeners.set(cell.handle, dirtyStateListener); + return mainCell; + }); + + this.cells.splice(splice[0], splice[1], ...newCells); + }); + + this._onDidChangeCells.fire(splices); + } + + // TODO@rebornix should this trigger content change event? + $spliceNotebookCellOutputs(cellHandle: number, splices: NotebookCellOutputsSplice[]): void { + let cell = this._mapping.get(cellHandle); + cell?.spliceNotebookCellOutputs(splices); + } + + dispose() { + this._onWillDispose.fire(); + this._cellListeners.forEach(val => val.dispose()); + super.dispose(); + } +} diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index d9a0365a4cf..73afe15d585 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -123,8 +123,9 @@ export interface ICell { cellKind: CellKind; outputs: IOutput[]; onDidChangeOutputs?: Event; - isDirty: boolean; resolveTextBufferFactory(): PieceTreeTextBufferFactory; + // TODO@rebornix it should be later on replaced by moving textmodel resolution into CellTextModel + contentChange(): void; } export interface LanguageInfo { @@ -135,7 +136,7 @@ export interface IMetadata { language_info: LanguageInfo; } -export interface INotebook { +export interface INotebookTextModel { handle: number; viewType: string; // metadata: IMetadata; @@ -144,9 +145,8 @@ export interface INotebook { cells: ICell[]; renderers: Set; onDidChangeCells?: Event; - onDidChangeDirtyState: Event; + onDidChangeContent: Event; onWillDispose(listener: () => void): IDisposable; - save(): Promise; } export interface IRenderOutput { diff --git a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts index 774dc89d1b2..9cc8e077e65 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts @@ -9,9 +9,10 @@ import { TestInstantiationService } from 'vs/platform/instantiation/test/common/ import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { TestNotebook, withTestNotebook, TestCell } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; +import { withTestNotebook, TestCell } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; suite('NotebookViewModel', () => { const instantiationService = new TestInstantiationService(); @@ -20,7 +21,7 @@ suite('NotebookViewModel', () => { instantiationService.spy(IUndoRedoService, 'pushElement'); test('ctor', function () { - const notebook = new TestNotebook(0, 'notebook', URI.parse('test')); + const notebook = new NotebookTextModel(0, 'notebook', URI.parse('test')); const model = new NotebookEditorModel(notebook); const viewModel = new NotebookViewModel('notebook', model, instantiationService, blukEditService, undoRedoService); assert.equal(viewModel.viewType, 'notebook'); diff --git a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts index e5847b2a3c9..889e61c1f8d 100644 --- a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts @@ -4,10 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { PieceTreeTextBufferFactory } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; -import { CellKind, ICell, INotebook, IOutput, NotebookCellOutputsSplice, NotebookCellsSplice, CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, ICell, IOutput, NotebookCellOutputsSplice, CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookViewModel, IModelDecorationsChangeAccessor } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel'; @@ -19,6 +18,8 @@ import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; import { Range } from 'vs/editor/common/core/range'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; export class TestCell implements ICell { uri: URI; @@ -50,40 +51,15 @@ export class TestCell implements ICell { this._outputs = outputs; this.uri = CellUri.generate(URI.parse('test:///fake/notebook'), handle); } + contentChange(): void { + // throw new Error('Method not implemented.'); + } resolveTextBufferFactory(): PieceTreeTextBufferFactory { throw new Error('Method not implemented.'); } } -export class TestNotebook extends Disposable implements INotebook { - private readonly _onDidChangeCells = new Emitter(); - get onDidChangeCells(): Event { return this._onDidChangeCells.event; } - private _onDidChangeDirtyState = new Emitter(); - onDidChangeDirtyState: Event = this._onDidChangeDirtyState.event; - private readonly _onWillDispose: Emitter = this._register(new Emitter()); - readonly onWillDispose: Event = this._onWillDispose.event; - cells: TestCell[]; - activeCell: TestCell | undefined; - languages: string[] = []; - renderers = new Set(); - - - constructor( - public handle: number, - public viewType: string, - public uri: URI - ) { - super(); - - this.cells = []; - } - - save(): Promise { - throw new Error('Method not implemented.'); - } -} - export class TestNotebookEditor implements INotebookEditor { get viewModel() { @@ -205,9 +181,9 @@ export function createTestCellViewModel(instantiationService: IInstantiationServ export function withTestNotebook(instantiationService: IInstantiationService, blukEditService: IBulkEditService, undoRedoService: IUndoRedoService, cells: [string[], string, CellKind, IOutput[]][], callback: (editor: TestNotebookEditor, viewModel: NotebookViewModel) => void) { const viewType = 'notebook'; const editor = new TestNotebookEditor(); - const notebook = new TestNotebook(0, viewType, URI.parse('test')); + const notebook = new NotebookTextModel(0, viewType, URI.parse('test')); notebook.cells = cells.map((cell, index) => { - return new TestCell(viewType, index, cell[0], cell[1], cell[2], cell[3]); + return new NotebookCellTextModel(notebook.uri, index, cell[0], cell[1], cell[2], cell[3]); }); const model = new NotebookEditorModel(notebook); const viewModel = new NotebookViewModel(viewType, model, instantiationService, blukEditService, undoRedoService);