diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts index 4e59843a163..a4c96f3100e 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts @@ -63,7 +63,9 @@ export namespace Schemas { export const vscodeNotebookCell = 'vscode-notebook-cell'; export const vscodeNotebookCellMetadata = 'vscode-notebook-cell-metadata'; + export const vscodeNotebookCellMetadataDiff = 'vscode-notebook-cell-metadata-diff'; export const vscodeNotebookCellOutput = 'vscode-notebook-cell-output'; + export const vscodeNotebookCellOutputDiff = 'vscode-notebook-cell-output-diff'; export const vscodeInteractiveInput = 'vscode-interactive-input'; export const vscodeSettings = 'vscode-settings'; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts index e074c182fc4..64a25c3c56c 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts @@ -12,6 +12,9 @@ import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/in import { IInlineChatSessionService } from './inlineChatSessionService'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; import { CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { NotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor'; +import { NotebookMultiTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookMultiDiffEditor'; export class InlineChatNotebookContribution { @@ -19,6 +22,7 @@ export class InlineChatNotebookContribution { constructor( @IInlineChatSessionService sessionService: IInlineChatSessionService, + @IEditorService editorService: IEditorService, @INotebookEditorService notebookEditorService: INotebookEditorService, ) { @@ -58,6 +62,11 @@ export class InlineChatNotebookContribution { return fallback; } + const activeEditor = editorService.activeEditorPane; + if (activeEditor && (activeEditor.getId() === NotebookTextDiffEditor.ID || activeEditor.getId() === NotebookMultiTextDiffEditor.ID)) { + return `${editor.getId()}#${uri}`; + } + throw illegalState('Expected notebook editor'); } })); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts index 24e92aa2590..cf5251d3143 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts @@ -11,7 +11,7 @@ import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/com import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ActiveEditorContext } from 'vs/workbench/common/contextkeys'; import { DiffElementCellViewModelBase, SideBySideDiffElementViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; -import { INotebookTextDiffEditor, NOTEBOOK_DIFF_CELL_IGNORE_WHITESPACE_KEY, NOTEBOOK_DIFF_CELL_INPUT, NOTEBOOK_DIFF_CELL_PROPERTY, NOTEBOOK_DIFF_CELL_PROPERTY_EXPANDED } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; +import { INotebookTextDiffEditor, NOTEBOOK_DIFF_CELL_IGNORE_WHITESPACE_KEY, NOTEBOOK_DIFF_CELL_INPUT, NOTEBOOK_DIFF_CELL_PROPERTY, NOTEBOOK_DIFF_CELL_PROPERTY_EXPANDED, NOTEBOOK_DIFF_CELLS_COLLAPSED, NOTEBOOK_DIFF_HAS_UNCHANGED_CELLS, NOTEBOOK_DIFF_UNCHANGED_CELLS_HIDDEN } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; import { NotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor'; import { NotebookDiffEditorInput } from 'vs/workbench/contrib/notebook/common/notebookDiffEditorInput'; import { nextChangeIcon, openAsTextIcon, previousChangeIcon, renderOutputIcon, revertIcon, toggleWhitespace } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; @@ -24,6 +24,8 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { CellEditType, NOTEBOOK_DIFF_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; +import { NotebookMultiTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookMultiDiffEditor'; +import { Codicon } from 'vs/base/common/codicons'; // ActiveEditorContext.isEqualTo(SearchEditorConstants.SearchEditorID) @@ -33,11 +35,11 @@ registerAction2(class extends Action2 { id: 'notebook.diff.switchToText', icon: openAsTextIcon, title: localize2('notebook.diff.switchToText', 'Open Text Diff Editor'), - precondition: ActiveEditorContext.isEqualTo(NotebookTextDiffEditor.ID), + precondition: ContextKeyExpr.or(ActiveEditorContext.isEqualTo(NotebookTextDiffEditor.ID), ActiveEditorContext.isEqualTo(NotebookMultiTextDiffEditor.ID)), menu: [{ id: MenuId.EditorTitle, group: 'navigation', - when: ActiveEditorContext.isEqualTo(NotebookTextDiffEditor.ID) + when: ContextKeyExpr.or(ActiveEditorContext.isEqualTo(NotebookTextDiffEditor.ID), ActiveEditorContext.isEqualTo(NotebookMultiTextDiffEditor.ID)), }] }); } @@ -46,7 +48,10 @@ registerAction2(class extends Action2 { const editorService = accessor.get(IEditorService); const activeEditor = editorService.activeEditorPane; - if (activeEditor && activeEditor instanceof NotebookTextDiffEditor) { + if (!activeEditor) { + return; + } + if (activeEditor instanceof NotebookTextDiffEditor || activeEditor instanceof NotebookMultiTextDiffEditor) { const diffEditorInput = activeEditor.input as NotebookDiffEditorInput; await editorService.openEditor( @@ -63,6 +68,91 @@ registerAction2(class extends Action2 { } }); + +registerAction2(class CollapseAllAction extends Action2 { + constructor() { + super({ + id: 'notebook.multiDiffEditor.collapseAll', + title: localize2('collapseAllDiffs', 'Collapse All Diffs'), + icon: Codicon.collapseAll, + precondition: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(NotebookMultiTextDiffEditor.ID), ContextKeyExpr.not(NOTEBOOK_DIFF_CELLS_COLLAPSED.key)), + menu: { + when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(NotebookMultiTextDiffEditor.ID), ContextKeyExpr.not(NOTEBOOK_DIFF_CELLS_COLLAPSED.key)), + id: MenuId.EditorTitle, + group: 'navigation', + order: 100 + }, + f1: true, + }); + } + + async run(accessor: ServicesAccessor): Promise { + const activeEditor = accessor.get(IEditorService).activeEditorPane; + if (!activeEditor) { + return; + } + if (activeEditor instanceof NotebookMultiTextDiffEditor) { + activeEditor.collapseAll(); + } + } +}); + +registerAction2(class ExpandAllAction extends Action2 { + constructor() { + super({ + id: 'notebook.multiDiffEditor.expandAll', + title: localize2('ExpandAllDiffs', 'Expand All Diffs'), + icon: Codicon.expandAll, + precondition: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(NotebookMultiTextDiffEditor.ID), ContextKeyExpr.has(NOTEBOOK_DIFF_CELLS_COLLAPSED.key)), + menu: { + when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(NotebookMultiTextDiffEditor.ID), ContextKeyExpr.has(NOTEBOOK_DIFF_CELLS_COLLAPSED.key)), + id: MenuId.EditorTitle, + group: 'navigation', + order: 100 + }, + f1: true, + }); + } + + async run(accessor: ServicesAccessor): Promise { + const activeEditor = accessor.get(IEditorService).activeEditorPane; + if (!activeEditor) { + return; + } + if (activeEditor instanceof NotebookMultiTextDiffEditor) { + activeEditor.expandAll(); + } + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'notebook.diffEditor.toggleCollapseUnchangedCells', + title: localize2('toggleCollapseUnchangedCells', 'Toggle Collapse Unchanged Cells'), + icon: Codicon.map, + toggled: ContextKeyExpr.has(NOTEBOOK_DIFF_UNCHANGED_CELLS_HIDDEN.key), + precondition: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(NotebookMultiTextDiffEditor.ID), ContextKeyExpr.has(NOTEBOOK_DIFF_HAS_UNCHANGED_CELLS.key)), + menu: { + when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(NotebookMultiTextDiffEditor.ID), ContextKeyExpr.has(NOTEBOOK_DIFF_HAS_UNCHANGED_CELLS.key)), + id: MenuId.EditorTitle, + order: 22, + group: 'navigation', + }, + }); + } + + run(accessor: ServicesAccessor, ...args: unknown[]): void { + const activeEditor = accessor.get(IEditorService).activeEditorPane; + if (!activeEditor) { + return; + } + if (activeEditor instanceof NotebookMultiTextDiffEditor) { + activeEditor.toggleUnchangedCells(); + } + } +}); + registerAction2(class extends Action2 { constructor() { super( diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts index 0b53f1cfb2c..15912c8dd51 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts @@ -17,6 +17,7 @@ import { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebo import { WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { localize } from 'vs/nls'; export enum DiffSide { Original = 0, @@ -145,6 +146,9 @@ export const NOTEBOOK_DIFF_CELL_IGNORE_WHITESPACE_KEY = 'notebookDiffCellIgnoreW export const NOTEBOOK_DIFF_CELL_IGNORE_WHITESPACE = new RawContextKey(NOTEBOOK_DIFF_CELL_IGNORE_WHITESPACE_KEY, false); export const NOTEBOOK_DIFF_CELL_PROPERTY = new RawContextKey('notebookDiffCellPropertyChanged', false); export const NOTEBOOK_DIFF_CELL_PROPERTY_EXPANDED = new RawContextKey('notebookDiffCellPropertyExpanded', false); +export const NOTEBOOK_DIFF_CELLS_COLLAPSED = new RawContextKey('notebookDiffEditorAllCollapsed', undefined, localize('notebookDiffEditorAllCollapsed', "Whether all cells in notebook diff editor are collapsed")); +export const NOTEBOOK_DIFF_HAS_UNCHANGED_CELLS = new RawContextKey('notebookDiffEditorHasUnchangedCells', undefined, localize('notebookDiffEditorHasUnchangedCells', "Whether there are unchanged cells in the notebook diff editor")); +export const NOTEBOOK_DIFF_UNCHANGED_CELLS_HIDDEN = new RawContextKey('notebookDiffEditorHiddenUnchangedCells', undefined, localize('notebookDiffEditorHiddenUnchangedCells', "Whether the unchanged cells in the notebook diff editor are hidden")); export interface INotebookDiffViewModelUpdateEvent { readonly start: number; diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffViewModel.ts index e4e0b93cd00..013a10c477a 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffViewModel.ts @@ -5,20 +5,23 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IDiffResult, IDiffChange } from 'vs/base/common/diff/diff'; -import { Emitter } from 'vs/base/common/event'; +import { Emitter, type IValueWithChangeEvent } from 'vs/base/common/event'; import { Disposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import type { URI } from 'vs/base/common/uri'; import { FontInfo } from 'vs/editor/common/config/fontInfo'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { MultiDiffEditorItem } from 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffSourceResolverService'; import { DiffElementCellViewModelBase, DiffElementPlaceholderViewModel, IDiffElementViewModelBase, SideBySideDiffElementViewModel, SingleSideDiffElementViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; import { NotebookDiffEditorEventDispatcher } from 'vs/workbench/contrib/notebook/browser/diff/eventDispatcher'; import { INotebookDiffViewModel, INotebookDiffViewModelUpdateEvent } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { INotebookDiffEditorModel, INotebookDiffResult } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellUri, INotebookDiffEditorModel, INotebookDiffResult } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { INotebookEditorWorkerService } from 'vs/workbench/contrib/notebook/common/services/notebookWorkerService'; -export class NotebookDiffViewModel extends Disposable implements INotebookDiffViewModel { +export class NotebookDiffViewModel extends Disposable implements INotebookDiffViewModel, IValueWithChangeEvent { private readonly placeholderAndRelatedCells = new Map(); private readonly _items: IDiffElementViewModelBase[] = []; get items(): readonly IDiffElementViewModelBase[] { @@ -27,6 +30,25 @@ export class NotebookDiffViewModel extends Disposable implements INotebookDiffVi private readonly _onDidChangeItems = this._register(new Emitter()); public readonly onDidChangeItems = this._onDidChangeItems.event; private readonly disposables = this._register(new DisposableStore()); + private _onDidChange = this._register(new Emitter()); + private diffEditorItems: NotebookMultiDiffEditorItem[] = []; + public onDidChange = this._onDidChange.event; + get value(): readonly NotebookMultiDiffEditorItem[] { + return this.diffEditorItems.filter(item => item.type !== 'placeholder').filter(item => this._includeUnchanged ? true : item.type !== 'unchanged'); + } + + private _hasUnchangedCells?: boolean; + public get hasUnchangedCells() { + return this._hasUnchangedCells === true; + } + private _includeUnchanged?: boolean; + public get includeUnchanged() { + return this._includeUnchanged === true; + } + public set includeUnchanged(value) { + this._includeUnchanged = value; + this._onDidChange.fire(); + } private originalCellViewModels: DiffElementCellViewModelBase[] = []; constructor(private readonly model: INotebookDiffEditorModel, @@ -36,6 +58,7 @@ export class NotebookDiffViewModel extends Disposable implements INotebookDiffVi private readonly eventDispatcher: NotebookDiffEditorEventDispatcher, private readonly notebookService: INotebookService, private readonly fontInfo?: FontInfo, + private readonly excludeUnchangedPlaceholder?: boolean, ) { super(); } @@ -67,10 +90,61 @@ export class NotebookDiffViewModel extends Disposable implements INotebookDiffVi return; } else { this.updateViewModels(cellDiffInfo); + this.updateDiffEditorItems(); return { firstChangeIndex }; } } + private updateDiffEditorItems() { + this.diffEditorItems = []; + const originalSourceUri = this.model.original.resource!; + const modifiedSourceUri = this.model.modified.resource!; + this._hasUnchangedCells = false; + this.items.forEach(item => { + switch (item.type) { + case 'delete': { + this.diffEditorItems.push(new NotebookMultiDiffEditorItem(item.original!.uri, undefined, undefined, item.type)); + const originalMetadata = CellUri.generateCellPropertyUri(originalSourceUri, item.original!.handle, Schemas.vscodeNotebookCellMetadata); + this.diffEditorItems.push(new NotebookMultiDiffEditorItem(originalMetadata, undefined, undefined, item.type)); + break; + } + case 'insert': { + this.diffEditorItems.push(new NotebookMultiDiffEditorItem(undefined, item.modified!.uri, item.modified!.uri, item.type)); + const modifiedMetadata = CellUri.generateCellPropertyUri(modifiedSourceUri, item.modified!.handle, Schemas.vscodeNotebookCellMetadata); + this.diffEditorItems.push(new NotebookMultiDiffEditorItem(modifiedMetadata, undefined, item.modified!.uri, item.type)); + break; + } + case 'modified': { + this.diffEditorItems.push(new NotebookMultiDiffEditorItem(item.original!.uri, item.modified!.uri, item.modified!.uri, item.type)); + if (item.checkMetadataIfModified()) { + const originalMetadata = CellUri.generateCellPropertyUri(originalSourceUri, item.original!.handle, Schemas.vscodeNotebookCellMetadata); + const modifiedMetadata = CellUri.generateCellPropertyUri(modifiedSourceUri, item.modified!.handle, Schemas.vscodeNotebookCellMetadata); + this.diffEditorItems.push(new NotebookMultiDiffEditorItem(originalMetadata, modifiedMetadata, item.modified!.uri, item.type)); + } + if (item.checkIfOutputsModified()) { + const originalOutput = CellUri.generateCellPropertyUri(originalSourceUri, item.original!.handle, Schemas.vscodeNotebookCellOutput); + const modifiedOutput = CellUri.generateCellPropertyUri(modifiedSourceUri, item.modified!.handle, Schemas.vscodeNotebookCellOutput); + this.diffEditorItems.push(new NotebookMultiDiffEditorItem(originalOutput, modifiedOutput, item.modified!.uri, item.type)); + } + break; + } + case 'unchanged': { + this._hasUnchangedCells = true; + this.diffEditorItems.push(new NotebookMultiDiffEditorItem(item.original!.uri, item.modified!.uri, item.modified!.uri, item.type)); + const originalMetadata = CellUri.generateCellPropertyUri(originalSourceUri, item.original!.handle, Schemas.vscodeNotebookCellMetadata); + const modifiedMetadata = CellUri.generateCellPropertyUri(modifiedSourceUri, item.modified!.handle, Schemas.vscodeNotebookCellMetadata); + this.diffEditorItems.push(new NotebookMultiDiffEditorItem(originalMetadata, modifiedMetadata, item.modified!.uri, item.type)); + const originalOutput = CellUri.generateCellPropertyUri(originalSourceUri, item.original!.handle, Schemas.vscodeNotebookCellOutput); + const modifiedOutput = CellUri.generateCellPropertyUri(modifiedSourceUri, item.modified!.handle, Schemas.vscodeNotebookCellOutput); + this.diffEditorItems.push(new NotebookMultiDiffEditorItem(originalOutput, modifiedOutput, item.modified!.uri, item.type)); + break; + } + } + }); + + this._onDidChange.fire(); + } + private updateViewModels(cellDiffInfo: CellDiffInfo[]) { const cellViewModels = createDiffViewModels(this.instantiationService, this.configurationService, this.model, this.eventDispatcher, cellDiffInfo, this.fontInfo, this.notebookService); const oldLength = this._items.length; @@ -80,7 +154,7 @@ export class NotebookDiffViewModel extends Disposable implements INotebookDiffVi let placeholder: DiffElementPlaceholderViewModel | undefined = undefined; this.originalCellViewModels = cellViewModels; cellViewModels.forEach((vm, index) => { - if (vm.type === 'unchanged') { + if (vm.type === 'unchanged' && !this.excludeUnchangedPlaceholder) { if (!placeholder) { vm.displayIconToHideUnmodifiedCells = true; placeholder = new DiffElementPlaceholderViewModel(vm.mainDocumentTextModel, vm.editorEventDispatcher, vm.initData); @@ -364,3 +438,15 @@ function computeModifiedLCS(change: IDiffChange, originalModel: NotebookTextMode return result; } + + +class NotebookMultiDiffEditorItem extends MultiDiffEditorItem { + constructor( + originalUri: URI | undefined, + modifiedUri: URI | undefined, + goToFileUri: URI | undefined, + public readonly type: IDiffElementViewModelBase['type'], + ) { + super(originalUri, modifiedUri, goToFileUri); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookMultiDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookMultiDiffEditor.ts new file mode 100644 index 00000000000..44c6db8a93a --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookMultiDiffEditor.ts @@ -0,0 +1,217 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import { IWorkbenchUIElementFactory, type IResourceLabel } from 'vs/editor/browser/widget/multiDiffEditor/workbenchUIElementFactory'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IEditorOpenContext } from 'vs/workbench/common/editor'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { INotebookEditorWorkerService } from 'vs/workbench/contrib/notebook/common/services/notebookWorkerService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { BareFontInfo, FontInfo } from 'vs/editor/common/config/fontInfo'; +import { PixelRatio } from 'vs/base/browser/pixelRatio'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; +import { INotebookDiffEditorModel, NOTEBOOK_MULTI_DIFF_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { FontMeasurements } from 'vs/editor/browser/config/fontMeasurements'; +import { NotebookOptions } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { NotebookMultiDiffEditorInput, NotebookMultiDiffEditorWidgetInput } from 'vs/workbench/contrib/notebook/browser/diff/notebookMultiDiffEditorInput'; +import { MultiDiffEditorWidget } from 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidget'; +import { ResourceLabel } from 'vs/workbench/browser/labels'; +import type { IMultiDiffEditorOptions } from 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl'; +import { INotebookDocumentService } from 'vs/workbench/services/notebook/common/notebookDocumentService'; +import { localize } from 'vs/nls'; +import { Schemas } from 'vs/base/common/network'; +import { getIconClassesForLanguageId } from 'vs/editor/common/services/getIconClasses'; +import { NotebookDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffViewModel'; +import { NotebookDiffEditorEventDispatcher } from 'vs/workbench/contrib/notebook/browser/diff/eventDispatcher'; +import { NOTEBOOK_DIFF_CELLS_COLLAPSED, NOTEBOOK_DIFF_HAS_UNCHANGED_CELLS, NOTEBOOK_DIFF_UNCHANGED_CELLS_HIDDEN } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; +import type { MultiDiffEditorViewModel } from 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel'; + +export class NotebookMultiTextDiffEditor extends EditorPane { + private _multiDiffEditorWidget?: MultiDiffEditorWidget; + static readonly ID: string = NOTEBOOK_MULTI_DIFF_EDITOR_ID; + private _fontInfo: FontInfo | undefined; + protected _scopeContextKeyService!: IContextKeyService; + private readonly modelSpecificResources = this._register(new DisposableStore()); + private _model?: INotebookDiffEditorModel; + private viewModel?: NotebookDiffViewModel; + private widgetViewModel?: MultiDiffEditorViewModel; + get textModel() { + return this._model?.modified.notebook; + } + private _notebookOptions: NotebookOptions; + get notebookOptions() { + return this._notebookOptions; + } + private readonly ctxAllCollapsed = this._parentContextKeyService.createKey(NOTEBOOK_DIFF_CELLS_COLLAPSED.key, false); + private readonly ctxHasUnchangedCells = this._parentContextKeyService.createKey(NOTEBOOK_DIFF_HAS_UNCHANGED_CELLS.key, false); + private readonly ctxHiddenUnchangedCells = this._parentContextKeyService.createKey(NOTEBOOK_DIFF_UNCHANGED_CELLS_HIDDEN.key, true); + + constructor( + group: IEditorGroup, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IThemeService themeService: IThemeService, + @IContextKeyService private readonly _parentContextKeyService: IContextKeyService, + @INotebookEditorWorkerService private readonly notebookEditorWorkerService: INotebookEditorWorkerService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @ITelemetryService telemetryService: ITelemetryService, + @IStorageService storageService: IStorageService, + @INotebookService private readonly notebookService: INotebookService, + ) { + super(NotebookMultiTextDiffEditor.ID, group, telemetryService, themeService, storageService); + this._notebookOptions = instantiationService.createInstance(NotebookOptions, this.window, false, undefined); + this._register(this._notebookOptions); + } + + private get fontInfo() { + if (!this._fontInfo) { + this._fontInfo = this.createFontInfo(); + } + + return this._fontInfo; + } + override layout(dimension: DOM.Dimension, position?: DOM.IDomPosition): void { + this._multiDiffEditorWidget!.layout(dimension); + } + + private createFontInfo() { + const editorOptions = this.configurationService.getValue('editor'); + return FontMeasurements.readFontInfo(this.window, BareFontInfo.createFromRawSettings(editorOptions, PixelRatio.getInstance(this.window).value)); + } + + protected createEditor(parent: HTMLElement): void { + this._multiDiffEditorWidget = this._register(this.instantiationService.createInstance( + MultiDiffEditorWidget, + parent, + this.instantiationService.createInstance(WorkbenchUIElementFactory), + )); + + this._register(this._multiDiffEditorWidget.onDidChangeActiveControl(() => { + this._onDidChangeControl.fire(); + })); + } + override async setInput(input: NotebookMultiDiffEditorInput, options: IMultiDiffEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + super.setInput(input, options, context, token); + const model = await input.resolve(); + if (this._model !== model) { + this._detachModel(); + this._model = model; + } + const eventDispatcher = this.modelSpecificResources.add(new NotebookDiffEditorEventDispatcher()); + this.viewModel = this.modelSpecificResources.add(new NotebookDiffViewModel(model, this.notebookEditorWorkerService, this.instantiationService, this.configurationService, eventDispatcher, this.notebookService, undefined, true)); + await this.viewModel.computeDiff(this.modelSpecificResources.add(new CancellationTokenSource()).token); + this.ctxHasUnchangedCells.set(this.viewModel.hasUnchangedCells); + + const widgetInput = this.modelSpecificResources.add(NotebookMultiDiffEditorWidgetInput.createInput(this.viewModel, this.instantiationService)); + this.widgetViewModel = this.modelSpecificResources.add(await widgetInput.getViewModel()); + this._multiDiffEditorWidget!.setViewModel(this.widgetViewModel); + } + + private _detachModel() { + this.viewModel = undefined; + this.modelSpecificResources.clear(); + } + _generateFontFamily(): string { + return this.fontInfo.fontFamily ?? `"SF Mono", Monaco, Menlo, Consolas, "Ubuntu Mono", "Liberation Mono", "DejaVu Sans Mono", "Courier New", monospace`; + } + override setOptions(options: IMultiDiffEditorOptions | undefined): void { + super.setOptions(options); + } + + override getControl() { + return this._multiDiffEditorWidget!.getActiveControl(); + } + + override focus(): void { + super.focus(); + + this._multiDiffEditorWidget?.getActiveControl()?.focus(); + } + + override hasFocus(): boolean { + return this._multiDiffEditorWidget?.getActiveControl()?.hasTextFocus() || super.hasFocus(); + } + + override clearInput(): void { + super.clearInput(); + this._multiDiffEditorWidget!.setViewModel(undefined); + this.modelSpecificResources.clear(); + this.viewModel = undefined; + this.widgetViewModel = undefined; + } + + public expandAll() { + if (this.widgetViewModel) { + this.widgetViewModel.expandAll(); + this.ctxAllCollapsed.set(false); + } + } + public collapseAll() { + if (this.widgetViewModel) { + this.widgetViewModel.collapseAll(); + this.ctxAllCollapsed.set(true); + } + } + + public toggleUnchangedCells() { + if (this.viewModel) { + this.viewModel.includeUnchanged = !this.viewModel.includeUnchanged; + this.ctxHiddenUnchangedCells.set(this.viewModel.includeUnchanged); + } + } +} + + +class WorkbenchUIElementFactory implements IWorkbenchUIElementFactory { + constructor( + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @INotebookDocumentService private readonly notebookDocumentService: INotebookDocumentService, + @INotebookService private readonly notebookService: INotebookService + ) { } + + createResourceLabel(element: HTMLElement): IResourceLabel { + const label = this._instantiationService.createInstance(ResourceLabel, element, {}); + const that = this; + return { + setUri(uri, options = {}) { + if (!uri) { + label.element.clear(); + } else { + let name = ''; + let description = ''; + let extraClasses: string[] | undefined = undefined; + + if (uri.scheme === Schemas.vscodeNotebookCell) { + const notebookDocument = uri.scheme === Schemas.vscodeNotebookCell ? that.notebookDocumentService.getNotebook(uri) : undefined; + const cellIndex = Schemas.vscodeNotebookCell ? that.notebookDocumentService.getNotebook(uri)?.getCellIndex(uri) : undefined; + if (notebookDocument && cellIndex !== undefined) { + name = localize('notebookCellLabel', "Cell {0}", `${cellIndex + 1}`); + const nb = notebookDocument ? that.notebookService.getNotebookTextModel(notebookDocument?.uri) : undefined; + const cellLanguage = nb && cellIndex !== undefined ? nb.cells[cellIndex].language : undefined; + extraClasses = cellLanguage ? getIconClassesForLanguageId(cellLanguage) : undefined; + } + } else if (uri.scheme === Schemas.vscodeNotebookCellMetadata || uri.scheme === Schemas.vscodeNotebookCellMetadataDiff) { + description = localize('notebookCellMetadataLabel', "Metadata"); + } else if (uri.scheme === Schemas.vscodeNotebookCellOutput || uri.scheme === Schemas.vscodeNotebookCellOutputDiff) { + description = localize('notebookCellOutputLabel', "Output"); + } + + label.element.setResource({ name, description }, { strikethrough: options.strikethrough, forceLabel: true, hideIcon: !extraClasses, extraClasses }); + } + }, + dispose() { + label.dispose(); + } + }; + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookMultiDiffEditorInput.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookMultiDiffEditorInput.ts new file mode 100644 index 00000000000..e0f66116604 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookMultiDiffEditorInput.ts @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from 'vs/base/common/uri'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { MultiDiffEditorInput } from 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput'; +import { IMultiDiffSourceResolverService, IResolvedMultiDiffSource, type IMultiDiffSourceResolver } from 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffSourceResolverService'; +import { NotebookDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffViewModel'; +import { NotebookDiffEditorInput } from 'vs/workbench/contrib/notebook/common/notebookDiffEditorInput'; +import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; + +export const NotebookMultiDiffEditorScheme = 'multi-cell-notebook-diff-editor'; + +export class NotebookMultiDiffEditorInput extends NotebookDiffEditorInput { + static override readonly ID: string = 'workbench.input.multiDiffNotebookInput'; + static override create(instantiationService: IInstantiationService, resource: URI, name: string | undefined, description: string | undefined, originalResource: URI, viewType: string) { + const original = NotebookEditorInput.getOrCreate(instantiationService, originalResource, undefined, viewType); + const modified = NotebookEditorInput.getOrCreate(instantiationService, resource, undefined, viewType); + return instantiationService.createInstance(NotebookMultiDiffEditorInput, name, description, original, modified, viewType); + } +} + +export class NotebookMultiDiffEditorWidgetInput extends MultiDiffEditorInput implements IMultiDiffSourceResolver { + public static createInput(notebookDiffViewModel: NotebookDiffViewModel, instantiationService: IInstantiationService): NotebookMultiDiffEditorWidgetInput { + const multiDiffSource = URI.parse(`${NotebookMultiDiffEditorScheme}:${new Date().getMilliseconds().toString() + Math.random().toString()}`); + return instantiationService.createInstance( + NotebookMultiDiffEditorWidgetInput, + multiDiffSource, + notebookDiffViewModel + ); + } + constructor( + multiDiffSource: URI, + private readonly notebookDiffViewModel: NotebookDiffViewModel, + @ITextModelService _textModelService: ITextModelService, + @ITextResourceConfigurationService _textResourceConfigurationService: ITextResourceConfigurationService, + @IInstantiationService _instantiationService: IInstantiationService, + @IMultiDiffSourceResolverService _multiDiffSourceResolverService: IMultiDiffSourceResolverService, + @ITextFileService _textFileService: ITextFileService, + ) { + super(multiDiffSource, undefined, undefined, true, _textModelService, _textResourceConfigurationService, _instantiationService, _multiDiffSourceResolverService, _textFileService); + this._register(_multiDiffSourceResolverService.registerResolver(this)); + } + + canHandleUri(uri: URI): boolean { + return uri.toString() === this.multiDiffSource.toString(); + } + + async resolveDiffSource(_: URI): Promise { + return { resources: this.notebookDiffViewModel }; + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index fd6253a67e1..35af06e4a75 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -44,7 +44,7 @@ import { NotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/brows import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema'; import { Event } from 'vs/base/common/event'; -import { getFormattedMetadataJSON, getStreamOutputData } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; +import { getFormattedMetadataJSON, getFormattedOutputJSON, getStreamOutputData } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; import { NotebookModelResolverServiceImpl } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl'; import { INotebookKernelHistoryService, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { NotebookKernelService } from 'vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl'; @@ -123,6 +123,8 @@ import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/access import { NotebookAccessibilityHelp } from 'vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp'; import { NotebookAccessibleView } from 'vs/workbench/contrib/notebook/browser/notebookAccessibleView'; import { DefaultFormatter } from 'vs/workbench/contrib/format/browser/formatActionsMultiple'; +import { NotebookMultiTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookMultiDiffEditor'; +import { NotebookMultiDiffEditorInput } from 'vs/workbench/contrib/notebook/browser/diff/notebookMultiDiffEditorInput'; /*--------------------------------------------------------------------------------------------- */ @@ -148,7 +150,19 @@ Registry.as(EditorExtensions.EditorPane).registerEditorPane ] ); +Registry.as(EditorExtensions.EditorPane).registerEditorPane( + EditorPaneDescriptor.create( + NotebookMultiTextDiffEditor, + NotebookMultiTextDiffEditor.ID, + 'Notebook Diff Editor' + ), + [ + new SyncDescriptor(NotebookMultiDiffEditorInput) + ] +); + class NotebookDiffEditorSerializer implements IEditorSerializer { + constructor(@IConfigurationService private readonly _configurationService: IConfigurationService) { } canSerialize(): boolean { return true; } @@ -176,8 +190,11 @@ class NotebookDiffEditorSerializer implements IEditorSerializer { return undefined; } - const input = NotebookDiffEditorInput.create(instantiationService, resource, name, undefined, originalResource, viewType); - return input; + if (this._configurationService.getValue('notebook.experimental.enableNewDiffEditor')) { + return NotebookMultiDiffEditorInput.create(instantiationService, resource, name, undefined, originalResource, viewType); + } else { + return NotebookDiffEditorInput.create(instantiationService, resource, name, undefined, originalResource, viewType); + } } static canResolveBackup(editorInput: EditorInput, backupResource: URI): boolean { @@ -493,6 +510,40 @@ class CellInfoContentProvider { return result; } + async provideOutputsTextContent(resource: URI): Promise { + const existing = this._modelService.getModel(resource); + if (existing) { + return existing; + } + + const data = CellUri.parseCellPropertyUri(resource, Schemas.vscodeNotebookCellOutput); + if (!data) { + return null; + } + + const ref = await this._notebookModelResolverService.resolve(data.notebook); + const cell = ref.object.notebook.cells.find(cell => cell.handle === data.handle); + + if (!cell) { + ref.dispose(); + return null; + } + + const mode = this._languageService.createById('json'); + const model = this._modelService.createModel(getFormattedOutputJSON(cell.outputs || []), mode, resource, true); + const cellModelListener = Event.any(cell.onDidChangeOutputs ?? Event.None, cell.onDidChangeOutputItems ?? Event.None)(() => { + model.setValue(getFormattedOutputJSON(cell.outputs || [])); + }); + + const once = model.onWillDispose(() => { + once.dispose(); + cellModelListener.dispose(); + ref.dispose(); + }); + + return model; + } + async provideOutputTextContent(resource: URI): Promise { const existing = this._modelService.getModel(resource); if (existing) { @@ -501,7 +552,7 @@ class CellInfoContentProvider { const data = CellUri.parseCellOutputUri(resource); if (!data) { - return null; + return this.provideOutputsTextContent(resource); } const ref = await this._notebookModelResolverService.resolve(data.notebook); diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts index aca165f547c..ba6970317dd 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts @@ -41,8 +41,10 @@ import { InstallRecommendedExtensionAction } from 'vs/workbench/contrib/extensio import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { INotebookDocument, INotebookDocumentService } from 'vs/workbench/services/notebook/common/notebookDocumentService'; import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; -import type { EditorInputWithOptions, IResourceMergeEditorInput } from 'vs/workbench/common/editor'; +import type { EditorInputWithOptions, IResourceDiffEditorInput, IResourceMergeEditorInput } from 'vs/workbench/common/editor'; import { streamToBuffer, VSBuffer, VSBufferReadableStream } from 'vs/base/common/buffer'; +import type { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { NotebookMultiDiffEditorInput } from 'vs/workbench/contrib/notebook/browser/diff/notebookMultiDiffEditorInput'; export class NotebookProviderInfoStore extends Disposable { @@ -213,7 +215,12 @@ export class NotebookProviderInfoStore extends Disposable { return { editor: NotebookEditorInput.getOrCreate(this._instantiationService, ref.object.resource, undefined, notebookProviderInfo.id), options }; }; - const notebookDiffEditorInputFactory: DiffEditorInputFactoryFunction = ({ modified, original, label, description }) => { + const notebookDiffEditorInputFactory: DiffEditorInputFactoryFunction = (diffEditorInput: IResourceDiffEditorInput, group: IEditorGroup) => { + const { modified, original, label, description } = diffEditorInput; + + if (this._configurationService.getValue('notebook.experimental.enableNewDiffEditor')) { + return { editor: NotebookMultiDiffEditorInput.create(this._instantiationService, modified.resource!, label, description, original.resource!, notebookProviderInfo.id) }; + } return { editor: NotebookDiffEditorInput.create(this._instantiationService, modified.resource!, label, description, original.resource!, notebookProviderInfo.id) }; }; const mergeEditorInputFactory: MergeEditorInputFactoryFunction = (mergeEditor: IResourceMergeEditorInput): EditorInputWithOptions => { diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index aea2596da48..4a7599869d1 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -39,6 +39,7 @@ import { IWorkingCopyBackupMeta, IWorkingCopySaveEvent } from 'vs/workbench/serv export const NOTEBOOK_EDITOR_ID = 'workbench.editor.notebook'; export const NOTEBOOK_DIFF_EDITOR_ID = 'workbench.editor.notebookTextDiffEditor'; +export const NOTEBOOK_MULTI_DIFF_EDITOR_ID = 'workbench.editor.notebookMultiTextDiffEditor'; export const INTERACTIVE_WINDOW_EDITOR_ID = 'workbench.editor.interactive'; export const REPL_EDITOR_ID = 'workbench.editor.repl'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts index 1e575c525f1..d35dde6208e 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts @@ -217,7 +217,7 @@ export function setupInstantiationService(disposables: Pick { + const notebook = disposables.add(instantiationService.createInstance(NotebookTextModel, viewType, URI.parse('test://test'), cells.map((cell): ICellDto2 => { return { source: cell[0], mime: undefined, @@ -378,12 +378,18 @@ export async function withTestNotebookDiffModel(originalCells: [source: override get notebook() { return originalNotebook.viewModel.notebookDocument; } + override get resource() { + return originalNotebook.viewModel.notebookDocument.uri; + } }; const modifiedResource = new class extends mock() { override get notebook() { return modifiedNotebook.viewModel.notebookDocument; } + override get resource() { + return modifiedNotebook.viewModel.notebookDocument.uri; + } }; const model = new class extends mock() {