diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 6da8d810814..ebaa7d6cf51 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1618,7 +1618,16 @@ declare module 'vscode' { notebookType: string, provider: NotebookContentProvider, options?: { - transientMetadata?: { [K in keyof NotebookCellMetadata]?: boolean } + /** + * Controls if outputs change will trigger notebook document content change and if it will be used in the diff editor + * Default to false. If the content provider doesn't persisit the outputs in the file document, this should be set to true. + */ + transientOutputs: boolean; + /** + * Controls if a meetadata property change will trigger notebook document content change and if it will be used in the diff editor + * Default to false. If the content provider doesn't persisit a metadata property in the file document, it should be set to true. + */ + transientMetadata: { [K in keyof NotebookCellMetadata]?: boolean } } ): Disposable; diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index 0abd71a96ad..a377146ad2b 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -375,7 +375,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo // } } - async $registerNotebookProvider(_extension: NotebookExtensionDescription, _viewType: string, _supportBackup: boolean, _kernel: INotebookKernelInfoDto | undefined, options: { transientMetadata: TransientMetadata }): Promise { + async $registerNotebookProvider(_extension: NotebookExtensionDescription, _viewType: string, _supportBackup: boolean, _kernel: INotebookKernelInfoDto | undefined, options: { transientOutputs: boolean; transientMetadata: TransientMetadata }): Promise { const controller: IMainNotebookController = { kernel: _kernel, supportBackup: _supportBackup, @@ -388,7 +388,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo mainthreadTextModel.languages = data.languages; mainthreadTextModel.metadata = data.metadata; - mainthreadTextModel.transientMetadata = options.transientMetadata; + mainthreadTextModel.transientOptions = options; const edits: ICellEditOperation[] = [ { editType: CellEditType.Delete, count: mainthreadTextModel.cells.length, index: 0 }, @@ -412,7 +412,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo textModel.languages = data.languages; textModel.metadata = data.metadata; - textModel.transientMetadata = options.transientMetadata; + textModel.transientOptions = options; if (data.cells.length) { textModel.initialize(data!.cells); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index f6810e625a4..ea6cfe93bfe 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -951,7 +951,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostNotebook.onDidChangeActiveNotebookKernel; }, registerNotebookContentProvider: (viewType: string, provider: vscode.NotebookContentProvider, options?: { - transientMetadata?: { [K in keyof vscode.NotebookCellMetadata]?: boolean } + transientOutputs: boolean; + transientMetadata: { [K in keyof vscode.NotebookCellMetadata]?: boolean } }) => { checkProposedApiEnabled(extension); return extHostNotebook.registerNotebookContentProvider(extension, viewType, provider, options); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 188c1a88dd8..13c722e1926 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -715,7 +715,7 @@ export type NotebookCellOutputsSplice = [ ]; export interface MainThreadNotebookShape extends IDisposable { - $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string, supportBackup: boolean, kernelInfoDto: INotebookKernelInfoDto | undefined, options: { transientMetadata: TransientMetadata }): Promise; + $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string, supportBackup: boolean, kernelInfoDto: INotebookKernelInfoDto | undefined, options: { transientOutputs: boolean; transientMetadata: TransientMetadata }): Promise; $onNotebookChange(viewType: string, resource: UriComponents): Promise; $unregisterNotebookProvider(viewType: string): Promise; $registerNotebookKernel(extension: NotebookExtensionDescription, id: string, label: string, selectors: (string | IRelativePattern)[], preloads: UriComponents[]): Promise; diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 9c639c41567..bd8fad9da0e 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -955,7 +955,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN viewType: string, provider: vscode.NotebookContentProvider & { kernel?: vscode.NotebookKernel }, options?: { - transientMetadata?: { [K in keyof NotebookCellMetadata]?: boolean } + transientOutputs: boolean; + transientMetadata: { [K in keyof NotebookCellMetadata]?: boolean }; } ): vscode.Disposable { @@ -988,7 +989,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN 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, { transientMetadata: options?.transientMetadata || {} }); + 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, { transientOutputs: options?.transientOutputs || false, transientMetadata: options?.transientMetadata || {} }); return new extHostTypes.Disposable(() => { listener.dispose(); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts index 4bc5911f51b..f339970b91e 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts @@ -261,7 +261,7 @@ abstract class AbstractCellRenderer extends Disposable { { updateInfoRendering: this.updateOutputRendering.bind(this), checkIfModified: (cell) => { - return cell.type === 'modified' && hash(cell.original?.outputs ?? []) !== hash(cell.modified?.outputs ?? []); + return !this.notebookEditor.textModel!.transientOptions.transientOutputs && cell.type === 'modified' && hash(cell.original?.outputs ?? []) !== hash(cell.modified?.outputs ?? []); }, getFoldingState: (cell) => { return this.cell.outputFoldingState; @@ -324,7 +324,7 @@ abstract class AbstractCellRenderer extends Disposable { protected _getFormatedMetadataJSON(metadata: NotebookCellMetadata, language?: string) { let filteredMetadata: { [key: string]: any } = {}; if (this.notebookEditor.textModel) { - const transientMetadata = this.notebookEditor.textModel!.transientMetadata; + const transientMetadata = this.notebookEditor.textModel!.transientOptions.transientMetadata; const keys = new Set([...Object.keys(metadata)]); for (let key of keys) { @@ -421,7 +421,7 @@ abstract class AbstractCellRenderer extends Disposable { } private _buildOutputEditor() { - if (this.cell.type === 'modified') { + if (this.cell.type === 'modified' && !this.notebookEditor.textModel!.transientOptions.transientOutputs) { const originalOutputsSource = this._getFormatedOutputJSON(this.cell.original?.outputs || []); const modifiedOutputsSource = this._getFormatedOutputJSON(this.cell.modified?.outputs || []); if (originalOutputsSource !== modifiedOutputsSource) { @@ -465,9 +465,11 @@ abstract class AbstractCellRenderer extends Disposable { const mode = this.modeService.create('json'); const originaloutputSource = this._getFormatedOutputJSON( - this.cell.type === 'insert' - ? this.cell.modified!.outputs || [] - : this.cell.original!.outputs || []); + this.notebookEditor.textModel!.transientOptions + ? [] + : this.cell.type === 'insert' + ? this.cell.modified!.outputs || [] + : this.cell.original!.outputs || []); const outputModel = this.modelService.createModel(originaloutputSource, mode, undefined, true); this._outputEditor.setModel(outputModel); diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts index 6d4b29511e2..c2b34adefcf 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { ICell, IProcessedOutput, NotebookCellOutputsSplice, CellKind, NotebookCellMetadata, NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ICell, IProcessedOutput, NotebookCellOutputsSplice, CellKind, NotebookCellMetadata, NotebookDocumentMetadata, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; import { URI } from 'vs/base/common/uri'; import * as model from 'vs/editor/common/model'; @@ -85,6 +85,7 @@ export class NotebookCellTextModel extends Disposable implements ICell { public cellKind: CellKind, outputs: IProcessedOutput[], metadata: NotebookCellMetadata | undefined, + public readonly transientOptions: TransientOptions, private readonly _modelService: ITextModelService ) { super(); @@ -108,11 +109,25 @@ export class NotebookCellTextModel extends Disposable implements ICell { } // TODO, raw outputs - this._hash = hash([hash(this.getValue()), this._metadata, this._outputs]); - // this._hash = hash(this.getValue()); + this._hash = hash([hash(this.getValue()), this._getPersisentMetadata, this.transientOptions.transientOutputs ? [] : this._outputs]); return this._hash; } + private _getPersisentMetadata() { + let filteredMetadata: { [key: string]: any } = {}; + const transientMetadata = this.transientOptions.transientMetadata; + + const keys = new Set([...Object.keys(this.metadata)]); + for (let key of keys) { + if (!(transientMetadata[key as keyof NotebookCellMetadata]) + ) { + filteredMetadata[key] = this.metadata[key as keyof NotebookCellMetadata]; + } + } + + return filteredMetadata; + } + getTextLength(): number { return this.textBuffer.getLength(); } diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index 3dc85af8047..b85e95045cb 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -8,7 +8,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { INotebookTextModel, NotebookCellOutputsSplice, NotebookCellTextModelSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, CellEditType, CellUri, NotebookCellsChangedEvent, CellKind, IProcessedOutput, notebookDocumentMetadataDefaults, diff, NotebookCellsChangeType, ICellDto2, IMainCellDto, TransientMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookTextModel, NotebookCellOutputsSplice, NotebookCellTextModelSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, CellEditType, CellUri, NotebookCellsChangedEvent, CellKind, IProcessedOutput, notebookDocumentMetadataDefaults, diff, NotebookCellsChangeType, ICellDto2, IMainCellDto, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ITextSnapshot } from 'vs/editor/common/model'; import { IUndoRedoService, UndoRedoElementType, IUndoRedoElement, IResourceUndoRedoElement } from 'vs/platform/undoRedo/common/undoRedo'; import { InsertCellEdit, DeleteCellEdit, MoveCellEdit, SpliceCellsEdit, CellMetadataEdit } from 'vs/workbench/contrib/notebook/common/model/cellEdit'; @@ -128,7 +128,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel cells: NotebookCellTextModel[]; languages: string[] = []; metadata: NotebookDocumentMetadata = notebookDocumentMetadataDefaults; - transientMetadata: TransientMetadata = {}; + transientOptions: TransientOptions = { transientMetadata: {}, transientOutputs: false }; private _isUntitled: boolean | undefined = undefined; private _versionId = 0; @@ -187,7 +187,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel ) { const cellHandle = this._cellhandlePool++; const cellUri = CellUri.generate(this.uri, cellHandle); - return new NotebookCellTextModel(cellUri, cellHandle, source, language, cellKind, outputs || [], metadata || {}, this._modelService); + return new NotebookCellTextModel(cellUri, cellHandle, source, language, cellKind, outputs || [], metadata || {}, this.transientOptions, this._modelService); } initialize(cells: ICellDto2[]) { @@ -197,7 +197,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel const mainCells = cells.map(cell => { const cellHandle = this._cellhandlePool++; const cellUri = CellUri.generate(this.uri, cellHandle); - return new NotebookCellTextModel(cellUri, cellHandle, cell.source, cell.language, cell.cellKind, cell.outputs || [], cell.metadata, this._modelService); + return new NotebookCellTextModel(cellUri, cellHandle, cell.source, cell.language, cell.cellKind, cell.outputs || [], cell.metadata, this.transientOptions, this._modelService); }); this._isUntitled = false; @@ -245,7 +245,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel const mainCells = edit.cells.map(cell => { const cellHandle = this._cellhandlePool++; const cellUri = CellUri.generate(this.uri, cellHandle); - return new NotebookCellTextModel(cellUri, cellHandle, cell.source, cell.language, cell.cellKind, cell.outputs || [], cell.metadata, this._modelService); + return new NotebookCellTextModel(cellUri, cellHandle, cell.source, cell.language, cell.cellKind, cell.outputs || [], cell.metadata, this.transientOptions, this._modelService); }); this.insertNewCell(edit.index, mainCells, false); break; @@ -473,6 +473,12 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel spliceNotebookCellOutputs(cellHandle: number, splices: NotebookCellOutputsSplice[]): void { const cell = this._mapping.get(cellHandle); cell?.spliceNotebookCellOutputs(splices); + + if (!this.transientOptions.transientOutputs) { + this._increaseVersionId(); + this.setDirty(true); + this._onDidChangeContent.fire(); + } } clearCellOutput(handle: number) { @@ -497,13 +503,13 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel } } - private _compareCellMetadata(a: NotebookCellMetadata, b: NotebookCellMetadata) { + private _isCellMetadataChanged(a: NotebookCellMetadata, b: NotebookCellMetadata) { const keys = new Set([...Object.keys(a || {}), ...Object.keys(b || {})]); for (let key of keys) { if ( (a[key as keyof NotebookCellMetadata] !== b[key as keyof NotebookCellMetadata]) && - !(this.transientMetadata[key as keyof NotebookCellMetadata]) + !(this.transientOptions.transientMetadata[key as keyof NotebookCellMetadata]) ) { return true; } @@ -519,7 +525,11 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return; } - const triggerDirtyChange = this._compareCellMetadata(cell.metadata, metadata); + if (!this._isCellMetadataChanged(cell.metadata, metadata)) { + return; + } + + const triggerDirtyChange = this._isCellMetadataChanged(cell.metadata, metadata); if (triggerDirtyChange) { if (pushUndoStop) { diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index b634b6bd63e..2505a5c855d 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -105,6 +105,11 @@ export interface NotebookCellMetadata { export type TransientMetadata = { [K in keyof NotebookCellMetadata]?: boolean }; +export interface TransientOptions { + transientOutputs: boolean; + transientMetadata: TransientMetadata; +} + export interface INotebookDisplayOrder { defaultOrder: string[]; userOrder?: string[]; diff --git a/src/vs/workbench/contrib/notebook/common/notebookService.ts b/src/vs/workbench/contrib/notebook/common/notebookService.ts index 89e0161170e..9e7a32ec7a1 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookService.ts @@ -24,7 +24,7 @@ export const INotebookService = createDecorator('notebookServi export interface IMainNotebookController { kernel: INotebookKernelInfoDto | undefined; supportBackup: boolean; - options: { transientMetadata: TransientMetadata }; + options: { transientOutputs: boolean; transientMetadata: TransientMetadata; }; createNotebook(textModel: NotebookTextModel, editorId?: string, backupId?: string): Promise; reloadNotebook(mainthreadTextModel: NotebookTextModel): Promise; resolveNotebookEditor(viewType: string, uri: URI, editorId: string): Promise; diff --git a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts index 7f69f420dd2..7953687f0ce 100644 --- a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts @@ -45,7 +45,7 @@ export class TestCell extends NotebookCellTextModel { outputs: IProcessedOutput[], modelService: ITextModelService ) { - super(CellUri.generate(URI.parse('test:///fake/notebook'), handle), handle, source, language, cellKind, outputs, undefined, modelService); + super(CellUri.generate(URI.parse('test:///fake/notebook'), handle), handle, source, language, cellKind, outputs, undefined, { transientMetadata: {}, transientOutputs: false }, modelService); } }