diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts index 35918ac40a8..58d7473742f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts @@ -19,6 +19,7 @@ import { IEditorPane, IResourceDiffEditorInput } from '../../../../../common/edi import { IEditorService } from '../../../../../services/editor/common/editorService.js'; import { NotebookDeletedCellDecorator } from '../../../../notebook/browser/diff/inlineDiff/notebookDeletedCellDecorator.js'; import { NotebookInsertedCellDecorator } from '../../../../notebook/browser/diff/inlineDiff/notebookInsertedCellDecorator.js'; +import { NotebookModifiedCellDecorator } from '../../../../notebook/browser/diff/inlineDiff/notebookModifiedCellDecorator.js'; import { INotebookTextDiffEditor } from '../../../../notebook/browser/diff/notebookDiffEditorBrowser.js'; import { getNotebookEditorFromEditorPane, ICellViewModel, INotebookEditor } from '../../../../notebook/browser/notebookBrowser.js'; import { INotebookEditorService } from '../../../../notebook/browser/services/notebookEditorService.js'; @@ -92,7 +93,7 @@ class ChatEditingNotebookEditorWidgetIntegration extends Disposable implements I private readonly cellEditorIntegrations = new Map }>(); - private readonly insertDeleteDecorators: IObservable<{ insertedCellDecorator: NotebookInsertedCellDecorator; deletedCellDecorator: NotebookDeletedCellDecorator } | undefined>; + private readonly insertDeleteDecorators: IObservable<{ insertedCellDecorator: NotebookInsertedCellDecorator; modifiedCellDecorator: NotebookModifiedCellDecorator; deletedCellDecorator: NotebookDeletedCellDecorator } | undefined>; constructor( private readonly _entry: ChatEditingModifiedNotebookEntry, @@ -258,6 +259,7 @@ class ChatEditingNotebookEditorWidgetIntegration extends Disposable implements I } const insertedCellDecorator = store.add(this.instantiationService.createInstance(NotebookInsertedCellDecorator, this.notebookEditor)); + const modifiedCellDecorator = store.add(this.instantiationService.createInstance(NotebookModifiedCellDecorator, this.notebookEditor)); const deletedCellDecorator = store.add(this.instantiationService.createInstance(NotebookDeletedCellDecorator, this.notebookEditor, { className: 'chat-diff-change-content-widget', telemetrySource: 'chatEditingNotebookHunk', @@ -286,6 +288,7 @@ class ChatEditingNotebookEditorWidgetIntegration extends Disposable implements I return { insertedCellDecorator, + modifiedCellDecorator, deletedCellDecorator }; }); @@ -297,9 +300,11 @@ class ChatEditingNotebookEditorWidgetIntegration extends Disposable implements I } // We can have inserted cells that have been accepted, in those cases we do not want any decorators on them. const changes = debouncedObservable(cellChanges, 10).read(r).filter(c => c.type === 'insert' ? !c.diff.read(r).identical : true); + const modifiedChanges = changes.filter(c => c.type === 'modified'); const decorators = debouncedObservable(this.insertDeleteDecorators, 10).read(r); if (decorators) { decorators.insertedCellDecorator.apply(changes); + decorators.modifiedCellDecorator.apply(modifiedChanges); decorators.deletedCellDecorator.apply(changes, originalModel); } })); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookDeletedCellDecorator.ts b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookDeletedCellDecorator.ts index b75579cab13..540f3080980 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookDeletedCellDecorator.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookDeletedCellDecorator.ts @@ -13,13 +13,14 @@ import { NotebookCellTextModel } from '../../../common/model/notebookCellTextMod import { NotebookTextModel } from '../../../common/model/notebookTextModel.js'; import { DefaultLineHeight } from '../diffElementViewModel.js'; import { CellDiffInfo } from '../notebookDiffViewModel.js'; -import { INotebookEditor } from '../../notebookBrowser.js'; +import { INotebookEditor, NotebookOverviewRulerLane } from '../../notebookBrowser.js'; import * as DOM from '../../../../../../base/browser/dom.js'; import { MenuWorkbenchToolBar, HiddenItemStrategy } from '../../../../../../platform/actions/browser/toolbar.js'; import { MenuId } from '../../../../../../platform/actions/common/actions.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ServiceCollection } from '../../../../../../platform/instantiation/common/serviceCollection.js'; import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { overviewRulerDeletedForeground } from '../../../../scm/common/quickDiff.js'; const ttPolicy = createTrustedTypesPolicy('notebookRenderer', { createHTML: value => value }); @@ -143,6 +144,16 @@ export class NotebookDeletedCellDecorator extends Disposable implements INoteboo const id = accessor.addZone(notebookViewZone); accessor.layoutZone(id); this.createdViewZones.set(index, id); + + const deletedCellOverviewRulereDecorationIds = this._notebookEditor.deltaCellDecorations([], [{ + viewZoneId: id, + options: { + overviewRuler: { + color: overviewRulerDeletedForeground, + position: NotebookOverviewRulerLane.Center, + } + } + }]); this.zoneRemover.add(toDisposable(() => { if (this.createdViewZones.get(index) === id) { this.createdViewZones.delete(index); @@ -152,6 +163,8 @@ export class NotebookDeletedCellDecorator extends Disposable implements INoteboo accessor.removeZone(id); dispose(widgets); }); + + this._notebookEditor.deltaCellDecorations(deletedCellOverviewRulereDecorationIds, []); } })); }); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookInsertedCellDecorator.ts b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookInsertedCellDecorator.ts index 996497312f3..7d3121d3465 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookInsertedCellDecorator.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookInsertedCellDecorator.ts @@ -5,7 +5,8 @@ import { Disposable, DisposableStore, toDisposable } from '../../../../../../base/common/lifecycle.js'; import { CellDiffInfo } from '../notebookDiffViewModel.js'; -import { INotebookEditor } from '../../notebookBrowser.js'; +import { INotebookEditor, NotebookOverviewRulerLane } from '../../notebookBrowser.js'; +import { overviewRulerAddedForeground } from '../../../../scm/common/quickDiff.js'; export class NotebookInsertedCellDecorator extends Disposable { private readonly decorators = this._register(new DisposableStore()); @@ -23,7 +24,14 @@ export class NotebookInsertedCellDecorator extends Disposable { const cells = diffInfo.filter(diff => diff.type === 'insert').map((diff) => model.cells[diff.modifiedCellIndex]); const ids = this.notebookEditor.deltaCellDecorations([], cells.map(cell => ({ handle: cell.handle, - options: { className: 'nb-insertHighlight', outputClassName: 'nb-insertHighlight' } + options: { + className: 'nb-insertHighlight', outputClassName: 'nb-insertHighlight', overviewRuler: { + color: overviewRulerAddedForeground, + modelRanges: [], + includeOutput: true, + position: NotebookOverviewRulerLane.Full + } + } }))); this.clear(); this.decorators.add(toDisposable(() => { diff --git a/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookModifiedCellDecorator.ts b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookModifiedCellDecorator.ts index f2993cbb42e..7475ec97b58 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookModifiedCellDecorator.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookModifiedCellDecorator.ts @@ -5,9 +5,9 @@ import { Disposable, DisposableStore, toDisposable } from '../../../../../../base/common/lifecycle.js'; import { CellDiffInfo } from '../notebookDiffViewModel.js'; -import { INotebookEditor } from '../../notebookBrowser.js'; -import { CellKind } from '../../../common/notebookCommon.js'; +import { INotebookEditor, NotebookOverviewRulerLane } from '../../notebookBrowser.js'; import { NotebookCellTextModel } from '../../../common/model/notebookCellTextModel.js'; +import { overviewRulerModifiedForeground } from '../../../../scm/common/quickDiff.js'; export class NotebookModifiedCellDecorator extends Disposable { private readonly decorators = this._register(new DisposableStore()); @@ -23,19 +23,24 @@ export class NotebookModifiedCellDecorator extends Disposable { return; } - const modifiedMarkdownCells: NotebookCellTextModel[] = []; + const modifiedCells: NotebookCellTextModel[] = []; for (const diff of diffInfo) { if (diff.type === 'modified') { const cell = model.cells[diff.modifiedCellIndex]; - if (cell.cellKind === CellKind.Markup) { - modifiedMarkdownCells.push(cell); - } + modifiedCells.push(cell); } } - const ids = this.notebookEditor.deltaCellDecorations([], modifiedMarkdownCells.map(cell => ({ + const ids = this.notebookEditor.deltaCellDecorations([], modifiedCells.map(cell => ({ handle: cell.handle, - options: { outputClassName: 'nb-insertHighlight' } + options: { + overviewRuler: { + color: overviewRulerModifiedForeground, + modelRanges: [], + includeOutput: true, + position: NotebookOverviewRulerLane.Full + } + } }))); this.clear(); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index b2cb00d955f..d92365c5049 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -486,6 +486,7 @@ export interface INotebookViewModel { getNearestVisibleCellIndexUpwards(index: number): number; getTrackedRange(id: string): ICellRange | null; setTrackedRange(id: string | null, newRange: ICellRange | null, newStickiness: TrackedRangeStickiness): string | null; + getOverviewRulerDecorations(): INotebookDeltaViewZoneDecoration[]; getSelections(): ICellRange[]; getCellIndex(cell: ICellViewModel): number; getMostRecentlyExecutedCell(): ICellViewModel | undefined; @@ -759,6 +760,8 @@ export interface INotebookEditor { changeViewZones(callback: (accessor: INotebookViewZoneChangeAccessor) => void): void; + getViewZoneLayoutInfo(id: string): { top: number; height: number } | null; + /** * Get a contribution of this editor. * @id Unique identifier of the contribution. diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 9e0b6f8cf18..efe594f2093 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -2299,6 +2299,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD changeViewZones(callback: (accessor: INotebookViewZoneChangeAccessor) => void): void { this._list.changeViewZones(callback); } + + getViewZoneLayoutInfo(id: string): { top: number; height: number } | null { + return this._list.getViewZoneLayoutInfo(id); + } //#endregion //#region Kernel/Execution diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index ff0678e8b25..ff41cc3c9c6 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -1266,6 +1266,10 @@ export class NotebookCellList extends WorkbenchList implements ID } } + getViewZoneLayoutInfo(viewZoneId: string): { height: number; top: number } | null { + return this.viewZones.getViewZoneLayoutInfo(viewZoneId); + } + // override override domFocus() { const focused = this.getFocusedElements()[0]; diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts index 94ced5b89d7..4d6ab174cf5 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts @@ -66,6 +66,7 @@ export interface INotebookCellList extends ICoordinatesConverter { revealOffsetInCenterIfOutsideViewport(offset: number): void; setHiddenAreas(_ranges: ICellRange[], triggerViewUpdate: boolean): boolean; changeViewZones(callback: (accessor: INotebookViewZoneChangeAccessor) => void): void; + getViewZoneLayoutInfo(viewZoneId: string): { height: number; top: number } | null; domElementOfElement(element: ICellViewModel): HTMLElement | null; focusView(): void; triggerScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent): void; diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts index bdfeee70b9a..56a7c1409b1 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts @@ -23,7 +23,7 @@ import { FoldingRegions } from '../../../../../editor/contrib/folding/browser/fo import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IUndoRedoService } from '../../../../../platform/undoRedo/common/undoRedo.js'; import { CellFindMatchModel } from '../contrib/find/findModel.js'; -import { CellEditState, CellFindMatchWithIndex, CellFoldingState, EditorFoldingStateDelegate, ICellModelDecorations, ICellModelDeltaDecorations, ICellViewModel, IModelDecorationsChangeAccessor, INotebookDeltaCellStatusBarItems, INotebookEditorViewState, INotebookViewCellsUpdateEvent, INotebookViewModel, INotebookDeltaDecoration, isNotebookCellDecoration } from '../notebookBrowser.js'; +import { CellEditState, CellFindMatchWithIndex, CellFoldingState, EditorFoldingStateDelegate, ICellModelDecorations, ICellModelDeltaDecorations, ICellViewModel, IModelDecorationsChangeAccessor, INotebookDeltaCellStatusBarItems, INotebookEditorViewState, INotebookViewCellsUpdateEvent, INotebookViewModel, INotebookDeltaDecoration, isNotebookCellDecoration, INotebookDeltaViewZoneDecoration } from '../notebookBrowser.js'; import { NotebookLayoutInfo, NotebookMetadataChangedEvent } from '../notebookViewEvents.js'; import { NotebookCellSelectionCollection } from './cellSelectionCollection.js'; import { CodeCellViewModel } from './codeCellViewModel.js'; @@ -189,6 +189,9 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD private _decorationIdToCellMap = new Map(); private _statusBarItemIdToCellMap = new Map(); + private _lastOverviewRulerDecorationId: number = 0; + private _overviewRulerDecorations = new Map(); + constructor( public viewType: string, private _notebook: NotebookTextModel, @@ -495,6 +498,10 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD return this._hiddenRanges; } + getOverviewRulerDecorations(): INotebookDeltaViewZoneDecoration[] { + return Array.from(this._overviewRulerDecorations.values()); + } + getCellByHandle(handle: number) { return this._handleToViewCellMapping.get(handle); } @@ -724,7 +731,9 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD this._decorationIdToCellMap.delete(id); } - // TODO@rebornix: trigger view zone decorations removal + if (this._overviewRulerDecorations.has(id)) { + this._overviewRulerDecorations.delete(id); + } }); const result: string[] = []; @@ -738,7 +747,10 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD }); result.push(...ret); } else { - // TODO@rebornix trigger view zone decorations + const id = ++this._lastOverviewRulerDecorationId; + const decorationId = `_overview_${this.id};${id}`; + this._overviewRulerDecorations.set(decorationId, decoration); + result.push(decorationId); } }); diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookOverviewRuler.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookOverviewRuler.ts index 4085b53aff2..1bab34e9d38 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookOverviewRuler.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookOverviewRuler.ts @@ -111,6 +111,45 @@ export class NotebookOverviewRuler extends Themable { currentFrom += cellHeight; } + + const overviewRulerDecorations = viewModel.getOverviewRulerDecorations(); + + for (let i = 0; i < overviewRulerDecorations.length; i++) { + const decoration = overviewRulerDecorations[i]; + if (!decoration.options.overviewRuler) { + continue; + } + const viewZoneInfo = this.notebookEditor.getViewZoneLayoutInfo(decoration.viewZoneId); + + if (!viewZoneInfo) { + continue; + } + + const fillStyle = this.getColor(decoration.options.overviewRuler.color) ?? '#000000'; + let x = 0; + switch (decoration.options.overviewRuler.position) { + case NotebookOverviewRulerLane.Left: + x = 0; + break; + case NotebookOverviewRulerLane.Center: + x = laneWidth; + break; + case NotebookOverviewRulerLane.Right: + x = laneWidth * 2; + break; + default: + break; + } + + const width = decoration.options.overviewRuler.position === NotebookOverviewRulerLane.Full ? laneWidth * 3 : laneWidth; + + ctx.fillStyle = fillStyle; + + const viewZoneHeight = (viewZoneInfo.height / scrollHeight) * ratio * height; + const viewZoneTop = (viewZoneInfo.top / scrollHeight) * ratio * height; + + ctx.fillRect(x, viewZoneTop, width, viewZoneHeight); + } } } } diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookViewZones.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookViewZones.ts index 8645d66dc19..47604ea0688 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookViewZones.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookViewZones.ts @@ -66,6 +66,16 @@ export class NotebookViewZones extends Disposable { return zonesHaveChanged; } + getViewZoneLayoutInfo(viewZoneId: string): { height: number; top: number } | null { + const zoneWidget = this._zones[viewZoneId]; + if (!zoneWidget) { + return null; + } + const top = this.listView.getWhitespacePosition(zoneWidget.whitespaceId); + const height = zoneWidget.zone.heightInPx; + return { height: height, top: top }; + } + onCellsChanged(e: INotebookViewCellsUpdateEvent): void { const splices = e.splices.slice().reverse(); splices.forEach(splice => {