Path: blob/main/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { groupBy } from '../../../../../base/common/collections.js';6import { onUnexpectedError } from '../../../../../base/common/errors.js';7import { Emitter, Event } from '../../../../../base/common/event.js';8import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';9import { clamp } from '../../../../../base/common/numbers.js';10import * as strings from '../../../../../base/common/strings.js';11import { URI } from '../../../../../base/common/uri.js';12import { IBulkEditService, ResourceTextEdit } from '../../../../../editor/browser/services/bulkEditService.js';13import { Range } from '../../../../../editor/common/core/range.js';14import * as editorCommon from '../../../../../editor/common/editorCommon.js';15import { IWorkspaceTextEdit } from '../../../../../editor/common/languages.js';16import { FindMatch, IModelDecorationOptions, IModelDeltaDecoration, TrackedRangeStickiness } from '../../../../../editor/common/model.js';17import { MultiModelEditStackElement, SingleModelEditStackElement } from '../../../../../editor/common/model/editStack.js';18import { IntervalNode, IntervalTree } from '../../../../../editor/common/model/intervalTree.js';19import { ModelDecorationOptions } from '../../../../../editor/common/model/textModel.js';20import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';21import { FoldingRegions } from '../../../../../editor/contrib/folding/browser/foldingRanges.js';22import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';23import { IUndoRedoService } from '../../../../../platform/undoRedo/common/undoRedo.js';24import { CellFindMatchModel } from '../contrib/find/findModel.js';25import { CellEditState, CellFindMatchWithIndex, CellFoldingState, EditorFoldingStateDelegate, ICellModelDecorations, ICellModelDeltaDecorations, ICellViewModel, IModelDecorationsChangeAccessor, INotebookDeltaCellStatusBarItems, INotebookEditorViewState, INotebookViewCellsUpdateEvent, INotebookViewModel, INotebookDeltaDecoration, isNotebookCellDecoration, INotebookDeltaViewZoneDecoration } from '../notebookBrowser.js';26import { NotebookLayoutInfo, NotebookMetadataChangedEvent } from '../notebookViewEvents.js';27import { NotebookCellSelectionCollection } from './cellSelectionCollection.js';28import { CodeCellViewModel } from './codeCellViewModel.js';29import { MarkupCellViewModel } from './markupCellViewModel.js';30import { ViewContext } from './viewContext.js';31import { NotebookCellTextModel } from '../../common/model/notebookCellTextModel.js';32import { NotebookTextModel } from '../../common/model/notebookTextModel.js';33import { CellKind, ICell, INotebookFindOptions, ISelectionState, NotebookCellsChangeType, NotebookCellTextModelSplice, NotebookFindScopeType, SelectionStateType } from '../../common/notebookCommon.js';34import { INotebookExecutionStateService, NotebookExecutionType } from '../../common/notebookExecutionStateService.js';35import { cellIndexesToRanges, cellRangesToIndexes, ICellRange, reduceCellRanges } from '../../common/notebookRange.js';3637const invalidFunc = () => { throw new Error(`Invalid change accessor`); };3839class DecorationsTree {40private readonly _decorationsTree: IntervalTree;4142constructor() {43this._decorationsTree = new IntervalTree();44}4546public intervalSearch(start: number, end: number, filterOwnerId: number, filterOutValidation: boolean, filterFontDecorations: boolean, cachedVersionId: number, onlyMarginDecorations: boolean = false): IntervalNode[] {47const r1 = this._decorationsTree.intervalSearch(start, end, filterOwnerId, filterOutValidation, filterFontDecorations, cachedVersionId, onlyMarginDecorations);48return r1;49}5051public search(filterOwnerId: number, filterOutValidation: boolean, filterFontDecorations: boolean, overviewRulerOnly: boolean, cachedVersionId: number, onlyMarginDecorations: boolean): IntervalNode[] {52return this._decorationsTree.search(filterOwnerId, filterOutValidation, filterFontDecorations, cachedVersionId, onlyMarginDecorations);5354}5556public collectNodesFromOwner(ownerId: number): IntervalNode[] {57const r1 = this._decorationsTree.collectNodesFromOwner(ownerId);58return r1;59}6061public collectNodesPostOrder(): IntervalNode[] {62const r1 = this._decorationsTree.collectNodesPostOrder();63return r1;64}6566public insert(node: IntervalNode): void {67this._decorationsTree.insert(node);68}6970public delete(node: IntervalNode): void {71this._decorationsTree.delete(node);72}7374public resolveNode(node: IntervalNode, cachedVersionId: number): void {75this._decorationsTree.resolveNode(node, cachedVersionId);76}7778public acceptReplace(offset: number, length: number, textLength: number, forceMoveMarkers: boolean): void {79this._decorationsTree.acceptReplace(offset, length, textLength, forceMoveMarkers);80}81}8283const TRACKED_RANGE_OPTIONS = [84ModelDecorationOptions.register({ description: 'notebook-view-model-tracked-range-always-grows-when-typing-at-edges', stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges }),85ModelDecorationOptions.register({ description: 'notebook-view-model-tracked-range-never-grows-when-typing-at-edges', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }),86ModelDecorationOptions.register({ description: 'notebook-view-model-tracked-range-grows-only-when-typing-before', stickiness: TrackedRangeStickiness.GrowsOnlyWhenTypingBefore }),87ModelDecorationOptions.register({ description: 'notebook-view-model-tracked-range-grows-only-when-typing-after', stickiness: TrackedRangeStickiness.GrowsOnlyWhenTypingAfter }),88];8990function _normalizeOptions(options: IModelDecorationOptions): ModelDecorationOptions {91if (options instanceof ModelDecorationOptions) {92return options;93}94return ModelDecorationOptions.createDynamic(options);95}9697let MODEL_ID = 0;9899export interface NotebookViewModelOptions {100isReadOnly: boolean;101}102103export class NotebookViewModel extends Disposable implements EditorFoldingStateDelegate, INotebookViewModel {104private readonly _localStore = this._register(new DisposableStore());105private _handleToViewCellMapping = new Map<number, CellViewModel>();106get options(): NotebookViewModelOptions { return this._options; }107private readonly _onDidChangeOptions = this._register(new Emitter<void>());108get onDidChangeOptions(): Event<void> { return this._onDidChangeOptions.event; }109private _viewCells: CellViewModel[] = [];110111get viewCells(): ICellViewModel[] {112return this._viewCells;113}114115get length(): number {116return this._viewCells.length;117}118119get notebookDocument() {120return this._notebook;121}122123get uri() {124return this._notebook.uri;125}126127get metadata() {128return this._notebook.metadata;129}130131private get isRepl() {132return this.viewType === 'repl';133}134135private readonly _onDidChangeViewCells = this._register(new Emitter<INotebookViewCellsUpdateEvent>());136get onDidChangeViewCells(): Event<INotebookViewCellsUpdateEvent> { return this._onDidChangeViewCells.event; }137138private _lastNotebookEditResource: URI[] = [];139140get lastNotebookEditResource(): URI | null {141if (this._lastNotebookEditResource.length) {142return this._lastNotebookEditResource[this._lastNotebookEditResource.length - 1];143}144return null;145}146147get layoutInfo(): NotebookLayoutInfo | null {148return this._layoutInfo;149}150151private readonly _onDidChangeSelection = this._register(new Emitter<string>());152get onDidChangeSelection(): Event<string> { return this._onDidChangeSelection.event; }153154private _selectionCollection = this._register(new NotebookCellSelectionCollection());155156private get selectionHandles() {157const handlesSet = new Set<number>();158const handles: number[] = [];159cellRangesToIndexes(this._selectionCollection.selections).map(index => index < this.length ? this.cellAt(index) : undefined).forEach(cell => {160if (cell && !handlesSet.has(cell.handle)) {161handles.push(cell.handle);162}163});164165return handles;166}167168private set selectionHandles(selectionHandles: number[]) {169const indexes = selectionHandles.map(handle => this._viewCells.findIndex(cell => cell.handle === handle));170this._selectionCollection.setSelections(cellIndexesToRanges(indexes), true, 'model');171}172173private _decorationsTree = new DecorationsTree();174private _decorations: { [decorationId: string]: IntervalNode } = Object.create(null);175private _lastDecorationId: number = 0;176private readonly _instanceId: string;177public readonly id: string;178private _foldingRanges: FoldingRegions | null = null;179private _onDidFoldingStateChanged = new Emitter<void>();180onDidFoldingStateChanged: Event<void> = this._onDidFoldingStateChanged.event;181private _hiddenRanges: ICellRange[] = [];182private _focused: boolean = true;183184get focused() {185return this._focused;186}187188private _decorationIdToCellMap = new Map<string, number>();189private _statusBarItemIdToCellMap = new Map<string, number>();190191private _lastOverviewRulerDecorationId: number = 0;192private _overviewRulerDecorations = new Map<string, INotebookDeltaViewZoneDecoration>();193194constructor(195public viewType: string,196private _notebook: NotebookTextModel,197private _viewContext: ViewContext,198private _layoutInfo: NotebookLayoutInfo | null,199private _options: NotebookViewModelOptions,200@IInstantiationService private readonly _instantiationService: IInstantiationService,201@IBulkEditService private readonly _bulkEditService: IBulkEditService,202@IUndoRedoService private readonly _undoService: IUndoRedoService,203@ITextModelService private readonly _textModelService: ITextModelService,204@INotebookExecutionStateService private readonly notebookExecutionStateService: INotebookExecutionStateService,205) {206super();207208MODEL_ID++;209this.id = '$notebookViewModel' + MODEL_ID;210this._instanceId = strings.singleLetterHash(MODEL_ID);211212const compute = (changes: NotebookCellTextModelSplice<ICell>[], synchronous: boolean) => {213const diffs = changes.map(splice => {214return [splice[0], splice[1], splice[2].map(cell => {215return createCellViewModel(this._instantiationService, this, cell as NotebookCellTextModel, this._viewContext);216})] as [number, number, CellViewModel[]];217});218219diffs.reverse().forEach(diff => {220const deletedCells = this._viewCells.splice(diff[0], diff[1], ...diff[2]);221222this._decorationsTree.acceptReplace(diff[0], diff[1], diff[2].length, true);223deletedCells.forEach(cell => {224this._handleToViewCellMapping.delete(cell.handle);225// dispose the cell to release ref to the cell text document226cell.dispose();227});228229diff[2].forEach(cell => {230this._handleToViewCellMapping.set(cell.handle, cell);231this._localStore.add(cell);232});233});234235const selectionHandles = this.selectionHandles;236237this._onDidChangeViewCells.fire({238synchronous: synchronous,239splices: diffs240});241242let endSelectionHandles: number[] = [];243if (selectionHandles.length) {244const primaryHandle = selectionHandles[0];245const primarySelectionIndex = this._viewCells.indexOf(this.getCellByHandle(primaryHandle)!);246endSelectionHandles = [primaryHandle];247let delta = 0;248249for (let i = 0; i < diffs.length; i++) {250const diff = diffs[0];251if (diff[0] + diff[1] <= primarySelectionIndex) {252delta += diff[2].length - diff[1];253continue;254}255256if (diff[0] > primarySelectionIndex) {257endSelectionHandles = [primaryHandle];258break;259}260261if (diff[0] + diff[1] > primarySelectionIndex) {262endSelectionHandles = [this._viewCells[diff[0] + delta].handle];263break;264}265}266}267268// TODO@rebornix269const selectionIndexes = endSelectionHandles.map(handle => this._viewCells.findIndex(cell => cell.handle === handle));270this._selectionCollection.setState(cellIndexesToRanges([selectionIndexes[0]])[0], cellIndexesToRanges(selectionIndexes), true, 'model');271};272273this._register(this._notebook.onDidChangeContent(e => {274for (let i = 0; i < e.rawEvents.length; i++) {275const change = e.rawEvents[i];276let changes: NotebookCellTextModelSplice<ICell>[] = [];277const synchronous = e.synchronous ?? true;278279if (change.kind === NotebookCellsChangeType.ModelChange || change.kind === NotebookCellsChangeType.Initialize) {280changes = change.changes;281compute(changes, synchronous);282continue;283} else if (change.kind === NotebookCellsChangeType.Move) {284compute([[change.index, change.length, []]], synchronous);285compute([[change.newIdx, 0, change.cells]], synchronous);286} else {287continue;288}289}290}));291292this._register(this._notebook.onDidChangeContent(contentChanges => {293contentChanges.rawEvents.forEach(e => {294if (e.kind === NotebookCellsChangeType.ChangeDocumentMetadata) {295this._viewContext.eventDispatcher.emit([new NotebookMetadataChangedEvent(this._notebook.metadata)]);296}297});298299if (contentChanges.endSelectionState) {300this.updateSelectionsState(contentChanges.endSelectionState);301}302}));303304this._register(this._viewContext.eventDispatcher.onDidChangeLayout((e) => {305this._layoutInfo = e.value;306307this._viewCells.forEach(cell => {308if (cell.cellKind === CellKind.Markup) {309if (e.source.width || e.source.fontInfo) {310cell.layoutChange({ outerWidth: e.value.width, font: e.value.fontInfo });311}312} else {313if (e.source.width !== undefined) {314cell.layoutChange({ outerWidth: e.value.width, font: e.value.fontInfo });315}316}317});318}));319320this._register(this._viewContext.notebookOptions.onDidChangeOptions(e => {321for (let i = 0; i < this.length; i++) {322const cell = this._viewCells[i];323cell.updateOptions(e);324}325}));326327this._register(notebookExecutionStateService.onDidChangeExecution(e => {328if (e.type !== NotebookExecutionType.cell) {329return;330}331const cell = this.getCellByHandle(e.cellHandle);332333if (cell instanceof CodeCellViewModel) {334cell.updateExecutionState(e);335}336}));337338this._register(this._selectionCollection.onDidChangeSelection(e => {339this._onDidChangeSelection.fire(e);340}));341342343const viewCellCount = this.isRepl ? this._notebook.cells.length - 1 : this._notebook.cells.length;344for (let i = 0; i < viewCellCount; i++) {345this._viewCells.push(createCellViewModel(this._instantiationService, this, this._notebook.cells[i], this._viewContext));346}347348349this._viewCells.forEach(cell => {350this._handleToViewCellMapping.set(cell.handle, cell);351});352}353354updateOptions(newOptions: Partial<NotebookViewModelOptions>) {355this._options = { ...this._options, ...newOptions };356this._viewCells.forEach(cell => cell.updateOptions({ readonly: this._options.isReadOnly }));357this._onDidChangeOptions.fire();358}359360getFocus() {361return this._selectionCollection.focus;362}363364getSelections() {365return this._selectionCollection.selections;366}367368getMostRecentlyExecutedCell(): ICellViewModel | undefined {369const handle = this.notebookExecutionStateService.getLastCompletedCellForNotebook(this._notebook.uri);370return handle !== undefined ? this.getCellByHandle(handle) : undefined;371}372373setEditorFocus(focused: boolean) {374this._focused = focused;375}376377validateRange(cellRange: ICellRange | null | undefined): ICellRange | null {378if (!cellRange) {379return null;380}381382const start = clamp(cellRange.start, 0, this.length);383const end = clamp(cellRange.end, 0, this.length);384385if (start <= end) {386return { start, end };387} else {388return { start: end, end: start };389}390}391392// selection change from list view's `setFocus` and `setSelection` should always use `source: view` to prevent events breaking the list view focus/selection change transaction393updateSelectionsState(state: ISelectionState, source: 'view' | 'model' = 'model') {394if (this._focused || source === 'model') {395if (state.kind === SelectionStateType.Handle) {396const primaryIndex = state.primary !== null ? this.getCellIndexByHandle(state.primary) : null;397const primarySelection = primaryIndex !== null ? this.validateRange({ start: primaryIndex, end: primaryIndex + 1 }) : null;398const selections = cellIndexesToRanges(state.selections.map(sel => this.getCellIndexByHandle(sel)))399.map(range => this.validateRange(range))400.filter(range => range !== null) as ICellRange[];401this._selectionCollection.setState(primarySelection, reduceCellRanges(selections), true, source);402} else {403const primarySelection = this.validateRange(state.focus);404const selections = state.selections405.map(range => this.validateRange(range))406.filter(range => range !== null) as ICellRange[];407this._selectionCollection.setState(primarySelection, reduceCellRanges(selections), true, source);408}409}410}411412getFoldingStartIndex(index: number): number {413if (!this._foldingRanges) {414return -1;415}416417const range = this._foldingRanges.findRange(index + 1);418const startIndex = this._foldingRanges.getStartLineNumber(range) - 1;419return startIndex;420}421422getFoldingState(index: number): CellFoldingState {423if (!this._foldingRanges) {424return CellFoldingState.None;425}426427const range = this._foldingRanges.findRange(index + 1);428const startIndex = this._foldingRanges.getStartLineNumber(range) - 1;429430if (startIndex !== index) {431return CellFoldingState.None;432}433434return this._foldingRanges.isCollapsed(range) ? CellFoldingState.Collapsed : CellFoldingState.Expanded;435}436437getFoldedLength(index: number): number {438if (!this._foldingRanges) {439return 0;440}441442const range = this._foldingRanges.findRange(index + 1);443const startIndex = this._foldingRanges.getStartLineNumber(range) - 1;444const endIndex = this._foldingRanges.getEndLineNumber(range) - 1;445446return endIndex - startIndex;447}448449updateFoldingRanges(ranges: FoldingRegions) {450this._foldingRanges = ranges;451let updateHiddenAreas = false;452const newHiddenAreas: ICellRange[] = [];453454let i = 0; // index into hidden455let k = 0;456457let lastCollapsedStart = Number.MAX_VALUE;458let lastCollapsedEnd = -1;459460for (; i < ranges.length; i++) {461if (!ranges.isCollapsed(i)) {462continue;463}464465const startLineNumber = ranges.getStartLineNumber(i) + 1; // the first line is not hidden466const endLineNumber = ranges.getEndLineNumber(i);467if (lastCollapsedStart <= startLineNumber && endLineNumber <= lastCollapsedEnd) {468// ignore ranges contained in collapsed regions469continue;470}471472if (!updateHiddenAreas && k < this._hiddenRanges.length && this._hiddenRanges[k].start + 1 === startLineNumber && (this._hiddenRanges[k].end + 1) === endLineNumber) {473// reuse the old ranges474newHiddenAreas.push(this._hiddenRanges[k]);475k++;476} else {477updateHiddenAreas = true;478newHiddenAreas.push({ start: startLineNumber - 1, end: endLineNumber - 1 });479}480lastCollapsedStart = startLineNumber;481lastCollapsedEnd = endLineNumber;482}483484if (updateHiddenAreas || k < this._hiddenRanges.length) {485this._hiddenRanges = newHiddenAreas;486this._onDidFoldingStateChanged.fire();487}488489this._viewCells.forEach(cell => {490if (cell.cellKind === CellKind.Markup) {491cell.triggerFoldingStateChange();492}493});494}495496getHiddenRanges() {497return this._hiddenRanges;498}499500getOverviewRulerDecorations(): INotebookDeltaViewZoneDecoration[] {501return Array.from(this._overviewRulerDecorations.values());502}503504getCellByHandle(handle: number) {505return this._handleToViewCellMapping.get(handle);506}507508getCellIndexByHandle(handle: number): number {509return this._viewCells.findIndex(cell => cell.handle === handle);510}511512getCellIndex(cell: ICellViewModel) {513return this._viewCells.indexOf(cell as CellViewModel);514}515516cellAt(index: number): CellViewModel | undefined {517// if (index < 0 || index >= this.length) {518// throw new Error(`Invalid index ${index}`);519// }520521return this._viewCells[index];522}523524getCellsInRange(range?: ICellRange): ReadonlyArray<ICellViewModel> {525if (!range) {526return this._viewCells.slice(0);527}528529const validatedRange = this.validateRange(range);530531if (validatedRange) {532const result: ICellViewModel[] = [];533534for (let i = validatedRange.start; i < validatedRange.end; i++) {535result.push(this._viewCells[i]);536}537538return result;539}540541return [];542}543544/**545* If this._viewCells[index] is visible then return index546*/547getNearestVisibleCellIndexUpwards(index: number) {548for (let i = this._hiddenRanges.length - 1; i >= 0; i--) {549const cellRange = this._hiddenRanges[i];550const foldStart = cellRange.start - 1;551const foldEnd = cellRange.end;552553if (foldStart > index) {554continue;555}556557if (foldStart <= index && foldEnd >= index) {558return index;559}560561// foldStart <= index, foldEnd < index562break;563}564565return index;566}567568getNextVisibleCellIndex(index: number) {569for (let i = 0; i < this._hiddenRanges.length; i++) {570const cellRange = this._hiddenRanges[i];571const foldStart = cellRange.start - 1;572const foldEnd = cellRange.end;573574if (foldEnd < index) {575continue;576}577578// foldEnd >= index579if (foldStart <= index) {580return foldEnd + 1;581}582583break;584}585586return index + 1;587}588589getPreviousVisibleCellIndex(index: number) {590for (let i = this._hiddenRanges.length - 1; i >= 0; i--) {591const cellRange = this._hiddenRanges[i];592const foldStart = cellRange.start - 1;593const foldEnd = cellRange.end;594595if (foldEnd < index) {596return index;597}598599if (foldStart <= index) {600return foldStart;601}602}603604return index;605}606607hasCell(cell: ICellViewModel) {608return this._handleToViewCellMapping.has(cell.handle);609}610611getVersionId() {612return this._notebook.versionId;613}614615getAlternativeId() {616return this._notebook.alternativeVersionId;617}618619getTrackedRange(id: string): ICellRange | null {620return this._getDecorationRange(id);621}622623private _getDecorationRange(decorationId: string): ICellRange | null {624const node = this._decorations[decorationId];625if (!node) {626return null;627}628const versionId = this.getVersionId();629if (node.cachedVersionId !== versionId) {630this._decorationsTree.resolveNode(node, versionId);631}632if (node.range === null) {633return { start: node.cachedAbsoluteStart - 1, end: node.cachedAbsoluteEnd - 1 };634}635636return { start: node.range.startLineNumber - 1, end: node.range.endLineNumber - 1 };637}638639setTrackedRange(id: string | null, newRange: ICellRange | null, newStickiness: TrackedRangeStickiness): string | null {640const node = (id ? this._decorations[id] : null);641642if (!node) {643if (!newRange) {644return null;645}646647return this._deltaCellDecorationsImpl(0, [], [{ range: new Range(newRange.start + 1, 1, newRange.end + 1, 1), options: TRACKED_RANGE_OPTIONS[newStickiness] }])[0];648}649650if (!newRange) {651// node exists, the request is to delete => delete node652this._decorationsTree.delete(node);653delete this._decorations[node.id];654return null;655}656657this._decorationsTree.delete(node);658node.reset(this.getVersionId(), newRange.start, newRange.end + 1, new Range(newRange.start + 1, 1, newRange.end + 1, 1));659node.setOptions(TRACKED_RANGE_OPTIONS[newStickiness]);660this._decorationsTree.insert(node);661return node.id;662}663664private _deltaCellDecorationsImpl(ownerId: number, oldDecorationsIds: string[], newDecorations: IModelDeltaDecoration[]): string[] {665const versionId = this.getVersionId();666667const oldDecorationsLen = oldDecorationsIds.length;668let oldDecorationIndex = 0;669670const newDecorationsLen = newDecorations.length;671let newDecorationIndex = 0;672673const result = new Array<string>(newDecorationsLen);674while (oldDecorationIndex < oldDecorationsLen || newDecorationIndex < newDecorationsLen) {675676let node: IntervalNode | null = null;677678if (oldDecorationIndex < oldDecorationsLen) {679// (1) get ourselves an old node680do {681node = this._decorations[oldDecorationsIds[oldDecorationIndex++]];682} while (!node && oldDecorationIndex < oldDecorationsLen);683684// (2) remove the node from the tree (if it exists)685if (node) {686this._decorationsTree.delete(node);687}688}689690if (newDecorationIndex < newDecorationsLen) {691// (3) create a new node if necessary692if (!node) {693const internalDecorationId = (++this._lastDecorationId);694const decorationId = `${this._instanceId};${internalDecorationId}`;695node = new IntervalNode(decorationId, 0, 0);696this._decorations[decorationId] = node;697}698699// (4) initialize node700const newDecoration = newDecorations[newDecorationIndex];701const range = newDecoration.range;702const options = _normalizeOptions(newDecoration.options);703704node.ownerId = ownerId;705node.reset(versionId, range.startLineNumber, range.endLineNumber, Range.lift(range));706node.setOptions(options);707708this._decorationsTree.insert(node);709710result[newDecorationIndex] = node.id;711712newDecorationIndex++;713} else {714if (node) {715delete this._decorations[node.id];716}717}718}719720return result;721}722723deltaCellDecorations(oldDecorations: string[], newDecorations: INotebookDeltaDecoration[]): string[] {724oldDecorations.forEach(id => {725const handle = this._decorationIdToCellMap.get(id);726727if (handle !== undefined) {728const cell = this.getCellByHandle(handle);729cell?.deltaCellDecorations([id], []);730this._decorationIdToCellMap.delete(id);731}732733if (this._overviewRulerDecorations.has(id)) {734this._overviewRulerDecorations.delete(id);735}736});737738const result: string[] = [];739740newDecorations.forEach(decoration => {741if (isNotebookCellDecoration(decoration)) {742const cell = this.getCellByHandle(decoration.handle);743const ret = cell?.deltaCellDecorations([], [decoration.options]) || [];744ret.forEach(id => {745this._decorationIdToCellMap.set(id, decoration.handle);746});747result.push(...ret);748} else {749const id = ++this._lastOverviewRulerDecorationId;750const decorationId = `_overview_${this.id};${id}`;751this._overviewRulerDecorations.set(decorationId, decoration);752result.push(decorationId);753}754755});756757return result;758}759760deltaCellStatusBarItems(oldItems: string[], newItems: INotebookDeltaCellStatusBarItems[]): string[] {761const deletesByHandle = groupBy(oldItems, id => this._statusBarItemIdToCellMap.get(id) ?? -1);762763const result: string[] = [];764newItems.forEach(itemDelta => {765const cell = this.getCellByHandle(itemDelta.handle);766const deleted = deletesByHandle[itemDelta.handle] ?? [];767delete deletesByHandle[itemDelta.handle];768deleted.forEach(id => this._statusBarItemIdToCellMap.delete(id));769770const ret = cell?.deltaCellStatusBarItems(deleted, itemDelta.items) || [];771ret.forEach(id => {772this._statusBarItemIdToCellMap.set(id, itemDelta.handle);773});774775result.push(...ret);776});777778for (const _handle in deletesByHandle) {779const handle = parseInt(_handle);780const ids = deletesByHandle[handle];781const cell = this.getCellByHandle(handle);782cell?.deltaCellStatusBarItems(ids, []);783ids.forEach(id => this._statusBarItemIdToCellMap.delete(id));784}785786return result;787}788789nearestCodeCellIndex(index: number /* exclusive */) {790const nearest = this.viewCells.slice(0, index).reverse().findIndex(cell => cell.cellKind === CellKind.Code);791if (nearest > -1) {792return index - nearest - 1;793} else {794const nearestCellTheOtherDirection = this.viewCells.slice(index + 1).findIndex(cell => cell.cellKind === CellKind.Code);795if (nearestCellTheOtherDirection > -1) {796return index + 1 + nearestCellTheOtherDirection;797}798return -1;799}800}801802getEditorViewState(): INotebookEditorViewState {803const editingCells: { [key: number]: boolean } = {};804const collapsedInputCells: { [key: number]: boolean } = {};805const collapsedOutputCells: { [key: number]: boolean } = {};806const cellLineNumberStates: { [key: number]: 'on' | 'off' } = {};807808this._viewCells.forEach((cell, i) => {809if (cell.getEditState() === CellEditState.Editing) {810editingCells[i] = true;811}812813if (cell.isInputCollapsed) {814collapsedInputCells[i] = true;815}816817if (cell instanceof CodeCellViewModel && cell.isOutputCollapsed) {818collapsedOutputCells[i] = true;819}820821if (cell.lineNumbers !== 'inherit') {822cellLineNumberStates[i] = cell.lineNumbers;823}824});825const editorViewStates: { [key: number]: editorCommon.ICodeEditorViewState } = {};826this._viewCells.map(cell => ({ handle: cell.model.handle, state: cell.saveEditorViewState() })).forEach((viewState, i) => {827if (viewState.state) {828editorViewStates[i] = viewState.state;829}830});831832return {833editingCells,834editorViewStates,835cellLineNumberStates,836collapsedInputCells,837collapsedOutputCells838};839}840841restoreEditorViewState(viewState: INotebookEditorViewState | undefined): void {842if (!viewState) {843return;844}845846this._viewCells.forEach((cell, index) => {847const isEditing = viewState.editingCells && viewState.editingCells[index];848const editorViewState = viewState.editorViewStates && viewState.editorViewStates[index];849850cell.updateEditState(isEditing ? CellEditState.Editing : CellEditState.Preview, 'viewState');851const cellHeight = viewState.cellTotalHeights ? viewState.cellTotalHeights[index] : undefined;852cell.restoreEditorViewState(editorViewState, cellHeight);853if (viewState.collapsedInputCells && viewState.collapsedInputCells[index]) {854cell.isInputCollapsed = true;855}856if (viewState.collapsedOutputCells && viewState.collapsedOutputCells[index] && cell instanceof CodeCellViewModel) {857cell.isOutputCollapsed = true;858}859if (viewState.cellLineNumberStates && viewState.cellLineNumberStates[index]) {860cell.lineNumbers = viewState.cellLineNumberStates[index];861}862});863}864865/**866* Editor decorations across cells. For example, find decorations for multiple code cells867* The reason that we can't completely delegate this to CodeEditorWidget is most of the time, the editors for cells are not created yet but we already have decorations for them.868*/869changeModelDecorations<T>(callback: (changeAccessor: IModelDecorationsChangeAccessor) => T): T | null {870const changeAccessor: IModelDecorationsChangeAccessor = {871deltaDecorations: (oldDecorations: ICellModelDecorations[], newDecorations: ICellModelDeltaDecorations[]): ICellModelDecorations[] => {872return this._deltaModelDecorationsImpl(oldDecorations, newDecorations);873}874};875876let result: T | null = null;877try {878result = callback(changeAccessor);879} catch (e) {880onUnexpectedError(e);881}882883changeAccessor.deltaDecorations = invalidFunc;884885return result;886}887888private _deltaModelDecorationsImpl(oldDecorations: ICellModelDecorations[], newDecorations: ICellModelDeltaDecorations[]): ICellModelDecorations[] {889890const mapping = new Map<number, { cell: CellViewModel; oldDecorations: readonly string[]; newDecorations: readonly IModelDeltaDecoration[] }>();891oldDecorations.forEach(oldDecoration => {892const ownerId = oldDecoration.ownerId;893894if (!mapping.has(ownerId)) {895const cell = this._viewCells.find(cell => cell.handle === ownerId);896if (cell) {897mapping.set(ownerId, { cell: cell, oldDecorations: [], newDecorations: [] });898}899}900901const data = mapping.get(ownerId)!;902if (data) {903data.oldDecorations = oldDecoration.decorations;904}905});906907newDecorations.forEach(newDecoration => {908const ownerId = newDecoration.ownerId;909910if (!mapping.has(ownerId)) {911const cell = this._viewCells.find(cell => cell.handle === ownerId);912913if (cell) {914mapping.set(ownerId, { cell: cell, oldDecorations: [], newDecorations: [] });915}916}917918const data = mapping.get(ownerId)!;919if (data) {920data.newDecorations = newDecoration.decorations;921}922});923924const ret: ICellModelDecorations[] = [];925mapping.forEach((value, ownerId) => {926const cellRet = value.cell.deltaModelDecorations(value.oldDecorations, value.newDecorations);927ret.push({928ownerId: ownerId,929decorations: cellRet930});931});932933return ret;934}935936//#region Find937find(value: string, options: INotebookFindOptions): CellFindMatchWithIndex[] {938const matches: CellFindMatchWithIndex[] = [];939let findCells: CellViewModel[] = [];940941if (options.findScope && (options.findScope.findScopeType === NotebookFindScopeType.Cells || options.findScope.findScopeType === NotebookFindScopeType.Text)) {942const selectedRanges = options.findScope.selectedCellRanges?.map(range => this.validateRange(range)).filter(range => !!range) ?? [];943const selectedIndexes = cellRangesToIndexes(selectedRanges);944findCells = selectedIndexes.map(index => this._viewCells[index]);945} else {946findCells = this._viewCells;947}948949findCells.forEach((cell, index) => {950const cellMatches = cell.startFind(value, options);951if (cellMatches) {952matches.push(new CellFindMatchModel(953cellMatches.cell,954index,955cellMatches.contentMatches,956[]957));958}959});960961// filter based on options and editing state962963return matches.filter(match => {964if (match.cell.cellKind === CellKind.Code) {965// code cell, we only include its match if include input is enabled966return options.includeCodeInput;967}968969// markup cell, it depends on the editing state970if (match.cell.getEditState() === CellEditState.Editing) {971// editing, even if we includeMarkupPreview972return options.includeMarkupInput;973} else {974// cell in preview mode, we should only include it if includeMarkupPreview is false but includeMarkupInput is true975// if includeMarkupPreview is true, then we should include the webview match result other than this976return !options.includeMarkupPreview && options.includeMarkupInput;977}978}979);980}981982replaceOne(cell: ICellViewModel, range: Range, text: string): Promise<void> {983const viewCell = cell as CellViewModel;984this._lastNotebookEditResource.push(viewCell.uri);985return viewCell.resolveTextModel().then(() => {986this._bulkEditService.apply(987[new ResourceTextEdit(cell.uri, { range, text })],988{ quotableLabel: 'Notebook Replace' }989);990});991}992993async replaceAll(matches: CellFindMatchWithIndex[], texts: string[]): Promise<void> {994if (!matches.length) {995return;996}997998const textEdits: IWorkspaceTextEdit[] = [];999this._lastNotebookEditResource.push(matches[0].cell.uri);10001001matches.forEach(match => {1002match.contentMatches.forEach((singleMatch, index) => {1003textEdits.push({1004versionId: undefined,1005textEdit: { range: (singleMatch as FindMatch).range, text: texts[index] },1006resource: match.cell.uri1007});1008});1009});10101011return Promise.all(matches.map(match => {1012return match.cell.resolveTextModel();1013})).then(async () => {1014this._bulkEditService.apply({ edits: textEdits }, { quotableLabel: 'Notebook Replace All' });1015return;1016});1017}10181019//#endregion10201021//#region Undo/Redo10221023private async _withElement(element: SingleModelEditStackElement | MultiModelEditStackElement, callback: () => Promise<void>) {1024const viewCells = this._viewCells.filter(cell => element.matchesResource(cell.uri));1025const refs = await Promise.all(viewCells.map(cell => this._textModelService.createModelReference(cell.uri)));1026await callback();1027refs.forEach(ref => ref.dispose());1028}10291030async undo() {10311032const editStack = this._undoService.getElements(this.uri);1033const element = editStack.past.length ? editStack.past[editStack.past.length - 1] : undefined;10341035if (element && element instanceof SingleModelEditStackElement || element instanceof MultiModelEditStackElement) {1036await this._withElement(element, async () => {1037await this._undoService.undo(this.uri);1038});10391040return (element instanceof SingleModelEditStackElement) ? [element.resource] : element.resources;1041}10421043await this._undoService.undo(this.uri);1044return [];1045}10461047async redo() {10481049const editStack = this._undoService.getElements(this.uri);1050const element = editStack.future[0];10511052if (element && element instanceof SingleModelEditStackElement || element instanceof MultiModelEditStackElement) {1053await this._withElement(element, async () => {1054await this._undoService.redo(this.uri);1055});10561057return (element instanceof SingleModelEditStackElement) ? [element.resource] : element.resources;1058}10591060await this._undoService.redo(this.uri);10611062return [];1063}10641065//#endregion10661067equal(notebook: NotebookTextModel) {1068return this._notebook === notebook;1069}10701071override dispose() {1072this._localStore.clear();1073this._viewCells.forEach(cell => {1074cell.dispose();1075});10761077super.dispose();1078}1079}10801081export type CellViewModel = (CodeCellViewModel | MarkupCellViewModel) & ICellViewModel;10821083export function createCellViewModel(instantiationService: IInstantiationService, notebookViewModel: NotebookViewModel, cell: NotebookCellTextModel, viewContext: ViewContext) {1084if (cell.cellKind === CellKind.Code) {1085return instantiationService.createInstance(CodeCellViewModel, notebookViewModel.viewType, cell, notebookViewModel.layoutInfo, viewContext);1086} else {1087return instantiationService.createInstance(MarkupCellViewModel, notebookViewModel.viewType, cell, notebookViewModel.layoutInfo, notebookViewModel, viewContext);1088}1089}109010911092