diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index c1fe2a39f11..5025eefc64a 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -6,485 +6,22 @@ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { readonly } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { hash } from 'vs/base/common/hash'; -import { Disposable, DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { Schemas } from 'vs/base/common/network'; -import { joinPath } from 'vs/base/common/resources'; -import { ISplice } from 'vs/base/common/sequence'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; import * as UUID from 'vs/base/common/uuid'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { CellKind, ExtHostNotebookShape, ICommandDto, IMainContext, IModelAddedData, INotebookDocumentPropertiesChangeData, INotebookDocumentsAndEditorsDelta, INotebookEditorPropertiesChangeData, MainContext, MainThreadNotebookShape, NotebookCellOutputsSplice } from 'vs/workbench/api/common/extHost.protocol'; +import { CellKind, ExtHostNotebookShape, ICommandDto, IMainContext, IModelAddedData, INotebookDocumentPropertiesChangeData, INotebookDocumentsAndEditorsDelta, INotebookEditorPropertiesChangeData, MainContext, MainThreadNotebookShape } from 'vs/workbench/api/common/extHost.protocol'; import { ILogService } from 'vs/platform/log/common/log'; import { CommandsConverter, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; -import { ExtHostDocumentsAndEditors, IExtHostModelAddedData } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; 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, NotebookCellsChangedEventDto, NotebookCellsChangeType, NotebookCellsSplice2, NotebookDataDto, NotebookDocumentMetadata, notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { addIdToOutput, CellEditType, CellStatusbarAlignment, CellUri, ICellEditOperation, ICellReplaceEdit, INotebookCellStatusBarEntry, INotebookDisplayOrder, INotebookEditData, INotebookKernelInfoDto2, NotebookCellMetadata, NotebookCellsChangedEventDto, NotebookCellsChangeType, NotebookDataDto, NotebookDocumentMetadata, notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import * as vscode from 'vscode'; -import { Cache } from './cache'; import { ResourceMap } from 'vs/base/common/map'; - -interface IObservable { - proxy: T; - onDidChange: Event; -} - -function getObservable(obj: T): IObservable { - const onDidChange = new Emitter(); - const proxy = new Proxy(obj, { - set(target: T, p: PropertyKey, value: any, _receiver: any): boolean { - target[p as keyof T] = value; - onDidChange.fire(); - return true; - } - }); - - return { - proxy, - onDidChange: onDidChange.event - }; -} - -interface INotebookEventEmitter { - emitModelChange(events: vscode.NotebookCellsChangeEvent): void; - emitCellOutputsChange(event: vscode.NotebookCellOutputsChangeEvent): void; - emitCellLanguageChange(event: vscode.NotebookCellLanguageChangeEvent): void; - emitCellMetadataChange(event: vscode.NotebookCellMetadataChangeEvent): void; -} - -export class ExtHostCell extends Disposable { - - public static asModelAddData(notebook: vscode.NotebookDocument, cell: IMainCellDto): IExtHostModelAddedData { - return { - EOL: cell.eol, - lines: cell.source, - modeId: cell.language, - uri: cell.uri, - isDirty: false, - versionId: 1, - notebook - }; - } - - private _onDidDispose = new Emitter(); - readonly onDidDispose: Event = this._onDidDispose.event; - - private _onDidChangeOutputs = new Emitter[]>(); - readonly onDidChangeOutputs: Event[]> = this._onDidChangeOutputs.event; - - private _outputs: any[]; - private _outputMapping = new WeakMap(); - - private _metadata: vscode.NotebookCellMetadata; - private _metadataChangeListener: IDisposable; - - readonly handle: number; - readonly uri: URI; - readonly cellKind: CellKind; - - private _cell: vscode.NotebookCell | undefined; - - constructor( - private readonly _proxy: MainThreadNotebookShape, - private readonly _notebook: ExtHostNotebookDocument, - private readonly _extHostDocument: ExtHostDocumentsAndEditors, - private readonly _cellData: IMainCellDto, - ) { - super(); - - this.handle = _cellData.handle; - this.uri = URI.revive(_cellData.uri); - this.cellKind = _cellData.cellKind; - - this._outputs = _cellData.outputs; - for (const output of this._outputs) { - this._outputMapping.set(output, output.outputId); - delete output.outputId; - } - - const observableMetadata = getObservable(_cellData.metadata ?? {}); - this._metadata = observableMetadata.proxy; - this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { - this._updateMetadata(); - })); - } - - get cell(): vscode.NotebookCell { - if (!this._cell) { - const that = this; - const document = this._extHostDocument.getDocument(this.uri)!.document; - this._cell = Object.freeze({ - notebook: that._notebook.notebookDocument, - uri: that.uri, - cellKind: this._cellData.cellKind, - document, - language: document.languageId, - get outputs() { return that._outputs; }, - set outputs(value) { that._updateOutputs(value); }, - get metadata() { return that._metadata; }, - set metadata(value) { - that.setMetadata(value); - that._updateMetadata(); - }, - }); - } - return this._cell; - } - - dispose() { - super.dispose(); - this._onDidDispose.fire(); - } - - setOutputs(newOutputs: vscode.CellOutput[]): void { - this._outputs = newOutputs; - } - - private _updateOutputs(newOutputs: vscode.CellOutput[]) { - const rawDiffs = diff(this._outputs || [], newOutputs || [], (a) => { - return this._outputMapping.has(a); - }); - - const transformedDiffs: ISplice[] = rawDiffs.map(diff => { - for (let i = diff.start; i < diff.start + diff.deleteCount; i++) { - this._outputMapping.delete(this._outputs[i]); - } - - return { - deleteCount: diff.deleteCount, - start: diff.start, - toInsert: diff.toInsert.map((output): IProcessedOutput => { - if (output.outputKind === CellOutputKind.Rich) { - const uuid = UUID.generateUuid(); - this._outputMapping.set(output, uuid); - return { ...output, outputId: uuid }; - } - - this._outputMapping.set(output, undefined); - return output; - }) - }; - }); - - this._outputs = newOutputs; - this._onDidChangeOutputs.fire(transformedDiffs); - } - - setMetadata(newMetadata: vscode.NotebookCellMetadata): void { - // Don't apply metadata defaults here, 'undefined' means 'inherit from document metadata' - this._metadataChangeListener.dispose(); - const observableMetadata = getObservable(newMetadata); - this._metadata = observableMetadata.proxy; - this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { - this._updateMetadata(); - })); - } - - private _updateMetadata(): Promise { - return this._proxy.$updateNotebookCellMetadata(this._notebook.notebookDocument.viewType, this._notebook.uri, this.handle, this._metadata); - } -} - -class RawContentChangeEvent { - - constructor(readonly start: number, readonly deletedCount: number, readonly deletedItems: ExtHostCell[], readonly items: ExtHostCell[]) { } - - static asApiEvent(event: RawContentChangeEvent): vscode.NotebookCellsChangeData { - return Object.freeze({ - start: event.start, - deletedCount: event.deletedCount, - deletedItems: event.deletedItems.map(data => data.cell), - items: event.items.map(data => data.cell) - }); - } -} - -export class ExtHostNotebookDocument extends Disposable { - - private static _handlePool: number = 0; - readonly handle = ExtHostNotebookDocument._handlePool++; - - private _cells: ExtHostCell[] = []; - - private _cellDisposableMapping = new Map(); - - private _notebook: vscode.NotebookDocument | undefined; - private _metadata: Required; - private _metadataChangeListener: IDisposable; - private _versionId = 0; - private _isDirty: boolean = false; - private _backupCounter = 1; - private _backup?: vscode.NotebookDocumentBackup; - private _disposed = false; - private _languages: string[] = []; - - private readonly _edits = new Cache('notebook documents'); - - constructor( - private readonly _proxy: MainThreadNotebookShape, - 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(metadata); - this._metadata = observableMetadata.proxy; - this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { - this._tryUpdateMetadata(); - })); - } - - dispose() { - this._disposed = true; - super.dispose(); - dispose(this._cellDisposableMapping.values()); - } - - private _updateMetadata(newMetadata: Required) { - this._metadataChangeListener.dispose(); - newMetadata = { - ...notebookDocumentMetadataDefaults, - ...newMetadata - }; - if (this._metadataChangeListener) { - this._metadataChangeListener.dispose(); - } - - const observableMetadata = getObservable(newMetadata); - this._metadata = observableMetadata.proxy; - this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { - this._tryUpdateMetadata(); - })); - - this._tryUpdateMetadata(); - } - - private _tryUpdateMetadata() { - this._proxy.$updateNotebookMetadata(this._viewType, this.uri, this._metadata); - } - get notebookDocument(): vscode.NotebookDocument { - if (!this._notebook) { - const that = this; - this._notebook = Object.freeze({ - get uri() { return that.uri; }, - get version() { return that._versionId; }, - get fileName() { return that.uri.fsPath; }, - get viewType() { return that._viewType; }, - get isDirty() { return that._isDirty; }, - get isUntitled() { return that.uri.scheme === Schemas.untitled; }, - get cells(): ReadonlyArray { return that._cells.map(cell => cell.cell); }, - get languages() { return that._languages; }, - set languages(value: string[]) { that._trySetLanguages(value); }, - get metadata() { return that._metadata; }, - set metadata(value: Required) { that._updateMetadata(value); }, - }); - } - return this._notebook; - } - - private _trySetLanguages(newLanguages: string[]) { - this._languages = newLanguages; - this._proxy.$updateNotebookLanguages(this._viewType, this.uri, this._languages); - } - - getNewBackupUri(): URI { - if (!this._storagePath) { - throw new Error('Backup requires a valid storage path'); - } - const fileName = hashPath(this.uri) + (this._backupCounter++); - return joinPath(this._storagePath, fileName); - } - - updateBackup(backup: vscode.NotebookDocumentBackup): void { - this._backup?.delete(); - this._backup = backup; - } - - disposeBackup(): void { - this._backup?.delete(); - this._backup = undefined; - } - - acceptModelChanged(event: NotebookCellsChangedEventDto, isDirty: boolean): void { - this._versionId = event.versionId; - this._isDirty = isDirty; - if (event.kind === NotebookCellsChangeType.Initialize) { - this._spliceNotebookCells(event.changes, true); - } if (event.kind === NotebookCellsChangeType.ModelChange) { - this._spliceNotebookCells(event.changes, false); - } else if (event.kind === NotebookCellsChangeType.Move) { - this._moveCell(event.index, event.newIdx); - } else if (event.kind === NotebookCellsChangeType.Output) { - this._setCellOutputs(event.index, event.outputs); - } else if (event.kind === NotebookCellsChangeType.ChangeLanguage) { - this._changeCellLanguage(event.index, event.language); - } else if (event.kind === NotebookCellsChangeType.ChangeCellMetadata) { - this._changeCellMetadata(event.index, event.metadata); - } - } - - private _spliceNotebookCells(splices: NotebookCellsSplice2[], initialization: boolean): void { - if (this._disposed) { - return; - } - - const contentChangeEvents: RawContentChangeEvent[] = []; - const addedCellDocuments: IExtHostModelAddedData[] = []; - const removedCellDocuments: URI[] = []; - - splices.reverse().forEach(splice => { - const cellDtos = splice[2]; - const newCells = cellDtos.map(cell => { - - const extCell = new ExtHostCell(this._proxy, this, this._documentsAndEditors, cell); - - if (!initialization) { - addedCellDocuments.push(ExtHostCell.asModelAddData(this.notebookDocument, cell)); - } - - if (!this._cellDisposableMapping.has(extCell.handle)) { - const store = new DisposableStore(); - store.add(extCell); - this._cellDisposableMapping.set(extCell.handle, store); - } - - const store = this._cellDisposableMapping.get(extCell.handle)!; - - store.add(extCell.onDidChangeOutputs((diffs) => { - this.eventuallyUpdateCellOutputs(extCell, diffs); - })); - - return extCell; - }); - - for (let j = splice[0]; j < splice[0] + splice[1]; j++) { - this._cellDisposableMapping.get(this._cells[j].handle)?.dispose(); - this._cellDisposableMapping.delete(this._cells[j].handle); - } - - const deletedItems = this._cells.splice(splice[0], splice[1], ...newCells); - for (let cell of deletedItems) { - removedCellDocuments.push(cell.uri); - } - - contentChangeEvents.push(new RawContentChangeEvent(splice[0], splice[1], deletedItems, newCells)); - }); - - this._documentsAndEditors.acceptDocumentsAndEditorsDelta({ - addedDocuments: addedCellDocuments, - removedDocuments: removedCellDocuments - }); - - if (!initialization) { - this._emitter.emitModelChange({ - document: this.notebookDocument, - changes: contentChangeEvents.map(RawContentChangeEvent.asApiEvent) - }); - } - } - - private _moveCell(index: number, newIdx: number): void { - const cells = this._cells.splice(index, 1); - this._cells.splice(newIdx, 0, ...cells); - const changes: vscode.NotebookCellsChangeData[] = [{ - start: index, - deletedCount: 1, - deletedItems: cells.map(data => data.cell), - items: [] - }, { - start: newIdx, - deletedCount: 0, - deletedItems: [], - items: cells.map(data => data.cell) - }]; - this._emitter.emitModelChange({ - document: this.notebookDocument, - changes - }); - } - - private _setCellOutputs(index: number, outputs: IProcessedOutput[]): void { - const cell = this._cells[index]; - cell.setOutputs(outputs); - this._emitter.emitCellOutputsChange({ document: this.notebookDocument, cells: [cell.cell] }); - } - - private _changeCellLanguage(index: number, language: string): void { - const cell = this._cells[index]; - const event: vscode.NotebookCellLanguageChangeEvent = { document: this.notebookDocument, cell: cell.cell, language }; - this._emitter.emitCellLanguageChange(event); - } - - private _changeCellMetadata(index: number, newMetadata: NotebookCellMetadata | undefined): void { - const cell = this._cells[index]; - cell.setMetadata(newMetadata || {}); - const event: vscode.NotebookCellMetadataChangeEvent = { document: this.notebookDocument, cell: cell.cell }; - this._emitter.emitCellMetadataChange(event); - } - - async eventuallyUpdateCellOutputs(cell: ExtHostCell, diffs: ISplice[]) { - const outputDtos: NotebookCellOutputsSplice[] = diffs.map(diff => { - const outputs = diff.toInsert; - return [diff.start, diff.deleteCount, outputs]; - }); - - if (!outputDtos.length) { - return; - } - - await this._proxy.$spliceNotebookCellOutputs(this._viewType, this.uri, cell.handle, outputDtos); - this._emitter.emitCellOutputsChange({ - document: this.notebookDocument, - cells: [cell.cell] - }); - } - - getCell(cellHandle: number): ExtHostCell | undefined { - return this._cells.find(cell => cell.handle === cellHandle); - } - - - addEdit(item: vscode.NotebookDocumentEditEvent): number { - return this._edits.add([item]); - } - - async undo(editId: number, isDirty: boolean): Promise { - await this.getEdit(editId).undo(); - // if (!isDirty) { - // this.disposeBackup(); - // } - } - - async redo(editId: number, isDirty: boolean): Promise { - await this.getEdit(editId).redo(); - // if (!isDirty) { - // this.disposeBackup(); - // } - } - - private getEdit(editId: number): vscode.NotebookDocumentEditEvent { - const edit = this._edits.get(editId, 0); - if (!edit) { - throw new Error('No edit found'); - } - - return edit; - } - - disposeEdits(editIds: number[]): void { - for (const id of editIds) { - this._edits.delete(id); - } - } -} +import { ExtHostCell, ExtHostNotebookDocument } from './extHostNotebookDocument'; export class NotebookEditorCellEditBuilder implements vscode.NotebookEditorEdit { @@ -1395,7 +932,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN emitCellMetadataChange(event: vscode.NotebookCellMetadataChangeEvent): void { that._onDidChangeCellMetadata.fire(event); } - }, viewType, { ...notebookDocumentMetadataDefaults, ...modelData.metadata }, uri, this, storageRoot); + }, viewType, { ...notebookDocumentMetadataDefaults, ...modelData.metadata }, uri, storageRoot); document.acceptModelChanged({ kind: NotebookCellsChangeType.Initialize, @@ -1520,11 +1057,6 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } } -function hashPath(resource: URI): string { - const str = resource.scheme === Schemas.file || resource.scheme === Schemas.untitled ? resource.fsPath : resource.toString(); - return hash(str) + ''; -} - function isEditEvent(e: vscode.NotebookDocumentEditEvent | vscode.NotebookDocumentContentChangeEvent): e is vscode.NotebookDocumentEditEvent { return typeof (e as vscode.NotebookDocumentEditEvent).undo === 'function' && typeof (e as vscode.NotebookDocumentEditEvent).redo === 'function'; diff --git a/src/vs/workbench/api/common/extHostNotebookDocument.ts b/src/vs/workbench/api/common/extHostNotebookDocument.ts new file mode 100644 index 00000000000..363d0eb98df --- /dev/null +++ b/src/vs/workbench/api/common/extHostNotebookDocument.ts @@ -0,0 +1,482 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { hash } from 'vs/base/common/hash'; +import { Disposable, DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { joinPath } from 'vs/base/common/resources'; +import { ISplice } from 'vs/base/common/sequence'; +import { URI } from 'vs/base/common/uri'; +import * as UUID from 'vs/base/common/uuid'; +import { CellKind, MainThreadNotebookShape, NotebookCellOutputsSplice } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostDocumentsAndEditors, IExtHostModelAddedData } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import { CellOutputKind, diff, IMainCellDto, IProcessedOutput, NotebookCellMetadata, NotebookCellsChangedEventDto, NotebookCellsChangeType, NotebookCellsSplice2, notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import * as vscode from 'vscode'; +import { Cache } from './cache'; + + +interface IObservable { + proxy: T; + onDidChange: Event; +} + +function getObservable(obj: T): IObservable { + const onDidChange = new Emitter(); + const proxy = new Proxy(obj, { + set(target: T, p: PropertyKey, value: any, _receiver: any): boolean { + target[p as keyof T] = value; + onDidChange.fire(); + return true; + } + }); + + return { + proxy, + onDidChange: onDidChange.event + }; +} + +class RawContentChangeEvent { + + constructor(readonly start: number, readonly deletedCount: number, readonly deletedItems: ExtHostCell[], readonly items: ExtHostCell[]) { } + + static asApiEvent(event: RawContentChangeEvent): vscode.NotebookCellsChangeData { + return Object.freeze({ + start: event.start, + deletedCount: event.deletedCount, + deletedItems: event.deletedItems.map(data => data.cell), + items: event.items.map(data => data.cell) + }); + } +} + +export class ExtHostCell extends Disposable { + + static asModelAddData(notebook: vscode.NotebookDocument, cell: IMainCellDto): IExtHostModelAddedData { + return { + EOL: cell.eol, + lines: cell.source, + modeId: cell.language, + uri: cell.uri, + isDirty: false, + versionId: 1, + notebook + }; + } + + private _onDidDispose = new Emitter(); + readonly onDidDispose: Event = this._onDidDispose.event; + + private _onDidChangeOutputs = new Emitter[]>(); + readonly onDidChangeOutputs: Event[]> = this._onDidChangeOutputs.event; + + private _outputs: any[]; + private _outputMapping = new WeakMap(); + + private _metadata: vscode.NotebookCellMetadata; + private _metadataChangeListener: IDisposable; + + readonly handle: number; + readonly uri: URI; + readonly cellKind: CellKind; + + private _cell: vscode.NotebookCell | undefined; + + constructor( + private readonly _proxy: MainThreadNotebookShape, + private readonly _notebook: ExtHostNotebookDocument, + private readonly _extHostDocument: ExtHostDocumentsAndEditors, + private readonly _cellData: IMainCellDto, + ) { + super(); + + this.handle = _cellData.handle; + this.uri = URI.revive(_cellData.uri); + this.cellKind = _cellData.cellKind; + + this._outputs = _cellData.outputs; + for (const output of this._outputs) { + this._outputMapping.set(output, output.outputId); + delete output.outputId; + } + + const observableMetadata = getObservable(_cellData.metadata ?? {}); + this._metadata = observableMetadata.proxy; + this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { + this._updateMetadata(); + })); + } + + get cell(): vscode.NotebookCell { + if (!this._cell) { + const that = this; + const document = this._extHostDocument.getDocument(this.uri)!.document; + this._cell = Object.freeze({ + notebook: that._notebook.notebookDocument, + uri: that.uri, + cellKind: this._cellData.cellKind, + document, + language: document.languageId, + get outputs() { return that._outputs; }, + set outputs(value) { that._updateOutputs(value); }, + get metadata() { return that._metadata; }, + set metadata(value) { + that.setMetadata(value); + that._updateMetadata(); + }, + }); + } + return this._cell; + } + + dispose() { + super.dispose(); + this._onDidDispose.fire(); + } + + setOutputs(newOutputs: vscode.CellOutput[]): void { + this._outputs = newOutputs; + } + + private _updateOutputs(newOutputs: vscode.CellOutput[]) { + const rawDiffs = diff(this._outputs || [], newOutputs || [], (a) => { + return this._outputMapping.has(a); + }); + + const transformedDiffs: ISplice[] = rawDiffs.map(diff => { + for (let i = diff.start; i < diff.start + diff.deleteCount; i++) { + this._outputMapping.delete(this._outputs[i]); + } + + return { + deleteCount: diff.deleteCount, + start: diff.start, + toInsert: diff.toInsert.map((output): IProcessedOutput => { + if (output.outputKind === CellOutputKind.Rich) { + const uuid = UUID.generateUuid(); + this._outputMapping.set(output, uuid); + return { ...output, outputId: uuid }; + } + + this._outputMapping.set(output, undefined); + return output; + }) + }; + }); + + this._outputs = newOutputs; + this._onDidChangeOutputs.fire(transformedDiffs); + } + + setMetadata(newMetadata: vscode.NotebookCellMetadata): void { + // Don't apply metadata defaults here, 'undefined' means 'inherit from document metadata' + this._metadataChangeListener.dispose(); + const observableMetadata = getObservable(newMetadata); + this._metadata = observableMetadata.proxy; + this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { + this._updateMetadata(); + })); + } + + private _updateMetadata(): Promise { + return this._proxy.$updateNotebookCellMetadata(this._notebook.notebookDocument.viewType, this._notebook.uri, this.handle, this._metadata); + } +} + +export interface INotebookEventEmitter { + emitModelChange(events: vscode.NotebookCellsChangeEvent): void; + emitCellOutputsChange(event: vscode.NotebookCellOutputsChangeEvent): void; + emitCellLanguageChange(event: vscode.NotebookCellLanguageChangeEvent): void; + emitCellMetadataChange(event: vscode.NotebookCellMetadataChangeEvent): void; +} + +function hashPath(resource: URI): string { + const str = resource.scheme === Schemas.file || resource.scheme === Schemas.untitled ? resource.fsPath : resource.toString(); + return hash(str) + ''; +} + +export class ExtHostNotebookDocument extends Disposable { + + private static _handlePool: number = 0; + readonly handle = ExtHostNotebookDocument._handlePool++; + + private _cells: ExtHostCell[] = []; + + private _cellDisposableMapping = new Map(); + + private _notebook: vscode.NotebookDocument | undefined; + private _metadata: Required; + private _metadataChangeListener: IDisposable; + private _versionId = 0; + private _isDirty: boolean = false; + private _backupCounter = 1; + private _backup?: vscode.NotebookDocumentBackup; + private _disposed = false; + private _languages: string[] = []; + + private readonly _edits = new Cache('notebook documents'); + + constructor( + private readonly _proxy: MainThreadNotebookShape, + private readonly _documentsAndEditors: ExtHostDocumentsAndEditors, + private readonly _emitter: INotebookEventEmitter, + private readonly _viewType: string, + metadata: Required, + public readonly uri: URI, + private readonly _storagePath: URI | undefined + ) { + super(); + + const observableMetadata = getObservable(metadata); + this._metadata = observableMetadata.proxy; + this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { + this._tryUpdateMetadata(); + })); + } + + dispose() { + this._disposed = true; + super.dispose(); + dispose(this._cellDisposableMapping.values()); + } + + private _updateMetadata(newMetadata: Required) { + this._metadataChangeListener.dispose(); + newMetadata = { + ...notebookDocumentMetadataDefaults, + ...newMetadata + }; + if (this._metadataChangeListener) { + this._metadataChangeListener.dispose(); + } + + const observableMetadata = getObservable(newMetadata); + this._metadata = observableMetadata.proxy; + this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { + this._tryUpdateMetadata(); + })); + + this._tryUpdateMetadata(); + } + + private _tryUpdateMetadata() { + this._proxy.$updateNotebookMetadata(this._viewType, this.uri, this._metadata); + } + get notebookDocument(): vscode.NotebookDocument { + if (!this._notebook) { + const that = this; + this._notebook = Object.freeze({ + get uri() { return that.uri; }, + get version() { return that._versionId; }, + get fileName() { return that.uri.fsPath; }, + get viewType() { return that._viewType; }, + get isDirty() { return that._isDirty; }, + get isUntitled() { return that.uri.scheme === Schemas.untitled; }, + get cells(): ReadonlyArray { return that._cells.map(cell => cell.cell); }, + get languages() { return that._languages; }, + set languages(value: string[]) { that._trySetLanguages(value); }, + get metadata() { return that._metadata; }, + set metadata(value: Required) { that._updateMetadata(value); }, + }); + } + return this._notebook; + } + + private _trySetLanguages(newLanguages: string[]) { + this._languages = newLanguages; + this._proxy.$updateNotebookLanguages(this._viewType, this.uri, this._languages); + } + + getNewBackupUri(): URI { + if (!this._storagePath) { + throw new Error('Backup requires a valid storage path'); + } + const fileName = hashPath(this.uri) + (this._backupCounter++); + return joinPath(this._storagePath, fileName); + } + + updateBackup(backup: vscode.NotebookDocumentBackup): void { + this._backup?.delete(); + this._backup = backup; + } + + disposeBackup(): void { + this._backup?.delete(); + this._backup = undefined; + } + + acceptModelChanged(event: NotebookCellsChangedEventDto, isDirty: boolean): void { + this._versionId = event.versionId; + this._isDirty = isDirty; + if (event.kind === NotebookCellsChangeType.Initialize) { + this._spliceNotebookCells(event.changes, true); + } if (event.kind === NotebookCellsChangeType.ModelChange) { + this._spliceNotebookCells(event.changes, false); + } else if (event.kind === NotebookCellsChangeType.Move) { + this._moveCell(event.index, event.newIdx); + } else if (event.kind === NotebookCellsChangeType.Output) { + this._setCellOutputs(event.index, event.outputs); + } else if (event.kind === NotebookCellsChangeType.ChangeLanguage) { + this._changeCellLanguage(event.index, event.language); + } else if (event.kind === NotebookCellsChangeType.ChangeCellMetadata) { + this._changeCellMetadata(event.index, event.metadata); + } + } + + private _spliceNotebookCells(splices: NotebookCellsSplice2[], initialization: boolean): void { + if (this._disposed) { + return; + } + + const contentChangeEvents: RawContentChangeEvent[] = []; + const addedCellDocuments: IExtHostModelAddedData[] = []; + const removedCellDocuments: URI[] = []; + + splices.reverse().forEach(splice => { + const cellDtos = splice[2]; + const newCells = cellDtos.map(cell => { + + const extCell = new ExtHostCell(this._proxy, this, this._documentsAndEditors, cell); + + if (!initialization) { + addedCellDocuments.push(ExtHostCell.asModelAddData(this.notebookDocument, cell)); + } + + if (!this._cellDisposableMapping.has(extCell.handle)) { + const store = new DisposableStore(); + store.add(extCell); + this._cellDisposableMapping.set(extCell.handle, store); + } + + const store = this._cellDisposableMapping.get(extCell.handle)!; + + store.add(extCell.onDidChangeOutputs((diffs) => { + this.eventuallyUpdateCellOutputs(extCell, diffs); + })); + + return extCell; + }); + + for (let j = splice[0]; j < splice[0] + splice[1]; j++) { + this._cellDisposableMapping.get(this._cells[j].handle)?.dispose(); + this._cellDisposableMapping.delete(this._cells[j].handle); + } + + const deletedItems = this._cells.splice(splice[0], splice[1], ...newCells); + for (let cell of deletedItems) { + removedCellDocuments.push(cell.uri); + } + + contentChangeEvents.push(new RawContentChangeEvent(splice[0], splice[1], deletedItems, newCells)); + }); + + this._documentsAndEditors.acceptDocumentsAndEditorsDelta({ + addedDocuments: addedCellDocuments, + removedDocuments: removedCellDocuments + }); + + if (!initialization) { + this._emitter.emitModelChange({ + document: this.notebookDocument, + changes: contentChangeEvents.map(RawContentChangeEvent.asApiEvent) + }); + } + } + + private _moveCell(index: number, newIdx: number): void { + const cells = this._cells.splice(index, 1); + this._cells.splice(newIdx, 0, ...cells); + const changes: vscode.NotebookCellsChangeData[] = [{ + start: index, + deletedCount: 1, + deletedItems: cells.map(data => data.cell), + items: [] + }, { + start: newIdx, + deletedCount: 0, + deletedItems: [], + items: cells.map(data => data.cell) + }]; + this._emitter.emitModelChange({ + document: this.notebookDocument, + changes + }); + } + + private _setCellOutputs(index: number, outputs: IProcessedOutput[]): void { + const cell = this._cells[index]; + cell.setOutputs(outputs); + this._emitter.emitCellOutputsChange({ document: this.notebookDocument, cells: [cell.cell] }); + } + + private _changeCellLanguage(index: number, language: string): void { + const cell = this._cells[index]; + const event: vscode.NotebookCellLanguageChangeEvent = { document: this.notebookDocument, cell: cell.cell, language }; + this._emitter.emitCellLanguageChange(event); + } + + private _changeCellMetadata(index: number, newMetadata: NotebookCellMetadata | undefined): void { + const cell = this._cells[index]; + cell.setMetadata(newMetadata || {}); + const event: vscode.NotebookCellMetadataChangeEvent = { document: this.notebookDocument, cell: cell.cell }; + this._emitter.emitCellMetadataChange(event); + } + + async eventuallyUpdateCellOutputs(cell: ExtHostCell, diffs: ISplice[]) { + const outputDtos: NotebookCellOutputsSplice[] = diffs.map(diff => { + const outputs = diff.toInsert; + return [diff.start, diff.deleteCount, outputs]; + }); + + if (!outputDtos.length) { + return; + } + + await this._proxy.$spliceNotebookCellOutputs(this._viewType, this.uri, cell.handle, outputDtos); + this._emitter.emitCellOutputsChange({ + document: this.notebookDocument, + cells: [cell.cell] + }); + } + + getCell(cellHandle: number): ExtHostCell | undefined { + return this._cells.find(cell => cell.handle === cellHandle); + } + + + addEdit(item: vscode.NotebookDocumentEditEvent): number { + return this._edits.add([item]); + } + + async undo(editId: number, isDirty: boolean): Promise { + await this.getEdit(editId).undo(); + // if (!isDirty) { + // this.disposeBackup(); + // } + } + + async redo(editId: number, isDirty: boolean): Promise { + await this.getEdit(editId).redo(); + // if (!isDirty) { + // this.disposeBackup(); + // } + } + + private getEdit(editId: number): vscode.NotebookDocumentEditEvent { + const edit = this._edits.get(editId, 0); + if (!edit) { + throw new Error('No edit found'); + } + + return edit; + } + + disposeEdits(editIds: number[]): void { + for (const id of editIds) { + this._edits.delete(id); + } + } +} diff --git a/src/vs/workbench/test/browser/api/extHostNotebook.test.ts b/src/vs/workbench/test/browser/api/extHostNotebook.test.ts index 2f6156164f1..c707cc3e276 100644 --- a/src/vs/workbench/test/browser/api/extHostNotebook.test.ts +++ b/src/vs/workbench/test/browser/api/extHostNotebook.test.ts @@ -11,7 +11,8 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { NullLogService } from 'vs/platform/log/common/log'; import { mock } from 'vs/base/test/common/mock'; import { IModelAddedData, MainContext, MainThreadCommandsShape, MainThreadNotebookShape } from 'vs/workbench/api/common/extHost.protocol'; -import { ExtHostNotebookDocument, ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; +import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; +import { ExtHostNotebookDocument } from 'vs/workbench/api/common/extHostNotebookDocument'; import { CellKind, CellUri, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { URI } from 'vs/base/common/uri'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; diff --git a/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts b/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts index c9f18c332a9..71e9d8216c8 100644 --- a/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts +++ b/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts @@ -9,7 +9,8 @@ import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { NullLogService } from 'vs/platform/log/common/log'; import { ExtHostNotebookConcatDocument } from 'vs/workbench/api/common/extHostNotebookConcatDocument'; -import { ExtHostNotebookDocument, ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; +import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; +import { ExtHostNotebookDocument } from 'vs/workbench/api/common/extHostNotebookDocument'; import { URI } from 'vs/base/common/uri'; import { CellKind, CellUri, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Position, Location, Range } from 'vs/workbench/api/common/extHostTypes';