/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 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 { 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, IMainContext, IModelAddedData, INotebookDocumentsAndEditorsDelta, INotebookEditorPropertiesChangeData, MainContext, MainThreadNotebookShape, NotebookCellOutputsSplice } from 'vs/workbench/api/common/extHost.protocol'; import { ILogService } from 'vs/platform/log/common/log'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { ExtHostDocumentsAndEditors, IExtHostModelAddedData } 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 { CellEditType, CellOutputKind, diff, ICellDeleteEdit, ICellEditOperation, ICellInsertEdit, IMainCellDto, INotebookDisplayOrder, INotebookEditData, INotebookKernelInfoDto2, IOutputRenderRequest, IOutputRenderResponse, IOutputRenderResponseCellInfo, IOutputRenderResponseOutputInfo, IProcessedOutput, IRawOutput, NotebookCellMetadata, NotebookCellsChangedEvent, NotebookCellsChangeType, NotebookCellsSplice2, NotebookDataDto, notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import * as vscode from 'vscode'; import { Cache } from './cache'; import { ResourceMap } from 'vs/base/common/map'; 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; } const addIdToOutput = (output: IRawOutput, id = UUID.generateUuid()): IProcessedOutput => output.outputKind === CellOutputKind.Rich ? ({ ...output, outputId: id }) : output; export class ExtHostCell extends Disposable implements vscode.NotebookCell { public static asModelAddData(notebook: ExtHostNotebookDocument, cell: IMainCellDto): IExtHostModelAddedData { return { EOL: cell.eol, lines: cell.source, modeId: cell.language, uri: cell.uri, isDirty: false, versionId: 1, notebook }; } 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; constructor( private _proxy: MainThreadNotebookShape, readonly notebook: ExtHostNotebookDocument, private _extHostDocument: ExtHostDocumentsAndEditors, cell: IMainCellDto, ) { super(); this.handle = cell.handle; this.uri = URI.revive(cell.uri); this.cellKind = cell.cellKind; this._outputs = cell.outputs; for (const output of this._outputs) { this._outputMapping.set(output, output.outputId); delete output.outputId; } const observableMetadata = getObservable(cell.metadata ?? {}); this._metadata = observableMetadata.proxy; this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { this._updateMetadata(); })); } get document(): vscode.TextDocument { return this._extHostDocument.getDocument(this.uri)!.document; } get language(): string { return this.document.languageId; } get outputs() { return this._outputs; } set outputs(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); } get metadata() { return this._metadata; } set metadata(newMetadata: vscode.NotebookCellMetadata) { this.setMetadata(newMetadata); this._updateMetadata(); } 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.viewType, this.notebook.uri, this.handle, this._metadata); } } export class ExtHostNotebookDocument extends Disposable implements vscode.NotebookDocument { private static _handlePool: number = 0; readonly handle = ExtHostNotebookDocument._handlePool++; private _cells: ExtHostCell[] = []; private _cellDisposableMapping = new Map(); get cells() { return this._cells; } private _languages: string[] = []; get languages() { return this._languages = []; } set languages(newLanguages: string[]) { this._languages = newLanguages; this._proxy.$updateNotebookLanguages(this.viewType, this.uri, this._languages); } get isUntitled() { return this.uri.scheme === Schemas.untitled; } private _metadata: Required = notebookDocumentMetadataDefaults; private _metadataChangeListener: IDisposable; get metadata() { return this._metadata; } set metadata(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.updateMetadata(); })); this.updateMetadata(); } private _displayOrder: string[] = []; get displayOrder() { return this._displayOrder; } set displayOrder(newOrder: string[]) { this._displayOrder = newOrder; } private _versionId = 0; get versionId() { return this._versionId; } private _backupCounter = 1; private _backup?: vscode.NotebookDocumentBackup; private readonly _edits = new Cache('notebook documents'); 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); } } private _disposed = false; constructor( private readonly _proxy: MainThreadNotebookShape, private _documentsAndEditors: ExtHostDocumentsAndEditors, private _emitter: INotebookEventEmitter, public viewType: string, public uri: URI, public renderingHandler: ExtHostNotebookOutputRenderingHandler, private readonly _storagePath: URI | undefined ) { super(); const observableMetadata = getObservable(notebookDocumentMetadataDefaults); this._metadata = observableMetadata.proxy; this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { this.updateMetadata(); })); } private updateMetadata() { this._proxy.$updateNotebookMetadata(this.viewType, this.uri, this._metadata); } 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; } dispose() { this._disposed = true; super.dispose(); dispose(this._cellDisposableMapping.values()); } get fileName() { return this.uri.fsPath; } get isDirty() { return false; } acceptModelChanged(event: NotebookCellsChangedEvent): void { this._versionId = event.versionId; 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.CellClearOutput) { this._clearCellOutputs(event.index); } else if (event.kind === NotebookCellsChangeType.CellsClearOutput) { this._clearAllCellOutputs(); } else if (event.kind === NotebookCellsChangeType.ChangeLanguage) { this._changeCellLanguage(event.index, event.language); } else if (event.kind === NotebookCellsChangeType.ChangeMetadata) { this._changeCellMetadata(event.index, event.metadata); } } private _spliceNotebookCells(splices: NotebookCellsSplice2[], initialization: boolean): void { if (this._disposed) { return; } const contentChangeEvents: vscode.NotebookCellsChangeData[] = []; const addedCellDocuments: IExtHostModelAddedData[] = []; 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, cell)); } if (!this._cellDisposableMapping.has(extCell.handle)) { this._cellDisposableMapping.set(extCell.handle, new DisposableStore()); } 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); contentChangeEvents.push({ start: splice[0], deletedCount: splice[1], deletedItems, items: newCells }); }); if (addedCellDocuments) { this._documentsAndEditors.acceptDocumentsAndEditorsDelta({ addedDocuments: addedCellDocuments }); } if (!initialization) { this._emitter.emitModelChange({ document: this, changes: contentChangeEvents }); } } 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, items: [] }, { start: newIdx, deletedCount: 0, deletedItems: [], items: cells }]; this._emitter.emitModelChange({ document: this, changes }); } private _clearCellOutputs(index: number): void { const cell = this.cells[index]; cell.outputs = []; const event: vscode.NotebookCellOutputsChangeEvent = { document: this, cells: [cell] }; this._emitter.emitCellOutputsChange(event); } private _clearAllCellOutputs(): void { const modifedCells: vscode.NotebookCell[] = []; this.cells.forEach(cell => { if (cell.outputs.length !== 0) { cell.outputs = []; modifedCells.push(cell); } }); const event: vscode.NotebookCellOutputsChangeEvent = { document: this, cells: modifedCells }; this._emitter.emitCellOutputsChange(event); } private _changeCellLanguage(index: number, language: string): void { const cell = this.cells[index]; const event: vscode.NotebookCellLanguageChangeEvent = { document: this, cell, language }; this._emitter.emitCellLanguageChange(event); } private _changeCellMetadata(index: number, newMetadata: NotebookCellMetadata): void { const cell = this.cells[index]; cell.setMetadata(newMetadata); const event: vscode.NotebookCellMetadataChangeEvent = { document: this, cell }; this._emitter.emitCellMetadataChange(event); } async eventuallyUpdateCellOutputs(cell: ExtHostCell, diffs: ISplice[]) { const renderers = new Set(); const outputDtos: NotebookCellOutputsSplice[] = diffs.map(diff => { const outputs = diff.toInsert; return [diff.start, diff.deleteCount, outputs]; }); await this._proxy.$spliceNotebookCellOutputs(this.viewType, this.uri, cell.handle, outputDtos, Array.from(renderers)); this._emitter.emitCellOutputsChange({ document: this, cells: [cell] }); } getCell(cellHandle: number) { return this.cells.find(cell => cell.handle === cellHandle); } getCell2(cellUri: UriComponents) { return this.cells.find(cell => cell.uri.fragment === cellUri.fragment); } } export class NotebookEditorCellEditBuilder implements vscode.NotebookEditorCellEdit { private _finalized: boolean = false; private readonly _documentVersionId: number; private _collectedEdits: ICellEditOperation[] = []; private _renderers = new Set(); constructor( readonly editor: ExtHostNotebookEditor ) { this._documentVersionId = editor.document.versionId; } finalize(): INotebookEditData { this._finalized = true; return { documentVersionId: this._documentVersionId, edits: this._collectedEdits, renderers: Array.from(this._renderers) }; } private _throwIfFinalized() { if (this._finalized) { throw new Error('Edit is only valid while callback runs'); } } insert(index: number, content: string | string[], language: string, type: CellKind, outputs: vscode.CellOutput[], metadata: vscode.NotebookCellMetadata | undefined): void { this._throwIfFinalized(); const sourceArr = Array.isArray(content) ? content : content.split(/\r|\n|\r\n/g); const cell = { source: sourceArr, language, cellKind: type, outputs: outputs.map(o => addIdToOutput(o)), metadata, }; this._collectedEdits.push({ editType: CellEditType.Insert, index, cells: [cell] }); } delete(index: number): void { this._throwIfFinalized(); this._collectedEdits.push({ editType: CellEditType.Delete, index, count: 1 }); } } class ExtHostWebviewCommWrapper extends Disposable { private readonly _onDidReceiveDocumentMessage = new Emitter(); private readonly _rendererIdToEmitters = new Map>(); constructor( private _editorId: string, public uri: URI, private _proxy: MainThreadNotebookShape, private _webviewInitData: WebviewInitData, public document: ExtHostNotebookDocument, ) { super(); } public onDidReceiveMessage(forRendererId: string | undefined, message: any) { this._onDidReceiveDocumentMessage.fire(message); if (forRendererId !== undefined) { this._rendererIdToEmitters.get(forRendererId)?.fire(message); } } public readonly contentProviderComm: vscode.NotebookCommunication = { editorId: this._editorId, onDidReceiveMessage: this._onDidReceiveDocumentMessage.event, postMessage: (message: any) => this._proxy.$postMessage(this._editorId, undefined, message), asWebviewUri: (uri: vscode.Uri) => this._asWebviewUri(uri), }; public getRendererComm(rendererId: string): vscode.NotebookCommunication { const emitter = new Emitter(); this._rendererIdToEmitters.set(rendererId, emitter); return { editorId: this._editorId, onDidReceiveMessage: emitter.event, postMessage: (message: any) => this._proxy.$postMessage(this._editorId, rendererId, message), asWebviewUri: (uri: vscode.Uri) => this._asWebviewUri(uri), }; } private _asWebviewUri(localResource: vscode.Uri): vscode.Uri { return asWebviewUri(this._webviewInitData, this._editorId, localResource); } } export class ExtHostNotebookEditor extends Disposable implements vscode.NotebookEditor { private _viewColumn: vscode.ViewColumn | undefined; selection?: ExtHostCell = undefined; private _active: boolean = false; get active(): boolean { return this._active; } set active(_state: boolean) { throw readonly('active'); } private _visible: boolean = false; get visible(): boolean { return this._visible; } set visible(_state: boolean) { throw readonly('visible'); } _acceptVisibility(value: boolean) { this._visible = value; } _acceptActive(value: boolean) { this._active = value; } private _kernel?: vscode.NotebookKernel; get kernel() { return this._kernel; } set kernel(_kernel: vscode.NotebookKernel | undefined) { throw readonly('kernel'); } private _onDidDispose = new Emitter(); readonly onDidDispose: Event = this._onDidDispose.event; private _onDidReceiveMessage = new Emitter(); onDidReceiveMessage: vscode.Event = this._onDidReceiveMessage.event; constructor( private readonly viewType: string, readonly id: string, public uri: URI, private _proxy: MainThreadNotebookShape, private _webComm: vscode.NotebookCommunication, public document: ExtHostNotebookDocument, ) { super(); this._register(this._webComm.onDidReceiveMessage(e => { this._onDidReceiveMessage.fire(e); })); } edit(callback: (editBuilder: NotebookEditorCellEditBuilder) => void): Thenable { const edit = new NotebookEditorCellEditBuilder(this); callback(edit); return this._applyEdit(edit); } private _applyEdit(editBuilder: NotebookEditorCellEditBuilder): Promise { const editData = editBuilder.finalize(); // return when there is nothing to do if (editData.edits.length === 0) { return Promise.resolve(true); } const compressedEdits: ICellEditOperation[] = []; let compressedEditsIndex = -1; for (let i = 0; i < editData.edits.length; i++) { if (compressedEditsIndex < 0) { compressedEdits.push(editData.edits[i]); compressedEditsIndex++; continue; } const prevIndex = compressedEditsIndex; const prev = compressedEdits[prevIndex]; if (prev.editType === CellEditType.Insert && editData.edits[i].editType === CellEditType.Insert) { if (prev.index === editData.edits[i].index) { prev.cells.push(...(editData.edits[i] as ICellInsertEdit).cells); continue; } } if (prev.editType === CellEditType.Delete && editData.edits[i].editType === CellEditType.Delete) { if (prev.index === editData.edits[i].index) { prev.count += (editData.edits[i] as ICellDeleteEdit).count; continue; } } compressedEdits.push(editData.edits[i]); compressedEditsIndex++; } return this._proxy.$tryApplyEdits(this.viewType, this.uri, editData.documentVersionId, compressedEdits, editData.renderers); } get viewColumn(): vscode.ViewColumn | undefined { return this._viewColumn; } set viewColumn(value) { throw readonly('viewColumn'); } updateActiveKernel(kernel?: vscode.NotebookKernel) { this._kernel = kernel; } async postMessage(message: any): Promise { return this._webComm.postMessage(message); } asWebviewUri(localResource: vscode.Uri): vscode.Uri { return this._webComm.asWebviewUri(localResource); } dispose() { this._onDidDispose.fire(); super.dispose(); } } export class ExtHostNotebookOutputRenderer { private static _handlePool: number = 0; private resolvedComms = new WeakSet(); readonly handle = ExtHostNotebookOutputRenderer._handlePool++; constructor( public type: string, public filter: vscode.NotebookOutputSelector, public renderer: vscode.NotebookOutputRenderer ) { } matches(mimeType: string): boolean { if (this.filter.mimeTypes) { if (this.filter.mimeTypes.indexOf(mimeType) >= 0) { return true; } } return false; } resolveNotebook(document: ExtHostNotebookDocument, comm: ExtHostWebviewCommWrapper) { if (!this.resolvedComms.has(comm) && this.renderer.resolveNotebook) { this.renderer.resolveNotebook(document, comm.getRendererComm(this.type)); this.resolvedComms.add(comm); } } render(document: ExtHostNotebookDocument, output: vscode.CellDisplayOutput, outputId: string, mimeType: string): string { const html = this.renderer.render(document, { output, outputId, mimeType }); return html; } } export interface ExtHostNotebookOutputRenderingHandler { outputDisplayOrder: INotebookDisplayOrder | undefined; findBestMatchedRenderer(mimeType: string): ExtHostNotebookOutputRenderer[]; } export class ExtHostNotebookKernelProviderAdapter extends Disposable { private _kernelToId = new Map(); private _idToKernel = new Map(); constructor( private readonly _proxy: MainThreadNotebookShape, private readonly _handle: number, private readonly _extension: IExtensionDescription, private readonly _provider: vscode.NotebookKernelProvider ) { super(); if (this._provider.onDidChangeKernels) { this._register(this._provider.onDidChangeKernels(() => { this._proxy.$onNotebookKernelChange(this._handle); })); } } async provideKernels(document: ExtHostNotebookDocument, token: vscode.CancellationToken): Promise { const data = await this._provider.provideKernels(document, token) || []; const newMap = new Map(); let kernel_unique_pool = 0; const kernelIdCache = new Set(); const transformedData: INotebookKernelInfoDto2[] = data.map(kernel => { let id = this._kernelToId.get(kernel); if (id === undefined) { if (kernel.id && kernelIdCache.has(kernel.id)) { id = `${this._extension.identifier.value}_${kernel.id}_${kernel_unique_pool++}`; } else { id = `${this._extension.identifier.value}_${kernel.id || UUID.generateUuid()}`; } this._kernelToId.set(kernel, id); } newMap.set(kernel, id); return { id, label: kernel.label, extension: this._extension.identifier, extensionLocation: this._extension.extensionLocation, description: kernel.description, isPreferred: kernel.isPreferred, preloads: kernel.preloads }; }); this._kernelToId = newMap; this._idToKernel.clear(); this._kernelToId.forEach((value, key) => { this._idToKernel.set(value, key); }); return transformedData; } getKernel(kernelId: string) { return this._idToKernel.get(kernelId); } async resolveNotebook(kernelId: string, document: ExtHostNotebookDocument, webview: vscode.NotebookCommunication, token: CancellationToken) { const kernel = this._idToKernel.get(kernelId); if (kernel && this._provider.resolveKernel) { return this._provider.resolveKernel(kernel, document, webview, token); } } async executeNotebook(kernelId: string, document: ExtHostNotebookDocument, cell: ExtHostCell | undefined) { const kernel = this._idToKernel.get(kernelId); if (!kernel) { return; } if (cell) { return withToken(token => (kernel.executeCell as any)(document, cell, token)); } else { return withToken(token => (kernel.executeAllCells as any)(document, token)); } } async cancelNotebook(kernelId: string, document: ExtHostNotebookDocument, cell: ExtHostCell | undefined) { const kernel = this._idToKernel.get(kernelId); if (!kernel) { return; } if (cell) { return kernel.cancelCellExecution(document, cell); } else { return kernel.cancelAllCellsExecution(document); } } } // TODO@roblou remove 'token' passed to all execute APIs once extensions are updated async function withToken(cb: (token: CancellationToken) => any) { const source = new CancellationTokenSource(); try { await cb(source.token); } finally { source.dispose(); } } export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostNotebookOutputRenderingHandler { private static _notebookKernelProviderHandlePool: number = 0; private readonly _proxy: MainThreadNotebookShape; private readonly _notebookContentProviders = new Map(); private readonly _notebookKernels = new Map(); private readonly _notebookKernelProviders = new Map(); private readonly _documents = new ResourceMap(); private readonly _unInitializedDocuments = new ResourceMap(); private readonly _editors = new Map(); private readonly _webviewComm = new Map(); private readonly _notebookOutputRenderers = new Map(); private readonly _renderersUsedInNotebooks = new WeakMap>(); private readonly _onDidChangeNotebookCells = new Emitter(); readonly onDidChangeNotebookCells = this._onDidChangeNotebookCells.event; private readonly _onDidChangeCellOutputs = new Emitter(); readonly onDidChangeCellOutputs = this._onDidChangeCellOutputs.event; private readonly _onDidChangeCellLanguage = new Emitter(); readonly onDidChangeCellLanguage = this._onDidChangeCellLanguage.event; private readonly _onDidChangeCellMetadata = new Emitter(); readonly onDidChangeCellMetadata = this._onDidChangeCellMetadata.event; private readonly _onDidChangeActiveNotebookEditor = new Emitter(); readonly onDidChangeActiveNotebookEditor = this._onDidChangeActiveNotebookEditor.event; private _outputDisplayOrder: INotebookDisplayOrder | undefined; get outputDisplayOrder(): INotebookDisplayOrder | undefined { return this._outputDisplayOrder; } private _activeNotebookEditor: ExtHostNotebookEditor | undefined; get activeNotebookEditor() { return this._activeNotebookEditor; } get notebookDocuments() { return [...this._documents.values()]; } private _onDidOpenNotebookDocument = new Emitter(); onDidOpenNotebookDocument: Event = this._onDidOpenNotebookDocument.event; private _onDidCloseNotebookDocument = new Emitter(); onDidCloseNotebookDocument: Event = this._onDidCloseNotebookDocument.event; private _onDidSaveNotebookDocument = new Emitter(); onDidSaveNotebookDocument: Event = this._onDidCloseNotebookDocument.event; visibleNotebookEditors: ExtHostNotebookEditor[] = []; private _onDidChangeActiveNotebookKernel = new Emitter<{ document: ExtHostNotebookDocument, kernel: vscode.NotebookKernel | undefined; }>(); onDidChangeActiveNotebookKernel = this._onDidChangeActiveNotebookKernel.event; private _onDidChangeVisibleNotebookEditors = new Emitter(); onDidChangeVisibleNotebookEditors = this._onDidChangeVisibleNotebookEditors.event; constructor( mainContext: IMainContext, commands: ExtHostCommands, private _documentsAndEditors: ExtHostDocumentsAndEditors, private readonly _webviewInitData: WebviewInitData, private readonly logService: ILogService, private readonly _extensionStoragePaths?: IExtensionStoragePaths, ) { this._proxy = mainContext.getProxy(MainContext.MainThreadNotebook); commands.registerArgumentProcessor({ processArgument: arg => { if (arg && arg.$mid === 12) { const documentHandle = arg.notebookEditor?.notebookHandle; const cellHandle = arg.cell.handle; for (const value of this._editors) { if (value[1].editor.document.handle === documentHandle) { const cell = value[1].editor.document.getCell(cellHandle); if (cell) { return cell; } } } } return arg; } }); } registerNotebookOutputRenderer( type: string, extension: IExtensionDescription, filter: vscode.NotebookOutputSelector, renderer: vscode.NotebookOutputRenderer ): vscode.Disposable { if (this._notebookOutputRenderers.has(type)) { throw new Error(`Notebook renderer for '${type}' already registered`); } const extHostRenderer = new ExtHostNotebookOutputRenderer(type, filter, renderer); this._notebookOutputRenderers.set(extHostRenderer.type, extHostRenderer); this._proxy.$registerNotebookRenderer({ id: extension.identifier, location: extension.extensionLocation, description: extension.description }, type, filter, renderer.preloads || []); return new extHostTypes.Disposable(() => { this._notebookOutputRenderers.delete(extHostRenderer.type); this._proxy.$unregisterNotebookRenderer(extHostRenderer.type); }); } async $renderOutputs(uriComponents: UriComponents, id: string, request: IOutputRenderRequest): Promise | undefined> { if (!this._notebookOutputRenderers.has(id)) { throw new Error(`Notebook renderer for '${id}' is not registered`); } const document = this._documents.get(URI.revive(uriComponents)); if (!document) { return; } const renderer = this._notebookOutputRenderers.get(id)!; this.provideCommToNotebookRenderers(document, renderer); const cellsResponse: IOutputRenderResponseCellInfo[] = request.items.map(cellInfo => { const cell = document.getCell2(cellInfo.key)!; const outputResponse: IOutputRenderResponseOutputInfo[] = cellInfo.outputs.map(output => { return { index: output.index, outputId: output.outputId, mimeType: output.mimeType, handlerId: id, transformedOutput: renderer.render(document, cell.outputs[output.index] as vscode.CellDisplayOutput, output.outputId, output.mimeType) }; }); return { key: cellInfo.key, outputs: outputResponse }; }); return { items: cellsResponse }; } /** * The request carry the raw data for outputs so we don't look up in the existing document */ async $renderOutputs2(uriComponents: UriComponents, id: string, request: IOutputRenderRequest): Promise | undefined> { if (!this._notebookOutputRenderers.has(id)) { throw new Error(`Notebook renderer for '${id}' is not registered`); } const document = this._documents.get(URI.revive(uriComponents)); if (!document) { return; } const renderer = this._notebookOutputRenderers.get(id)!; this.provideCommToNotebookRenderers(document, renderer); const cellsResponse: IOutputRenderResponseCellInfo[] = request.items.map(cellInfo => { const outputResponse: IOutputRenderResponseOutputInfo[] = cellInfo.outputs.map(output => { return { index: output.index, outputId: output.outputId, mimeType: output.mimeType, handlerId: id, transformedOutput: renderer.render(document, output.output! as vscode.CellDisplayOutput, output.outputId, output.mimeType) }; }); return { key: cellInfo.key, outputs: outputResponse }; }); return { items: cellsResponse }; } findBestMatchedRenderer(mimeType: string): ExtHostNotebookOutputRenderer[] { const matches: ExtHostNotebookOutputRenderer[] = []; for (const renderer of this._notebookOutputRenderers) { if (renderer[1].matches(mimeType)) { matches.push(renderer[1]); } } return matches; } registerNotebookContentProvider( extension: IExtensionDescription, viewType: string, provider: vscode.NotebookContentProvider, ): vscode.Disposable { if (this._notebookContentProviders.has(viewType)) { throw new Error(`Notebook provider for '${viewType}' already registered`); } // if ((provider).executeCell) { // throw new Error('NotebookContentKernel.executeCell is removed, please use vscode.notebook.registerNotebookKernel instead.'); // } this._notebookContentProviders.set(viewType, { extension, provider }); const listener = provider.onDidChangeNotebook ? provider.onDidChangeNotebook(e => { const document = this._documents.get(URI.revive(e.document.uri)); if (!document) { throw new Error(`Notebook document ${e.document.uri.toString()} not found`); } if (isEditEvent(e)) { const editId = document.addEdit(e); this._proxy.$onDidEdit(e.document.uri, viewType, editId, e.label); } else { this._proxy.$onContentChange(e.document.uri, viewType); } }) : Disposable.None; const supportBackup = !!provider.backupNotebook; this._proxy.$registerNotebookProvider({ id: extension.identifier, location: extension.extensionLocation, description: extension.description }, viewType, supportBackup, provider.kernel ? { id: viewType, label: provider.kernel.label, extensionLocation: extension.extensionLocation, preloads: provider.kernel.preloads } : undefined); return new extHostTypes.Disposable(() => { listener.dispose(); this._notebookContentProviders.delete(viewType); this._proxy.$unregisterNotebookProvider(viewType); }); } registerNotebookKernelProvider(extension: IExtensionDescription, selector: vscode.NotebookDocumentFilter, provider: vscode.NotebookKernelProvider) { const handle = ExtHostNotebookController._notebookKernelProviderHandlePool++; const adapter = new ExtHostNotebookKernelProviderAdapter(this._proxy, handle, extension, provider); this._notebookKernelProviders.set(handle, adapter); this._proxy.$registerNotebookKernelProvider({ id: extension.identifier, location: extension.extensionLocation, description: extension.description }, handle, { viewType: selector.viewType, filenamePattern: selector.filenamePattern ? typeConverters.GlobPattern.from(selector.filenamePattern) : undefined, excludeFileNamePattern: selector.excludeFileNamePattern ? typeConverters.GlobPattern.from(selector.excludeFileNamePattern) : undefined, }); return new extHostTypes.Disposable(() => { adapter.dispose(); this._notebookKernelProviders.delete(handle); this._proxy.$unregisterNotebookKernelProvider(handle); }); } private _withAdapter(handle: number, uri: UriComponents, callback: (adapter: ExtHostNotebookKernelProviderAdapter, document: ExtHostNotebookDocument) => Promise) { const document = this._documents.get(URI.revive(uri)); if (!document) { return []; } const provider = this._notebookKernelProviders.get(handle); if (!provider) { return []; } return callback(provider, document); } async $provideNotebookKernels(handle: number, uri: UriComponents, token: CancellationToken): Promise { return this._withAdapter(handle, uri, (adapter, document) => { return adapter.provideKernels(document, token); }); } async $resolveNotebookKernel(handle: number, editorId: string, uri: UriComponents, kernelId: string, token: CancellationToken): Promise { await this._withAdapter(handle, uri, async (adapter, document) => { const webComm = this._webviewComm.get(editorId); if (webComm) { await adapter.resolveNotebook(kernelId, document, webComm.contentProviderComm, token); } }); } registerNotebookKernel(extension: IExtensionDescription, id: string, selectors: vscode.GlobPattern[], kernel: vscode.NotebookKernel): vscode.Disposable { if (this._notebookKernels.has(id)) { throw new Error(`Notebook kernel for '${id}' already registered`); } this._notebookKernels.set(id, { kernel, extension }); const transformedSelectors = selectors.map(selector => typeConverters.GlobPattern.from(selector)); this._proxy.$registerNotebookKernel({ id: extension.identifier, location: extension.extensionLocation, description: extension.description }, id, kernel.label, transformedSelectors, kernel.preloads || []); return new extHostTypes.Disposable(() => { this._notebookKernels.delete(id); this._proxy.$unregisterNotebookKernel(id); }); } async $resolveNotebookData(viewType: string, uri: UriComponents, backupId?: string): Promise { const provider = this._notebookContentProviders.get(viewType); const revivedUri = URI.revive(uri); if (!provider) { return; } let storageRoot: URI | undefined; if (this._extensionStoragePaths) { storageRoot = this._extensionStoragePaths.workspaceValue(provider.extension) ?? this._extensionStoragePaths.globalValue(provider.extension); } let document = this._documents.get(revivedUri); if (!document) { const that = this; document = this._unInitializedDocuments.get(revivedUri) ?? new ExtHostNotebookDocument(this._proxy, this._documentsAndEditors, { emitModelChange(event: vscode.NotebookCellsChangeEvent): void { that._onDidChangeNotebookCells.fire(event); }, emitCellOutputsChange(event: vscode.NotebookCellOutputsChangeEvent): void { that._onDidChangeCellOutputs.fire(event); }, emitCellLanguageChange(event: vscode.NotebookCellLanguageChangeEvent): void { that._onDidChangeCellLanguage.fire(event); }, emitCellMetadataChange(event: vscode.NotebookCellMetadataChangeEvent): void { that._onDidChangeCellMetadata.fire(event); }, }, viewType, revivedUri, this, storageRoot); this._unInitializedDocuments.set(revivedUri, document); } const rawCells = await provider.provider.openNotebook(URI.revive(uri), { backupId }); const dto = { metadata: { ...notebookDocumentMetadataDefaults, ...rawCells.metadata }, languages: rawCells.languages, cells: rawCells.cells.map(cell => ({ ...cell, outputs: cell.outputs.map(o => addIdToOutput(o)) })), }; return dto; } async $resolveNotebookEditor(viewType: string, uri: UriComponents, editorId: string): Promise { const provider = this._notebookContentProviders.get(viewType); const revivedUri = URI.revive(uri); const document = this._documents.get(revivedUri); if (!document || !provider) { return; } let webComm = this._webviewComm.get(editorId); if (!webComm) { webComm = new ExtHostWebviewCommWrapper(editorId, revivedUri, this._proxy, this._webviewInitData, document); this._webviewComm.set(editorId, webComm); } if (!provider.provider.resolveNotebook) { return; } await provider.provider.resolveNotebook(document, webComm.contentProviderComm); } private provideCommToNotebookRenderers(document: ExtHostNotebookDocument, renderer: ExtHostNotebookOutputRenderer) { let alreadyRegistered = this._renderersUsedInNotebooks.get(document); if (!alreadyRegistered) { alreadyRegistered = new Set(); this._renderersUsedInNotebooks.set(document, alreadyRegistered); } if (alreadyRegistered.has(renderer)) { return; } alreadyRegistered.add(renderer); for (const editorId of this._editors.keys()) { const comm = this._webviewComm.get(editorId); if (comm) { renderer.resolveNotebook(document, comm); } } } async $executeNotebookByAttachedKernel(viewType: string, uri: UriComponents, cellHandle: number | undefined): Promise { const document = this._documents.get(URI.revive(uri)); if (!document) { return; } if (this._notebookContentProviders.has(viewType)) { const cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined; const provider = this._notebookContentProviders.get(viewType)!.provider; if (provider.kernel) { if (cell) { return withToken(token => (provider.kernel!.executeCell as any)(document, cell, token)); } else { return withToken(token => (provider.kernel!.executeAllCells as any)(document, token)); } } } } async $cancelNotebookByAttachedKernel(viewType: string, uri: UriComponents, cellHandle: number | undefined): Promise { const document = this._documents.get(URI.revive(uri)); if (!document) { return; } if (this._notebookContentProviders.has(viewType)) { const cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined; const provider = this._notebookContentProviders.get(viewType)!.provider; if (provider.kernel) { if (cell) { return provider.kernel.cancelCellExecution(document, cell); } else { return provider.kernel.cancelAllCellsExecution(document); } } } } async $executeNotebookKernelFromProvider(handle: number, uri: UriComponents, kernelId: string, cellHandle: number | undefined): Promise { await this._withAdapter(handle, uri, async (adapter, document) => { const cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined; return adapter.executeNotebook(kernelId, document, cell); }); } async $cancelNotebookKernelFromProvider(handle: number, uri: UriComponents, kernelId: string, cellHandle: number | undefined): Promise { await this._withAdapter(handle, uri, async (adapter, document) => { const cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined; return adapter.cancelNotebook(kernelId, document, cell); }); } async $executeNotebook2(kernelId: string, viewType: string, uri: UriComponents, cellHandle: number | undefined): Promise { const document = this._documents.get(URI.revive(uri)); if (!document || document.viewType !== viewType) { return; } const kernelInfo = this._notebookKernels.get(kernelId); if (!kernelInfo) { return; } const cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined; if (cell) { return withToken(token => (kernelInfo!.kernel.executeCell as any)(document, cell, token)); } else { return withToken(token => (kernelInfo!.kernel.executeAllCells as any)(document, token)); } } async $saveNotebook(viewType: string, uri: UriComponents, token: CancellationToken): Promise { const document = this._documents.get(URI.revive(uri)); if (!document) { return false; } if (this._notebookContentProviders.has(viewType)) { await this._notebookContentProviders.get(viewType)!.provider.saveNotebook(document, token); return true; } return false; } async $saveNotebookAs(viewType: string, uri: UriComponents, target: UriComponents, token: CancellationToken): Promise { const document = this._documents.get(URI.revive(uri)); if (!document) { return false; } if (this._notebookContentProviders.has(viewType)) { await this._notebookContentProviders.get(viewType)!.provider.saveNotebookAs(URI.revive(target), document, token); return true; } return false; } async $undoNotebook(viewType: string, uri: UriComponents, editId: number, isDirty: boolean): Promise { const document = this._documents.get(URI.revive(uri)); if (!document) { return; } document.undo(editId, isDirty); } async $redoNotebook(viewType: string, uri: UriComponents, editId: number, isDirty: boolean): Promise { const document = this._documents.get(URI.revive(uri)); if (!document) { return; } document.redo(editId, isDirty); } async $backup(viewType: string, uri: UriComponents, cancellation: CancellationToken): Promise { const document = this._documents.get(URI.revive(uri)); const provider = this._notebookContentProviders.get(viewType); if (document && provider && provider.provider.backupNotebook) { const backup = await provider.provider.backupNotebook(document, { destination: document.getNewBackupUri() }, cancellation); document.updateBackup(backup); return backup.id; } return; } $acceptDisplayOrder(displayOrder: INotebookDisplayOrder): void { this._outputDisplayOrder = displayOrder; } $acceptNotebookActiveKernelChange(event: { uri: UriComponents, providerHandle: number | undefined, kernelId: string | undefined; }) { if (event.providerHandle !== undefined) { this._withAdapter(event.providerHandle, event.uri, async (adapter, document) => { const kernel = event.kernelId ? adapter.getKernel(event.kernelId) : undefined; this._editors.forEach(editor => { if (editor.editor.document === document) { editor.editor.updateActiveKernel(kernel); } }); this._onDidChangeActiveNotebookKernel.fire({ document, kernel }); }); } } // TODO: remove document - editor one on one mapping private _getEditorFromURI(uriComponents: UriComponents) { const uriStr = URI.revive(uriComponents).toString(); let editor: { editor: ExtHostNotebookEditor; } | undefined; this._editors.forEach(e => { if (e.editor.uri.toString() === uriStr) { editor = e; } }); return editor; } $onDidReceiveMessage(editorId: string, forRendererType: string | undefined, message: any): void { this._webviewComm.get(editorId)?.onDidReceiveMessage(forRendererType, message); } $acceptModelChanged(uriComponents: UriComponents, event: NotebookCellsChangedEvent): void { const document = this._documents.get(URI.revive(uriComponents)); if (document) { document.acceptModelChanged(event); } } public $acceptModelSaved(uriComponents: UriComponents): void { const document = this._documents.get(URI.revive(uriComponents)); if (document) { // this.$acceptDirtyStateChanged(uriComponents, false); this._onDidSaveNotebookDocument.fire(document); } } $acceptEditorPropertiesChanged(uriComponents: UriComponents, data: INotebookEditorPropertiesChangeData): void { this.logService.debug('ExtHostNotebook#$acceptEditorPropertiesChanged', uriComponents.path, data); const editor = this._getEditorFromURI(uriComponents); if (!editor) { return; } if (data.selections) { const cells = editor.editor.document.cells; if (data.selections.selections.length) { const firstCell = data.selections.selections[0]; editor.editor.selection = cells.find(cell => cell.handle === firstCell); } else { editor.editor.selection = undefined; } } if (data.metadata) { editor.editor.document.metadata = { ...notebookDocumentMetadataDefaults, ...data.metadata }; } } private _createExtHostEditor(document: ExtHostNotebookDocument, editorId: string, selections: number[]) { const revivedUri = document.uri; let webComm = this._webviewComm.get(editorId); if (!webComm) { webComm = new ExtHostWebviewCommWrapper(editorId, revivedUri, this._proxy, this._webviewInitData, document); this._webviewComm.set(editorId, webComm); } const editor = new ExtHostNotebookEditor( document.viewType, editorId, revivedUri, this._proxy, webComm.contentProviderComm, document ); const cells = editor.document.cells; if (selections.length) { const firstCell = selections[0]; editor.selection = cells.find(cell => cell.handle === firstCell); } else { editor.selection = undefined; } this._editors.get(editorId)?.editor.dispose(); for (const renderer of this._renderersUsedInNotebooks.get(document) ?? []) { renderer.resolveNotebook(document, webComm); } this._editors.set(editorId, { editor }); } $acceptDocumentAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta): void { let editorChanged = false; if (delta.removedDocuments) { for (const uri of delta.removedDocuments) { const revivedUri = URI.revive(uri); const document = this._documents.get(revivedUri); if (document) { document.dispose(); this._documents.delete(revivedUri); this._documentsAndEditors.$acceptDocumentsAndEditorsDelta({ removedDocuments: document.cells.map(cell => cell.uri) }); this._onDidCloseNotebookDocument.fire(document); } for (const e of this._editors.values()) { if (e.editor.uri.toString() === revivedUri.toString()) { e.editor.dispose(); this._editors.delete(e.editor.id); editorChanged = true; } } } } if (delta.addedDocuments) { const addedCellDocuments: IModelAddedData[] = []; for (const modelData of delta.addedDocuments) { const revivedUri = URI.revive(modelData.uri); const viewType = modelData.viewType; const entry = this._notebookContentProviders.get(viewType); let storageRoot: URI | undefined; if (entry && this._extensionStoragePaths) { storageRoot = this._extensionStoragePaths.workspaceValue(entry.extension) ?? this._extensionStoragePaths.globalValue(entry.extension); } if (!this._documents.has(revivedUri)) { const that = this; const document = this._unInitializedDocuments.get(revivedUri) ?? new ExtHostNotebookDocument(this._proxy, this._documentsAndEditors, { emitModelChange(event: vscode.NotebookCellsChangeEvent): void { that._onDidChangeNotebookCells.fire(event); }, emitCellOutputsChange(event: vscode.NotebookCellOutputsChangeEvent): void { that._onDidChangeCellOutputs.fire(event); }, emitCellLanguageChange(event: vscode.NotebookCellLanguageChangeEvent): void { that._onDidChangeCellLanguage.fire(event); }, emitCellMetadataChange(event: vscode.NotebookCellMetadataChangeEvent): void { that._onDidChangeCellMetadata.fire(event); } }, viewType, revivedUri, this, storageRoot); this._unInitializedDocuments.delete(revivedUri); if (modelData.metadata) { document.metadata = { ...notebookDocumentMetadataDefaults, ...modelData.metadata }; } document.acceptModelChanged({ kind: NotebookCellsChangeType.Initialize, versionId: modelData.versionId, changes: [[ 0, 0, modelData.cells ]] }); // add cell document as vscode.TextDocument addedCellDocuments.push(...modelData.cells.map(cell => ExtHostCell.asModelAddData(document, cell))); this._documents.get(revivedUri)?.dispose(); this._documents.set(revivedUri, document); // create editor if populated if (modelData.attachedEditor) { this._createExtHostEditor(document, modelData.attachedEditor.id, modelData.attachedEditor.selections); editorChanged = true; } } this._documentsAndEditors.$acceptDocumentsAndEditorsDelta({ addedDocuments: addedCellDocuments }); const document = this._documents.get(revivedUri)!; this._onDidOpenNotebookDocument.fire(document); } } if (delta.addedEditors) { for (const editorModelData of delta.addedEditors) { if (this._editors.has(editorModelData.id)) { return; } const revivedUri = URI.revive(editorModelData.documentUri); const document = this._documents.get(revivedUri); if (document) { this._createExtHostEditor(document, editorModelData.id, editorModelData.selections); editorChanged = true; } } } const removedEditors: { editor: ExtHostNotebookEditor; }[] = []; if (delta.removedEditors) { for (const editorid of delta.removedEditors) { const editor = this._editors.get(editorid); if (editor) { editorChanged = true; this._editors.delete(editorid); if (this.activeNotebookEditor?.id === editor.editor.id) { this._activeNotebookEditor = undefined; } removedEditors.push(editor); } } } if (editorChanged) { removedEditors.forEach(e => { e.editor.dispose(); }); } if (delta.visibleEditors) { this.visibleNotebookEditors = delta.visibleEditors.map(id => this._editors.get(id)!.editor).filter(editor => !!editor) as ExtHostNotebookEditor[]; const visibleEditorsSet = new Set(); this.visibleNotebookEditors.forEach(editor => visibleEditorsSet.add(editor.id)); for (const e of this._editors.values()) { const newValue = visibleEditorsSet.has(e.editor.id); e.editor._acceptVisibility(newValue); } this.visibleNotebookEditors = [...this._editors.values()].map(e => e.editor).filter(e => e.visible); this._onDidChangeVisibleNotebookEditors.fire(this.visibleNotebookEditors); } if (delta.newActiveEditor !== undefined) { if (delta.newActiveEditor) { this._activeNotebookEditor = this._editors.get(delta.newActiveEditor)?.editor; this._activeNotebookEditor?._acceptActive(true); for (const e of this._editors.values()) { if (e.editor !== this.activeNotebookEditor) { e.editor._acceptActive(false); } } } else { // clear active notebook as current active editor is non-notebook editor this._activeNotebookEditor = undefined; for (const e of this._editors.values()) { e.editor._acceptActive(false); } } this._onDidChangeActiveNotebookEditor.fire(this._activeNotebookEditor); } } } 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'; }