diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index 734d1da9521..ce17aa2916e 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -3,14 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import * as DOM from 'vs/base/browser/dom'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; -import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IExtHostContext, ExtHostNotebookShape, ExtHostContext, INotebookDocumentsAndEditorsDelta, INotebookModelAddedData } from '../common/extHost.protocol'; -import { Disposable, IDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; +import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IExtHostContext, ExtHostNotebookShape, ExtHostContext, INotebookDocumentsAndEditorsDelta } from '../common/extHost.protocol'; +import { Disposable, IDisposable, combinedDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; import { INotebookService, IMainNotebookController } from 'vs/workbench/contrib/notebook/common/notebookService'; -import { INotebookTextModel, INotebookMimeTypeSelector, NOTEBOOK_DISPLAY_ORDER, NotebookCellOutputsSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, CellEditType, CellKind, INotebookKernelInfo, INotebookKernelInfoDto, INotebookTextModelBackup, IEditor, INotebookRendererInfo, IOutputRenderRequest, IOutputRenderResponse, INotebookDocumentFilter } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookMimeTypeSelector, NOTEBOOK_DISPLAY_ORDER, NotebookCellOutputsSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, CellEditType, CellKind, INotebookKernelInfo, INotebookKernelInfoDto, IEditor, INotebookRendererInfo, IOutputRenderRequest, IOutputRenderResponse, INotebookDocumentFilter } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -19,8 +18,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IRelativePattern } from 'vs/base/common/glob'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IUndoRedoService, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo'; +import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { Emitter } from 'vs/base/common/event'; @@ -55,42 +53,8 @@ export class MainThreadNotebookDocument extends Disposable { })); } - async applyEdit(modelVersionId: number, edits: ICellEditOperation[], emitToExtHost: boolean, synchronous: boolean): Promise { - await this.notebookService.transformEditsOutputs(this.textModel, edits); - if (synchronous) { - return this._textModel.$applyEdit(modelVersionId, edits, emitToExtHost, synchronous); - } else { - return new Promise(resolve => { - this._register(DOM.scheduleAtNextAnimationFrame(() => { - const ret = this._textModel.$applyEdit(modelVersionId, edits, emitToExtHost, true); - resolve(ret); - })); - }); - } - } - - async spliceNotebookCellOutputs(cellHandle: number, splices: NotebookCellOutputsSplice[]) { - await this.notebookService.transformSpliceOutputs(this.textModel, splices); - this._textModel.$spliceNotebookCellOutputs(cellHandle, splices); - } - - handleEdit(editId: number, label: string | undefined): void { - this.undoRedoService.pushElement({ - type: UndoRedoElementType.Resource, - resource: this._textModel.uri, - label: label ?? nls.localize('defaultEditLabel', "Edit"), - undo: async () => { - await this._proxy.$undoNotebook(this._textModel.viewType, this._textModel.uri, editId, this._textModel.isDirty); - }, - redo: async () => { - await this._proxy.$redoNotebook(this._textModel.viewType, this._textModel.uri, editId, this._textModel.isDirty); - }, - }); - this._textModel.setDirty(true); - } - dispose() { - this._textModel.dispose(); + // this._textModel.dispose(); super.dispose(); } } @@ -203,21 +167,21 @@ class DocumentAndEditorState { @extHostNamedCustomer(MainContext.MainThreadNotebook) export class MainThreadNotebooks extends Disposable implements MainThreadNotebookShape { - private readonly _notebookProviders = new Map(); + private readonly _notebookProviders = new Map(); private readonly _notebookKernels = new Map(); private readonly _notebookKernelProviders = new Map, provider: IDisposable }>(); private readonly _notebookRenderers = new Map(); private readonly _proxy: ExtHostNotebookShape; private _toDisposeOnEditorRemove = new Map(); private _currentState?: DocumentAndEditorState; + private _editorEventListenersMapping: Map = new Map(); constructor( extHostContext: IExtHostContext, @INotebookService private _notebookService: INotebookService, @IConfigurationService private readonly configurationService: IConfigurationService, @IEditorService private readonly editorService: IEditorService, - @IAccessibilityService private readonly accessibilityService: IAccessibilityService, - @IInstantiationService private readonly _instantiationService: IInstantiationService + @IAccessibilityService private readonly accessibilityService: IAccessibilityService ) { super(); @@ -226,15 +190,23 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo } async $tryApplyEdits(viewType: string, resource: UriComponents, modelVersionId: number, edits: ICellEditOperation[], renderers: number[]): Promise { - let controller = this._notebookProviders.get(viewType); - - if (controller) { - return controller.tryApplyEdits(resource, modelVersionId, edits, renderers); + const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); + if (textModel) { + await this._notebookService.transformEditsOutputs(textModel, edits); + return textModel.$applyEdit(modelVersionId, edits, true); } return false; } + async removeNotebookTextModel(uri: URI): Promise { + // TODO@rebornix, remove cell should use emitDelta as well to ensure document/editor events are sent together + await this._proxy.$acceptDocumentAndEditorsDelta({ removedDocuments: [uri] }); + let textModelDisposableStore = this._editorEventListenersMapping.get(uri.toString()); + textModelDisposableStore?.dispose(); + this._editorEventListenersMapping.delete(URI.from(uri).toString()); + } + private _isDeltaEmpty(delta: INotebookDocumentsAndEditorsDelta) { if (delta.addedDocuments !== undefined && delta.addedDocuments.length > 0) { return false; @@ -300,11 +272,32 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo this._removeNotebookEditor(editors); })); - this._register(this._notebookService.onNotebookDocumentAdd(() => { + this._register(this._notebookService.onNotebookDocumentAdd((documents) => { + documents.forEach(doc => { + if (!this._editorEventListenersMapping.has(doc.toString())) { + const disposableStore = new DisposableStore(); + const textModel = this._notebookService.getNotebookTextModel(doc); + disposableStore.add(textModel!.onDidModelChangeProxy(e => { + this._proxy.$acceptModelChanged(textModel!.uri, e); + this._proxy.$acceptEditorPropertiesChanged(doc, { selections: { selections: textModel!.selections }, metadata: null }); + })); + disposableStore.add(textModel!.onDidSelectionChange(e => { + const selectionsChange = e ? { selections: e } : null; + this._proxy.$acceptEditorPropertiesChanged(doc, { selections: selectionsChange, metadata: null }); + })); + + this._editorEventListenersMapping.set(textModel!.uri.toString(), disposableStore); + } + }); this._updateState(); })); - this._register(this._notebookService.onNotebookDocumentRemove(() => { + this._register(this._notebookService.onNotebookDocumentRemove((documents) => { + documents.forEach(doc => { + this._editorEventListenersMapping.get(doc.toString())?.dispose(); + this._editorEventListenersMapping.delete(doc.toString()); + }); + this._updateState(); })); @@ -337,10 +330,6 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo this._updateState(notebookEditor); } - async addNotebookDocument(data: INotebookModelAddedData) { - this._updateState(); - } - private _addNotebookEditor(e: IEditor) { this._toDisposeOnEditorRemove.set(e.getId(), combinedDisposable( e.onDidChangeModel(() => this._updateState()), @@ -429,18 +418,86 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo this._notebookService.unregisterNotebookRenderer(id); } - async $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string, supportBackup: boolean, kernel: INotebookKernelInfoDto | undefined): Promise { - let controller = new MainThreadNotebookController(this._proxy, this, viewType, supportBackup, kernel, this._notebookService, this._instantiationService); - this._notebookProviders.set(viewType, controller); - this._notebookService.registerNotebookController(viewType, extension, controller); + async $registerNotebookProvider(_extension: NotebookExtensionDescription, _viewType: string, _supportBackup: boolean, _kernel: INotebookKernelInfoDto | undefined): Promise { + const controller: IMainNotebookController = { + kernel: _kernel, + supportBackup: _supportBackup, + reloadNotebook: async (mainthreadTextModel: NotebookTextModel) => { + const data = await this._proxy.$resolveNotebookData(_viewType, mainthreadTextModel.uri); + if (!data) { + return; + } + + mainthreadTextModel.languages = data.languages; + mainthreadTextModel.metadata = data.metadata; + + const edits: ICellEditOperation[] = [ + { editType: CellEditType.Delete, count: mainthreadTextModel.cells.length, index: 0 }, + { editType: CellEditType.Insert, index: 0, cells: data.cells } + ]; + + await this._notebookService.transformEditsOutputs(mainthreadTextModel, edits); + await new Promise(resolve => { + DOM.scheduleAtNextAnimationFrame(() => { + const ret = mainthreadTextModel!.$applyEdit(mainthreadTextModel!.versionId, edits, true); + resolve(ret); + }); + }); + }, + createNotebook: async (textModel: NotebookTextModel, backupId?: string) => { + // open notebook document + const data = await this._proxy.$resolveNotebookData(textModel.viewType, textModel.uri, backupId); + if (!data) { + return; + } + + textModel.languages = data.languages; + textModel.metadata = data.metadata; + + if (data.cells.length) { + textModel.initialize(data!.cells); + } else { + const mainCell = textModel.createCellTextModel([''], textModel.languages.length ? textModel.languages[0] : '', CellKind.Code, [], undefined); + textModel.insertTemplateCell(mainCell); + } + + this._proxy.$acceptEditorPropertiesChanged(textModel.uri, { selections: null, metadata: textModel.metadata }); + return; + }, + resolveNotebookEditor: async (viewType: string, uri: URI, editorId: string) => { + await this._proxy.$resolveNotebookEditor(viewType, uri, editorId); + }, + executeNotebookByAttachedKernel: async (viewType: string, uri: URI, token: CancellationToken) => { + return this.executeNotebookByAttachedKernel(viewType, uri, token); + }, + onDidReceiveMessage: (editorId: string, rendererType: string | undefined, message: unknown) => { + this._proxy.$onDidReceiveMessage(editorId, rendererType, message); + }, + removeNotebookDocument: async (uri: URI) => { + return this.removeNotebookTextModel(uri); + }, + executeNotebookCell: async (uri: URI, handle: number, token: CancellationToken) => { + return this._proxy.$executeNotebookByAttachedKernel(_viewType, uri, handle, token); + }, + save: async (uri: URI, token: CancellationToken) => { + return this._proxy.$saveNotebook(_viewType, uri, token); + }, + saveAs: async (uri: URI, target: URI, token: CancellationToken) => { + return this._proxy.$saveNotebookAs(_viewType, uri, target, token); + }, + backup: async (uri: URI, token: CancellationToken) => { + return this._proxy.$backup(_viewType, uri, token); + } + }; + + this._notebookProviders.set(_viewType, controller); + this._notebookService.registerNotebookController(_viewType, _extension, controller); return; } async $onNotebookChange(viewType: string, uri: UriComponents): Promise { - let controller = this._notebookProviders.get(viewType); - if (controller) { - controller.handleNotebookChange(uri); - } + const textModel = this._notebookService.getNotebookTextModel(URI.from(uri)); + textModel?.handleUnknownChange(); } async $unregisterNotebookProvider(viewType: string): Promise { @@ -510,32 +567,27 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo } async $updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise { - let controller = this._notebookProviders.get(viewType); - - if (controller) { - controller.updateLanguages(resource, languages); - } + const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); + textModel?.updateLanguages(languages); } async $updateNotebookMetadata(viewType: string, resource: UriComponents, metadata: NotebookDocumentMetadata): Promise { - let controller = this._notebookProviders.get(viewType); - - if (controller) { - controller.updateNotebookMetadata(resource, metadata); - } + const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); + textModel?.updateNotebookMetadata(metadata); } async $updateNotebookCellMetadata(viewType: string, resource: UriComponents, handle: number, metadata: NotebookCellMetadata): Promise { - let controller = this._notebookProviders.get(viewType); - - if (controller) { - controller.updateNotebookCellMetadata(resource, handle, metadata); - } + const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); + textModel?.updateNotebookCellMetadata(handle, metadata); } async $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): Promise { - let controller = this._notebookProviders.get(viewType); - await controller?.spliceNotebookCellOutputs(resource, cellHandle, splices, renderers); + const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); + + if (textModel) { + await this._notebookService.transformSpliceOutputs(textModel, splices); + textModel.$spliceNotebookCellOutputs(cellHandle, splices); + } } async executeNotebookByAttachedKernel(viewType: string, uri: URI, token: CancellationToken): Promise { @@ -553,220 +605,20 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo } $onDidEdit(resource: UriComponents, viewType: string, editId: number, label: string | undefined): void { - let controller = this._notebookProviders.get(viewType); - controller?.handleEdit(resource, editId, label); + const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); + + if (textModel) { + textModel.$handleEdit(label, () => { + return this._proxy.$undoNotebook(textModel.viewType, textModel.uri, editId, textModel.isDirty); + }, () => { + return this._proxy.$redoNotebook(textModel.viewType, textModel.uri, editId, textModel.isDirty); + }); + } } $onContentChange(resource: UriComponents, viewType: string): void { - let controller = this._notebookProviders.get(viewType); - controller?.handleNotebookChange(resource); - } -} - -export class MainThreadNotebookController implements IMainNotebookController { - private _mapping: Map = new Map(); - static documentHandle: number = 0; - - constructor( - private readonly _proxy: ExtHostNotebookShape, - private _mainThreadNotebook: MainThreadNotebooks, - private _viewType: string, - private _supportBackup: boolean, - readonly kernel: INotebookKernelInfoDto | undefined, - readonly notebookService: INotebookService, - readonly _instantiationService: IInstantiationService - - ) { - } - - async createNotebook(viewType: string, uri: URI, backup: INotebookTextModelBackup | undefined, forceReload: boolean, editorId?: string, backupId?: string): Promise { - let mainthreadNotebook = this._mapping.get(URI.from(uri).toString()); - - if (mainthreadNotebook) { - if (forceReload) { - const data = await this._proxy.$resolveNotebookData(viewType, uri); - if (!data) { - return; - } - - mainthreadNotebook.textModel.languages = data.languages; - mainthreadNotebook.textModel.metadata = data.metadata; - await mainthreadNotebook.applyEdit(mainthreadNotebook.textModel.versionId, [ - { editType: CellEditType.Delete, count: mainthreadNotebook.textModel.cells.length, index: 0 }, - { editType: CellEditType.Insert, index: 0, cells: data.cells } - ], true, false); - } - return mainthreadNotebook.textModel; - } - - let document = this._instantiationService.createInstance(MainThreadNotebookDocument, this._proxy, MainThreadNotebookController.documentHandle++, viewType, this._supportBackup, uri); - this._mapping.set(document.uri.toString(), document); - - if (backup) { - // trigger events - document.textModel.metadata = backup.metadata; - document.textModel.languages = backup.languages; - - // restored from backup, update the text model without emitting any event to exthost - await document.applyEdit(document.textModel.versionId, [ - { - editType: CellEditType.Insert, - index: 0, - cells: backup.cells || [] - } - ], false, true); - - // create document in ext host with cells data - await this._mainThreadNotebook.addNotebookDocument({ - viewType: document.viewType, - handle: document.handle, - uri: document.uri, - metadata: document.textModel.metadata, - versionId: document.textModel.versionId, - cells: document.textModel.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 - })), - attachedEditor: editorId ? { - id: editorId, - selections: document.textModel.selections - } : undefined - }); - - return document.textModel; - } - - // open notebook document - const data = await this._proxy.$resolveNotebookData(viewType, uri, backupId); - if (!data) { - return; - } - - document.textModel.languages = data.languages; - document.textModel.metadata = data.metadata; - - if (data.cells.length) { - document.textModel.initialize(data!.cells); - } else { - const mainCell = document.textModel.createCellTextModel([''], document.textModel.languages.length ? document.textModel.languages[0] : '', CellKind.Code, [], undefined); - document.textModel.insertTemplateCell(mainCell); - } - - await this._mainThreadNotebook.addNotebookDocument({ - viewType: document.viewType, - handle: document.handle, - uri: document.uri, - metadata: document.textModel.metadata, - versionId: document.textModel.versionId, - cells: document.textModel.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 - })), - attachedEditor: editorId ? { - id: editorId, - selections: document.textModel.selections - } : undefined - }); - - this._proxy.$acceptEditorPropertiesChanged(uri, { selections: null, metadata: document.textModel.metadata }); - - return document.textModel; - } - - async resolveNotebookEditor(viewType: string, uri: URI, editorId: string) { - await this._proxy.$resolveNotebookEditor(viewType, uri, editorId); - } - - async tryApplyEdits(resource: UriComponents, modelVersionId: number, edits: ICellEditOperation[], renderers: number[]): Promise { - let mainthreadNotebook = this._mapping.get(URI.from(resource).toString()); - - if (mainthreadNotebook) { - return await mainthreadNotebook.applyEdit(modelVersionId, edits, true, true); - } - - return false; - } - - async spliceNotebookCellOutputs(resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): Promise { - let mainthreadNotebook = this._mapping.get(URI.from(resource).toString()); - await mainthreadNotebook?.spliceNotebookCellOutputs(cellHandle, splices); - } - - async executeNotebookByAttachedKernel(viewType: string, uri: URI, token: CancellationToken): Promise { - return this._mainThreadNotebook.executeNotebookByAttachedKernel(viewType, uri, token); - } - - onDidReceiveMessage(editorId: string, rendererType: string | undefined, message: unknown): void { - this._proxy.$onDidReceiveMessage(editorId, rendererType, message); - } - - async removeNotebookDocument(notebook: INotebookTextModel): Promise { - let document = this._mapping.get(URI.from(notebook.uri).toString()); - - if (!document) { - return; - } - - // TODO@rebornix, remove cell should use emitDelta as well to ensure document/editor events are sent together - await this._proxy.$acceptDocumentAndEditorsDelta({ removedDocuments: [notebook.uri] }); - document.dispose(); - this._mapping.delete(URI.from(notebook.uri).toString()); - } - - // Methods for ExtHost - - handleNotebookChange(resource: UriComponents) { - let document = this._mapping.get(URI.from(resource).toString()); - document?.textModel.handleUnknownChange(); - } - - handleEdit(resource: UriComponents, editId: number, label: string | undefined): void { - let document = this._mapping.get(URI.from(resource).toString()); - document?.handleEdit(editId, label); - } - - updateLanguages(resource: UriComponents, languages: string[]) { - let document = this._mapping.get(URI.from(resource).toString()); - document?.textModel.updateLanguages(languages); - } - - updateNotebookMetadata(resource: UriComponents, metadata: NotebookDocumentMetadata) { - let document = this._mapping.get(URI.from(resource).toString()); - document?.textModel.updateNotebookMetadata(metadata); - } - - updateNotebookCellMetadata(resource: UriComponents, handle: number, metadata: NotebookCellMetadata) { - let document = this._mapping.get(URI.from(resource).toString()); - document?.textModel.updateNotebookCellMetadata(handle, metadata); - } - - async executeNotebookCell(uri: URI, handle: number, token: CancellationToken): Promise { - return this._proxy.$executeNotebookByAttachedKernel(this._viewType, uri, handle, token); - } - - async save(uri: URI, token: CancellationToken): Promise { - return this._proxy.$saveNotebook(this._viewType, uri, token); - } - - async saveAs(uri: URI, target: URI, token: CancellationToken): Promise { - return this._proxy.$saveNotebookAs(this._viewType, uri, target, token); - } - - async backup(uri: URI, token: CancellationToken): Promise { - const backupId = await this._proxy.$backup(this._viewType, uri, token); - return backupId; + const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); + textModel?.handleUnknownChange(); } } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts index 41d059f556a..99284eabed1 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -10,7 +10,7 @@ import { notebookProviderExtensionPoint, notebookRendererExtensionPoint, INotebo import { NotebookProviderInfo, NotebookEditorDescriptor } 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 { INotebookTextModel, INotebookRendererInfo, NotebookDocumentMetadata, ICellDto2, INotebookKernelInfo, CellOutputKind, ITransformedDisplayOutputDto, IDisplayOutput, ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, NOTEBOOK_DISPLAY_ORDER, sortMimeTypes, IOrderedMimeType, mimeTypeSupportedByCore, IOutputRenderRequestOutputInfo, IOutputRenderRequestCellInfo, NotebookCellOutputsSplice, ICellEditOperation, CellEditType, ICellInsertEdit, IOutputRenderResponse, IProcessedOutput, BUILTIN_RENDERER_ID, NotebookEditorPriority, INotebookKernelProvider, notebookDocumentFilterMatch, INotebookKernelInfo2 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookTextModel, INotebookRendererInfo, INotebookKernelInfo, CellOutputKind, ITransformedDisplayOutputDto, IDisplayOutput, ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, NOTEBOOK_DISPLAY_ORDER, sortMimeTypes, IOrderedMimeType, mimeTypeSupportedByCore, IOutputRenderRequestOutputInfo, IOutputRenderRequestCellInfo, NotebookCellOutputsSplice, ICellEditOperation, CellEditType, ICellInsertEdit, IOutputRenderResponse, IProcessedOutput, BUILTIN_RENDERER_ID, NotebookEditorPriority, INotebookKernelProvider, notebookDocumentFilterMatch, INotebookKernelInfo2 } 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'; @@ -29,6 +29,7 @@ import { StorageScope, IStorageService } from 'vs/platform/storage/common/storag import { IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { generateUuid } from 'vs/base/common/uuid'; import { flatten } from 'vs/base/common/arrays'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; function MODEL_ID(resource: URI): string { return resource.toString(); @@ -166,6 +167,7 @@ class ModelData implements IDisposable { } export class NotebookService extends Disposable implements INotebookService, ICustomEditorViewTypesHandler { declare readonly _serviceBrand: undefined; + static mainthreadNotebookDocumentHandle: number = 0; private readonly _notebookProviders = new Map(); private readonly _notebookRenderers = new Map(); private readonly _notebookKernels = new Map(); @@ -205,7 +207,8 @@ export class NotebookService extends Disposable implements INotebookService, ICu @IEditorService private readonly _editorService: IEditorService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, - @IStorageService private readonly _storageService: IStorageService + @IStorageService private readonly _storageService: IStorageService, + @IInstantiationService private readonly _instantiationService: IInstantiationService ) { super(); @@ -393,43 +396,33 @@ export class NotebookService extends Disposable implements INotebookService, ICu return renderer; } - async createNotebookFromBackup(viewType: string, uri: URI, metadata: NotebookDocumentMetadata, languages: string[], cells: ICellDto2[], editorId?: string): Promise { - const provider = this._notebookProviders.get(viewType); - if (!provider) { - return undefined; - } - - const notebookModel = await provider.controller.createNotebook(viewType, uri, { metadata, languages, cells }, false, editorId); - if (!notebookModel) { - return undefined; - } - - // new notebook model created - const modelId = MODEL_ID(uri); - const modelData = new ModelData( - notebookModel, - (model) => this._onWillDisposeDocument(model), - ); - this._models.set(modelId, modelData); - this._onNotebookDocumentAdd.fire([notebookModel.uri]); - // after the document is added to the store and sent to ext host, we transform the ouputs - await this.transformTextModelOutputs(notebookModel!); - return modelData.model; - } - async resolveNotebook(viewType: string, uri: URI, forceReload: boolean, editorId?: string, backupId?: string): Promise { const provider = this._notebookProviders.get(viewType); if (!provider) { return undefined; } - const notebookModel = await provider.controller.createNotebook(viewType, uri, undefined, forceReload, editorId, backupId); - if (!notebookModel) { - return undefined; + const modelId = MODEL_ID(uri); + + let notebookModel: NotebookTextModel | undefined = undefined; + if (this._models.has(modelId)) { + // the model already exists + notebookModel = this._models.get(modelId)!.model; + if (forceReload) { + await provider.controller.reloadNotebook(notebookModel); + } + + return notebookModel; + } else { + notebookModel = this._instantiationService.createInstance(NotebookTextModel, NotebookService.mainthreadNotebookDocumentHandle++, viewType, provider.controller.supportBackup, uri); + await provider.controller.createNotebook(notebookModel, backupId); + + if (!notebookModel) { + return undefined; + } } // new notebook model created - const modelId = MODEL_ID(uri); const modelData = new ModelData( notebookModel!, (model) => this._onWillDisposeDocument(model), @@ -447,6 +440,12 @@ export class NotebookService extends Disposable implements INotebookService, ICu return modelData.model; } + getNotebookTextModel(uri: URI): NotebookTextModel | undefined { + const modelId = MODEL_ID(uri); + + return this._models.get(modelId)?.model; + } + private async _fillInTransformedOutputs( renderers: Set, requestItems: IOutputRenderRequestCellInfo[], @@ -861,7 +860,8 @@ export class NotebookService extends Disposable implements INotebookService, ICu let provider = this._notebookProviders.get(modelData!.model.viewType); if (provider) { - provider.controller.removeNotebookDocument(modelData!.model); + provider.controller.removeNotebookDocument(modelData!.model.uri); + modelData!.model.dispose(); } diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index 925e0144e46..4bcb41cab07 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -3,13 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as nls from 'vs/nls'; 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, NotebookCellTextModelSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, CellEditType, CellUri, ICellInsertEdit, NotebookCellsChangedEvent, CellKind, IProcessedOutput, notebookDocumentMetadataDefaults, diff, ICellDeleteEdit, NotebookCellsChangeType, ICellDto2, IMainCellDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ITextSnapshot } from 'vs/editor/common/model'; -import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; +import { IUndoRedoService, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo'; import { InsertCellEdit, DeleteCellEdit, MoveCellEdit, SpliceCellsEdit } from 'vs/workbench/contrib/notebook/common/model/cellEdit'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; @@ -116,8 +117,8 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel public viewType: string, public supportBackup: boolean, public uri: URI, - private _undoService: IUndoRedoService, - private _modelService: ITextModelService + @IUndoRedoService private _undoService: IUndoRedoService, + @ITextModelService private _modelService: ITextModelService ) { super(); this.cells = []; @@ -172,7 +173,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._increaseVersionId(); } - $applyEdit(modelVersionId: number, rawEdits: ICellEditOperation[], emitToExtHost: boolean, synchronous: boolean): boolean { + $applyEdit(modelVersionId: number, rawEdits: ICellEditOperation[], synchronous: boolean): boolean { if (modelVersionId !== this._versionId) { return false; } @@ -233,22 +234,20 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return [diff.start, diff.deleteCount, diff.toInsert] as [number, number, NotebookCellTextModel[]]; }); - if (emitToExtHost) { - 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[]]) - }); - } + 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]); @@ -266,6 +265,21 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return true; } + $handleEdit(label: string | undefined, undo: () => void, redo: () => void): void { + this._undoService.pushElement({ + type: UndoRedoElementType.Resource, + resource: this.uri, + label: label ?? nls.localize('defaultEditLabel', "Edit"), + undo: async () => { + undo(); + }, + redo: async () => { + redo(); + }, + }); + this.setDirty(true); + } + createSnapshot(preserveBOM?: boolean): ITextSnapshot { return new NotebookTextModelSnapshot(this); } diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index 35213ab164a..a0e064a2aec 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -14,7 +14,6 @@ import { IWorkingCopyService, IWorkingCopy, IWorkingCopyBackup, WorkingCopyCapab import { basename } from 'vs/base/common/resources'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; -import { DefaultEndOfLine, ITextBuffer, EndOfLinePreference } from 'vs/editor/common/model'; import { Schemas } from 'vs/base/common/network'; import { IFileStatWithMetadata, IFileService } from 'vs/platform/files/common/files'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; @@ -142,42 +141,9 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN return this; // Make sure meanwhile someone else did not succeed in loading } - if (backup && backup.meta?.backupId === undefined) { - try { - return await this.loadFromBackup(backup.value.create(DefaultEndOfLine.LF), options?.editorId); - } catch (error) { - // this.logService.error('[text file model] load() from backup', error); // ignore error and continue to load as file below - } - } - return this.loadFromProvider(false, options?.editorId, backup?.meta?.backupId); } - private async loadFromBackup(content: ITextBuffer, editorId?: string): Promise { - const fullRange = content.getRangeAt(0, content.getLength()); - const data = JSON.parse(content.getValueInRange(fullRange, EndOfLinePreference.LF)); - - const notebook = await this._notebookService.createNotebookFromBackup(this.viewType!, this.resource, data.metadata, data.languages, data.cells, editorId); - this._notebook = notebook!; - const newStats = await this._resolveStats(this.resource); - this._lastResolvedFileStat = newStats; - this._register(this._notebook); - - this._name = basename(this._notebook!.uri); - - this._register(this._notebook.onDidChangeContent(() => { - this._onDidChangeContent.fire(); - })); - this._register(this._notebook.onDidChangeDirty(() => { - this._onDidChangeDirty.fire(); - })); - - await this._backupFileService.discardBackup(this._workingCopyResource); - this._notebook.setDirty(true); - - return this; - } - private async loadFromProvider(forceReloadFromDisk: boolean, editorId: string | undefined, backupId: string | undefined) { const notebook = await this._notebookService.resolveNotebook(this.viewType!, this.resource, forceReloadFromDisk, editorId, backupId); this._notebook = notebook!; diff --git a/src/vs/workbench/contrib/notebook/common/notebookService.ts b/src/vs/workbench/contrib/notebook/common/notebookService.ts index 36f7d3192fa..fab79518dad 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookService.ts @@ -9,7 +9,7 @@ import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/noteb import { NotebookExtensionDescription } from 'vs/workbench/api/common/extHost.protocol'; import { Event } from 'vs/base/common/event'; import { - INotebookTextModel, INotebookRendererInfo, NotebookDocumentMetadata, ICellDto2, INotebookKernelInfo, INotebookKernelInfoDto, INotebookTextModelBackup, + INotebookTextModel, INotebookRendererInfo, INotebookKernelInfo, INotebookKernelInfoDto, IEditor, ICellEditOperation, NotebookCellOutputsSplice, IOrderedMimeType, IProcessedOutput, INotebookKernelProvider, INotebookKernelInfo2 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; @@ -22,12 +22,14 @@ export const INotebookService = createDecorator('notebookServi export interface IMainNotebookController { kernel: INotebookKernelInfoDto | undefined; - createNotebook(viewType: string, uri: URI, backup: INotebookTextModelBackup | undefined, forceReload: boolean, editorId?: string, backupId?: string): Promise; + supportBackup: boolean; + createNotebook(textModel: NotebookTextModel, editorId?: string, backupId?: string): Promise; + reloadNotebook(mainthreadTextModel: NotebookTextModel): Promise; resolveNotebookEditor(viewType: string, uri: URI, editorId: string): Promise; executeNotebookByAttachedKernel(viewType: string, uri: URI, token: CancellationToken): Promise; onDidReceiveMessage(editorId: string, rendererType: string | undefined, message: any): void; executeNotebookCell(uri: URI, handle: number, token: CancellationToken): Promise; - removeNotebookDocument(notebook: INotebookTextModel): Promise; + removeNotebookDocument(uri: URI): Promise; save(uri: URI, token: CancellationToken): Promise; saveAs(uri: URI, target: URI, token: CancellationToken): Promise; backup(uri: URI, token: CancellationToken): Promise; @@ -58,7 +60,7 @@ export interface INotebookService { getContributedNotebookKernels2(viewType: string, resource: URI, token: CancellationToken): Promise; getRendererInfo(id: string): INotebookRendererInfo | undefined; resolveNotebook(viewType: string, uri: URI, forceReload: boolean, editorId?: string, backupId?: string): Promise; - createNotebookFromBackup(viewType: string, uri: URI, metadata: NotebookDocumentMetadata, languages: string[], cells: ICellDto2[], editorId?: string): Promise; + getNotebookTextModel(uri: URI): NotebookTextModel | undefined; executeNotebook(viewType: string, uri: URI, token: CancellationToken): Promise; executeNotebookCell(viewType: string, uri: URI, handle: number, token: CancellationToken): Promise; executeNotebook2(viewType: string, uri: URI, kernelId: string, token: CancellationToken): Promise; diff --git a/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts index a5e5a19039c..23ffd1fb439 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts @@ -32,7 +32,7 @@ suite('NotebookTextModel', () => { textModel.$applyEdit(textModel.versionId, [ { editType: CellEditType.Insert, index: 1, cells: [new TestCell(viewModel.viewType, 5, ['var e = 5;'], 'javascript', CellKind.Code, [], textModelService)] }, { editType: CellEditType.Insert, index: 3, cells: [new TestCell(viewModel.viewType, 6, ['var f = 6;'], 'javascript', CellKind.Code, [], textModelService)] }, - ], true, true); + ], true); assert.equal(textModel.cells.length, 6); @@ -57,7 +57,7 @@ suite('NotebookTextModel', () => { textModel.$applyEdit(textModel.versionId, [ { editType: CellEditType.Insert, index: 1, cells: [new TestCell(viewModel.viewType, 5, ['var e = 5;'], 'javascript', CellKind.Code, [], textModelService)] }, { editType: CellEditType.Insert, index: 1, cells: [new TestCell(viewModel.viewType, 6, ['var f = 6;'], 'javascript', CellKind.Code, [], textModelService)] }, - ], true, true); + ], true); assert.equal(textModel.cells.length, 6); @@ -82,7 +82,7 @@ suite('NotebookTextModel', () => { textModel.$applyEdit(textModel.versionId, [ { editType: CellEditType.Delete, index: 1, count: 1 }, { editType: CellEditType.Delete, index: 3, count: 1 }, - ], true, true); + ], true); assert.equal(textModel.cells[0].getValue(), 'var a = 1;'); assert.equal(textModel.cells[1].getValue(), 'var c = 3;'); @@ -105,7 +105,7 @@ suite('NotebookTextModel', () => { textModel.$applyEdit(textModel.versionId, [ { editType: CellEditType.Delete, index: 1, count: 1 }, { editType: CellEditType.Insert, index: 3, cells: [new TestCell(viewModel.viewType, 5, ['var e = 5;'], 'javascript', CellKind.Code, [], textModelService)] }, - ], true, true); + ], true); assert.equal(textModel.cells.length, 4); @@ -130,7 +130,7 @@ suite('NotebookTextModel', () => { textModel.$applyEdit(textModel.versionId, [ { editType: CellEditType.Delete, index: 1, count: 1 }, { editType: CellEditType.Insert, index: 1, cells: [new TestCell(viewModel.viewType, 5, ['var e = 5;'], 'javascript', CellKind.Code, [], textModelService)] }, - ], true, true); + ], true); assert.equal(textModel.cells.length, 4); assert.equal(textModel.cells[0].getValue(), 'var a = 1;');