diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index a1acb30628d..0abd71a96ad 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -9,7 +9,7 @@ import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IEx import { Disposable, IDisposable, combinedDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; import { INotebookService, IMainNotebookController } from 'vs/workbench/contrib/notebook/common/notebookService'; -import { NOTEBOOK_DISPLAY_ORDER, NotebookCellOutputsSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, CellEditType, CellKind, INotebookKernelInfo, INotebookKernelInfoDto, IEditor, INotebookDocumentFilter, DisplayOrderKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NOTEBOOK_DISPLAY_ORDER, NotebookCellOutputsSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, CellEditType, CellKind, INotebookKernelInfo, INotebookKernelInfoDto, IEditor, INotebookDocumentFilter, DisplayOrderKey, TransientMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -375,10 +375,11 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo // } } - async $registerNotebookProvider(_extension: NotebookExtensionDescription, _viewType: string, _supportBackup: boolean, _kernel: INotebookKernelInfoDto | undefined): Promise { + async $registerNotebookProvider(_extension: NotebookExtensionDescription, _viewType: string, _supportBackup: boolean, _kernel: INotebookKernelInfoDto | undefined, options: { transientMetadata: TransientMetadata }): Promise { const controller: IMainNotebookController = { kernel: _kernel, supportBackup: _supportBackup, + options: options, reloadNotebook: async (mainthreadTextModel: NotebookTextModel) => { const data = await this._proxy.$resolveNotebookData(_viewType, mainthreadTextModel.uri); if (!data) { @@ -387,6 +388,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo mainthreadTextModel.languages = data.languages; mainthreadTextModel.metadata = data.metadata; + mainthreadTextModel.transientMetadata = options.transientMetadata; const edits: ICellEditOperation[] = [ { editType: CellEditType.Delete, count: mainthreadTextModel.cells.length, index: 0 }, @@ -410,6 +412,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo textModel.languages = data.languages; textModel.metadata = data.metadata; + textModel.transientMetadata = options.transientMetadata; if (data.cells.length) { textModel.initialize(data!.cells); @@ -551,7 +554,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo async $updateNotebookCellMetadata(viewType: string, resource: UriComponents, handle: number, metadata: NotebookCellMetadata): Promise { this.logService.debug('MainThreadNotebooks#updateNotebookCellMetadata', resource.path, handle, metadata); const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); - textModel?.updateNotebookCellMetadata(handle, metadata); + textModel?.changeCellMetadata(handle, metadata, true); } async $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[]): Promise { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index cbf9308517b..f6810e625a4 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -950,9 +950,11 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostNotebook.onDidChangeActiveNotebookKernel; }, - registerNotebookContentProvider: (viewType: string, provider: vscode.NotebookContentProvider) => { + registerNotebookContentProvider: (viewType: string, provider: vscode.NotebookContentProvider, options?: { + transientMetadata?: { [K in keyof vscode.NotebookCellMetadata]?: boolean } + }) => { checkProposedApiEnabled(extension); - return extHostNotebook.registerNotebookContentProvider(extension, viewType, provider); + return extHostNotebook.registerNotebookContentProvider(extension, viewType, provider, options); }, registerNotebookKernel: (id: string, selector: vscode.GlobPattern[], kernel: vscode.NotebookKernel) => { checkProposedApiEnabled(extension); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index e0415cf4584..188c1a88dd8 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -51,7 +51,7 @@ import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; import { TunnelOptions } from 'vs/platform/remote/common/tunnel'; import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, InternalTimelineOptions } from 'vs/workbench/contrib/timeline/common/timeline'; import { revive } from 'vs/base/common/marshalling'; -import { IProcessedOutput, INotebookDisplayOrder, NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEvent, NotebookDataDto, INotebookKernelInfoDto, IMainCellDto, INotebookDocumentFilter, INotebookKernelInfoDto2 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IProcessedOutput, INotebookDisplayOrder, NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEvent, NotebookDataDto, INotebookKernelInfoDto, IMainCellDto, INotebookDocumentFilter, INotebookKernelInfoDto2, TransientMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { Dto } from 'vs/base/common/types'; import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; @@ -715,7 +715,7 @@ export type NotebookCellOutputsSplice = [ ]; export interface MainThreadNotebookShape extends IDisposable { - $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string, supportBackup: boolean, kernelInfoDto: INotebookKernelInfoDto | undefined): Promise; + $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string, supportBackup: boolean, kernelInfoDto: INotebookKernelInfoDto | undefined, options: { 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 240d9a4fb6d..e6429ff35ae 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -954,6 +954,9 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN extension: IExtensionDescription, viewType: string, provider: vscode.NotebookContentProvider & { kernel?: vscode.NotebookKernel }, + options?: { + transientMetadata?: { [K in keyof NotebookCellMetadata]?: boolean } + } ): vscode.Disposable { if (this._notebookContentProviders.has(viewType)) { @@ -985,7 +988,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); + 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 || {} }); return new extHostTypes.Disposable(() => { listener.dispose(); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index 7c41f1bf8c3..55419ec854e 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -1292,7 +1292,7 @@ registerAction2(class extends NotebookCellAction { editor.viewModel.notebookDocument.clearCellOutput(context.cell.handle); if (context.cell.metadata && context.cell.metadata?.runState !== NotebookCellRunState.Running) { - context.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(context.cell.handle, { + context.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(context.cell.handle, { runState: NotebookCellRunState.Idle, runStartTime: undefined, lastRunDuration: undefined, @@ -1562,7 +1562,7 @@ registerAction2(class extends NotebookCellAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { - context.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(context.cell.handle, { inputCollapsed: true }); + context.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(context.cell.handle, { inputCollapsed: true }); } }); @@ -1585,7 +1585,7 @@ registerAction2(class extends NotebookCellAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { - context.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(context.cell.handle, { inputCollapsed: false }); + context.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(context.cell.handle, { inputCollapsed: false }); } }); @@ -1608,7 +1608,7 @@ registerAction2(class extends NotebookCellAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { - context.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(context.cell.handle, { outputCollapsed: true }); + context.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(context.cell.handle, { outputCollapsed: true }); } }); @@ -1631,7 +1631,7 @@ registerAction2(class extends NotebookCellAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { - context.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(context.cell.handle, { outputCollapsed: false }); + context.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(context.cell.handle, { outputCollapsed: false }); } }); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index 9d02f255038..19a715474a2 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -329,9 +329,9 @@ abstract class AbstractCellRenderer { } if (templateData.currentRenderedCell.metadata?.inputCollapsed) { - this.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(templateData.currentRenderedCell.handle, { inputCollapsed: false }); + this.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(templateData.currentRenderedCell.handle, { inputCollapsed: false }); } else if (templateData.currentRenderedCell.metadata?.outputCollapsed) { - this.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(templateData.currentRenderedCell.handle, { outputCollapsed: false }); + this.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(templateData.currentRenderedCell.handle, { outputCollapsed: false }); } })); } diff --git a/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts b/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts index aaf080d20d1..abd87ce1d2c 100644 --- a/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts +++ b/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts @@ -6,6 +6,7 @@ import { IResourceUndoRedoElement, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo'; import { URI } from 'vs/base/common/uri'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; /** * It should not modify Undo/Redo stack @@ -14,6 +15,7 @@ export interface ITextCellEditingDelegate { insertCell?(index: number, cell: NotebookCellTextModel): void; deleteCell?(index: number): void; moveCell?(fromIndex: number, length: number, toIndex: number, beforeSelections: number[] | undefined, endSelections: number[] | undefined): void; + updateCellMetadata?(index: number, newMetadata: NotebookCellMetadata | undefined): void; emitSelections(selections: number[]): void; } @@ -183,3 +185,33 @@ export class SpliceCellsEdit implements IResourceUndoRedoElement { } } } + +export class CellMetadataEdit implements IResourceUndoRedoElement { + type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; + label: string = 'Update Cell Metadata'; + constructor( + public resource: URI, + readonly index: number, + readonly oldMetadata: NotebookCellMetadata | undefined, + readonly newMetadata: NotebookCellMetadata | undefined, + private editingDelegate: ITextCellEditingDelegate, + ) { + + } + + undo(): void { + if (!this.editingDelegate.updateCellMetadata) { + return; + } + + this.editingDelegate.updateCellMetadata(this.index, this.oldMetadata); + } + + redo(): void | Promise { + if (!this.editingDelegate.updateCellMetadata) { + return; + } + + this.editingDelegate.updateCellMetadata(this.index, this.newMetadata); + } +} diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index fe268309fb9..79903890afc 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -8,10 +8,10 @@ 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 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +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 { ITextSnapshot } from 'vs/editor/common/model'; import { IUndoRedoService, UndoRedoElementType, IUndoRedoElement, IResourceUndoRedoElement } from 'vs/platform/undoRedo/common/undoRedo'; -import { InsertCellEdit, DeleteCellEdit, MoveCellEdit, SpliceCellsEdit } from 'vs/workbench/contrib/notebook/common/model/cellEdit'; +import { InsertCellEdit, DeleteCellEdit, MoveCellEdit, SpliceCellsEdit, CellMetadataEdit } from 'vs/workbench/contrib/notebook/common/model/cellEdit'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; export class NotebookTextModelSnapshot implements ITextSnapshot { @@ -128,6 +128,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel cells: NotebookCellTextModel[]; languages: string[] = []; metadata: NotebookDocumentMetadata = notebookDocumentMetadataDefaults; + transientMetadata: TransientMetadata = {}; private _isUntitled: boolean | undefined = undefined; private _versionId = 0; @@ -259,7 +260,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel break; case CellEditType.Metadata: this.assertIndex(edit.index); - this.changeCellMetadata(this.cells[edit.index].handle, edit.metadata); + this.deltaCellMetadata(this.cells[edit.index].handle, edit.metadata); break; } } @@ -342,14 +343,6 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._onDidChangeMetadata.fire(this.metadata); } - updateNotebookCellMetadata(handle: number, metadata: NotebookCellMetadata) { - const cell = this.cells.find(cell => cell.handle === handle); - - if (cell) { - cell.metadata = metadata; - } - } - insertTemplateCell(cell: NotebookCellTextModel) { if (this.cells.length > 0 || this._isUntitled !== undefined) { return; @@ -504,16 +497,99 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel } } - changeCellMetadata(handle: number, newMetadata: NotebookCellMetadata) { + private _compareCellMetadata(a: NotebookCellMetadata | undefined, b: NotebookCellMetadata | undefined) { + if (a?.editable !== b?.editable && !this.transientMetadata.editable) { + return true; + } + + if (a?.runnable !== b?.runnable && !this.transientMetadata.runnable) { + return true; + } + + if (a?.breakpointMargin !== b?.breakpointMargin && !this.transientMetadata.breakpointMargin) { + return true; + } + + if (a?.hasExecutionOrder !== b?.hasExecutionOrder && !this.transientMetadata.hasExecutionOrder) { + return true; + } + + if (a?.executionOrder !== b?.executionOrder && !this.transientMetadata.executionOrder) { + return true; + } + + if (a?.statusMessage !== b?.statusMessage && !this.transientMetadata.statusMessage) { + return true; + } + + if (a?.runState !== b?.runState && !this.transientMetadata.runState) { + return true; + } + + if (a?.runStartTime !== b?.runStartTime && !this.transientMetadata.runStartTime) { + return true; + } + + if (a?.lastRunDuration !== b?.lastRunDuration && !this.transientMetadata.lastRunDuration) { + return true; + } + + if (a?.inputCollapsed !== b?.inputCollapsed && !this.transientMetadata.inputCollapsed) { + return true; + } + + if (a?.outputCollapsed !== b?.outputCollapsed && !this.transientMetadata.outputCollapsed) { + return true; + } + + if (a?.custom !== b?.custom && !this.transientMetadata.custom) { + return true; + } + + return false; + } + + changeCellMetadata(handle: number, metadata: NotebookCellMetadata | undefined, pushUndoStop: boolean) { + const cell = this.cells.find(cell => cell.handle === handle); + + if (!cell) { + return; + } + + const triggerDirtyChange = this._compareCellMetadata(cell.metadata, metadata); + + if (triggerDirtyChange) { + if (pushUndoStop) { + const index = this.cells.indexOf(cell); + this._operationManager.pushEditOperation(new CellMetadataEdit(this.uri, index, Object.freeze(cell.metadata), Object.freeze(metadata), { + updateCellMetadata: (index, newMetadata) => { + const cell = this.cells[index]; + if (!cell) { + return; + } + this.changeCellMetadata(cell.handle, newMetadata, false); + }, + emitSelections: this._emitSelectionsDelegate.bind(this) + })); + } + cell.metadata = metadata; + this.setDirty(true); + this._onDidChangeContent.fire(); + } else { + cell.metadata = metadata; + } + + this._increaseVersionId(); + this._onDidModelChangeProxy.fire({ kind: NotebookCellsChangeType.ChangeMetadata, versionId: this._versionId, index: this.cells.indexOf(cell), metadata: cell.metadata }); + } + + deltaCellMetadata(handle: number, newMetadata: NotebookCellMetadata) { const cell = this._mapping.get(handle); if (cell) { - cell.metadata = { + this.changeCellMetadata(handle, { ...cell.metadata, ...newMetadata - }; - - this._increaseVersionId(); - this._onDidModelChangeProxy.fire({ kind: NotebookCellsChangeType.ChangeMetadata, versionId: this._versionId, index: this.cells.indexOf(cell), metadata: cell.metadata }); + }, true); } } diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 52520303aa6..f4327ddd295 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -103,6 +103,8 @@ export interface NotebookCellMetadata { custom?: { [key: string]: unknown }; } +export type TransientMetadata = { [K in keyof NotebookCellMetadata]?: boolean }; + 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 1eb32840394..89e0161170e 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookService.ts @@ -10,7 +10,7 @@ import { NotebookExtensionDescription } from 'vs/workbench/api/common/extHost.pr import { Event } from 'vs/base/common/event'; import { INotebookTextModel, INotebookRendererInfo, INotebookKernelInfo, INotebookKernelInfoDto, - IEditor, ICellEditOperation, NotebookCellOutputsSplice, INotebookKernelProvider, INotebookKernelInfo2 + IEditor, ICellEditOperation, NotebookCellOutputsSplice, INotebookKernelProvider, INotebookKernelInfo2, TransientMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -24,6 +24,7 @@ export const INotebookService = createDecorator('notebookServi export interface IMainNotebookController { kernel: INotebookKernelInfoDto | undefined; supportBackup: boolean; + options: { transientMetadata: TransientMetadata }; createNotebook(textModel: NotebookTextModel, editorId?: string, backupId?: string): Promise; reloadNotebook(mainthreadTextModel: NotebookTextModel): Promise; resolveNotebookEditor(viewType: string, uri: URI, editorId: string): Promise;