Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { groupBy } from '../../../../../base/common/collections.js';
7
import { onUnexpectedError } from '../../../../../base/common/errors.js';
8
import { Emitter, Event } from '../../../../../base/common/event.js';
9
import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';
10
import { clamp } from '../../../../../base/common/numbers.js';
11
import * as strings from '../../../../../base/common/strings.js';
12
import { URI } from '../../../../../base/common/uri.js';
13
import { IBulkEditService, ResourceTextEdit } from '../../../../../editor/browser/services/bulkEditService.js';
14
import { Range } from '../../../../../editor/common/core/range.js';
15
import * as editorCommon from '../../../../../editor/common/editorCommon.js';
16
import { IWorkspaceTextEdit } from '../../../../../editor/common/languages.js';
17
import { FindMatch, IModelDecorationOptions, IModelDeltaDecoration, TrackedRangeStickiness } from '../../../../../editor/common/model.js';
18
import { MultiModelEditStackElement, SingleModelEditStackElement } from '../../../../../editor/common/model/editStack.js';
19
import { IntervalNode, IntervalTree } from '../../../../../editor/common/model/intervalTree.js';
20
import { ModelDecorationOptions } from '../../../../../editor/common/model/textModel.js';
21
import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';
22
import { FoldingRegions } from '../../../../../editor/contrib/folding/browser/foldingRanges.js';
23
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
24
import { IUndoRedoService } from '../../../../../platform/undoRedo/common/undoRedo.js';
25
import { CellFindMatchModel } from '../contrib/find/findModel.js';
26
import { CellEditState, CellFindMatchWithIndex, CellFoldingState, EditorFoldingStateDelegate, ICellModelDecorations, ICellModelDeltaDecorations, ICellViewModel, IModelDecorationsChangeAccessor, INotebookDeltaCellStatusBarItems, INotebookEditorViewState, INotebookViewCellsUpdateEvent, INotebookViewModel, INotebookDeltaDecoration, isNotebookCellDecoration, INotebookDeltaViewZoneDecoration } from '../notebookBrowser.js';
27
import { NotebookLayoutInfo, NotebookMetadataChangedEvent } from '../notebookViewEvents.js';
28
import { NotebookCellSelectionCollection } from './cellSelectionCollection.js';
29
import { CodeCellViewModel } from './codeCellViewModel.js';
30
import { MarkupCellViewModel } from './markupCellViewModel.js';
31
import { ViewContext } from './viewContext.js';
32
import { NotebookCellTextModel } from '../../common/model/notebookCellTextModel.js';
33
import { NotebookTextModel } from '../../common/model/notebookTextModel.js';
34
import { CellKind, ICell, INotebookFindOptions, ISelectionState, NotebookCellsChangeType, NotebookCellTextModelSplice, NotebookFindScopeType, SelectionStateType } from '../../common/notebookCommon.js';
35
import { INotebookExecutionStateService, NotebookExecutionType } from '../../common/notebookExecutionStateService.js';
36
import { cellIndexesToRanges, cellRangesToIndexes, ICellRange, reduceCellRanges } from '../../common/notebookRange.js';
37
38
const invalidFunc = () => { throw new Error(`Invalid change accessor`); };
39
40
class DecorationsTree {
41
private readonly _decorationsTree: IntervalTree;
42
43
constructor() {
44
this._decorationsTree = new IntervalTree();
45
}
46
47
public intervalSearch(start: number, end: number, filterOwnerId: number, filterOutValidation: boolean, filterFontDecorations: boolean, cachedVersionId: number, onlyMarginDecorations: boolean = false): IntervalNode[] {
48
const r1 = this._decorationsTree.intervalSearch(start, end, filterOwnerId, filterOutValidation, filterFontDecorations, cachedVersionId, onlyMarginDecorations);
49
return r1;
50
}
51
52
public search(filterOwnerId: number, filterOutValidation: boolean, filterFontDecorations: boolean, overviewRulerOnly: boolean, cachedVersionId: number, onlyMarginDecorations: boolean): IntervalNode[] {
53
return this._decorationsTree.search(filterOwnerId, filterOutValidation, filterFontDecorations, cachedVersionId, onlyMarginDecorations);
54
55
}
56
57
public collectNodesFromOwner(ownerId: number): IntervalNode[] {
58
const r1 = this._decorationsTree.collectNodesFromOwner(ownerId);
59
return r1;
60
}
61
62
public collectNodesPostOrder(): IntervalNode[] {
63
const r1 = this._decorationsTree.collectNodesPostOrder();
64
return r1;
65
}
66
67
public insert(node: IntervalNode): void {
68
this._decorationsTree.insert(node);
69
}
70
71
public delete(node: IntervalNode): void {
72
this._decorationsTree.delete(node);
73
}
74
75
public resolveNode(node: IntervalNode, cachedVersionId: number): void {
76
this._decorationsTree.resolveNode(node, cachedVersionId);
77
}
78
79
public acceptReplace(offset: number, length: number, textLength: number, forceMoveMarkers: boolean): void {
80
this._decorationsTree.acceptReplace(offset, length, textLength, forceMoveMarkers);
81
}
82
}
83
84
const TRACKED_RANGE_OPTIONS = [
85
ModelDecorationOptions.register({ description: 'notebook-view-model-tracked-range-always-grows-when-typing-at-edges', stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges }),
86
ModelDecorationOptions.register({ description: 'notebook-view-model-tracked-range-never-grows-when-typing-at-edges', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }),
87
ModelDecorationOptions.register({ description: 'notebook-view-model-tracked-range-grows-only-when-typing-before', stickiness: TrackedRangeStickiness.GrowsOnlyWhenTypingBefore }),
88
ModelDecorationOptions.register({ description: 'notebook-view-model-tracked-range-grows-only-when-typing-after', stickiness: TrackedRangeStickiness.GrowsOnlyWhenTypingAfter }),
89
];
90
91
function _normalizeOptions(options: IModelDecorationOptions): ModelDecorationOptions {
92
if (options instanceof ModelDecorationOptions) {
93
return options;
94
}
95
return ModelDecorationOptions.createDynamic(options);
96
}
97
98
let MODEL_ID = 0;
99
100
export interface NotebookViewModelOptions {
101
isReadOnly: boolean;
102
}
103
104
export class NotebookViewModel extends Disposable implements EditorFoldingStateDelegate, INotebookViewModel {
105
private readonly _localStore = this._register(new DisposableStore());
106
private _handleToViewCellMapping = new Map<number, CellViewModel>();
107
get options(): NotebookViewModelOptions { return this._options; }
108
private readonly _onDidChangeOptions = this._register(new Emitter<void>());
109
get onDidChangeOptions(): Event<void> { return this._onDidChangeOptions.event; }
110
private _viewCells: CellViewModel[] = [];
111
112
get viewCells(): ICellViewModel[] {
113
return this._viewCells;
114
}
115
116
get length(): number {
117
return this._viewCells.length;
118
}
119
120
get notebookDocument() {
121
return this._notebook;
122
}
123
124
get uri() {
125
return this._notebook.uri;
126
}
127
128
get metadata() {
129
return this._notebook.metadata;
130
}
131
132
private get isRepl() {
133
return this.viewType === 'repl';
134
}
135
136
private readonly _onDidChangeViewCells = this._register(new Emitter<INotebookViewCellsUpdateEvent>());
137
get onDidChangeViewCells(): Event<INotebookViewCellsUpdateEvent> { return this._onDidChangeViewCells.event; }
138
139
private _lastNotebookEditResource: URI[] = [];
140
141
get lastNotebookEditResource(): URI | null {
142
if (this._lastNotebookEditResource.length) {
143
return this._lastNotebookEditResource[this._lastNotebookEditResource.length - 1];
144
}
145
return null;
146
}
147
148
get layoutInfo(): NotebookLayoutInfo | null {
149
return this._layoutInfo;
150
}
151
152
private readonly _onDidChangeSelection = this._register(new Emitter<string>());
153
get onDidChangeSelection(): Event<string> { return this._onDidChangeSelection.event; }
154
155
private _selectionCollection = this._register(new NotebookCellSelectionCollection());
156
157
private get selectionHandles() {
158
const handlesSet = new Set<number>();
159
const handles: number[] = [];
160
cellRangesToIndexes(this._selectionCollection.selections).map(index => index < this.length ? this.cellAt(index) : undefined).forEach(cell => {
161
if (cell && !handlesSet.has(cell.handle)) {
162
handles.push(cell.handle);
163
}
164
});
165
166
return handles;
167
}
168
169
private set selectionHandles(selectionHandles: number[]) {
170
const indexes = selectionHandles.map(handle => this._viewCells.findIndex(cell => cell.handle === handle));
171
this._selectionCollection.setSelections(cellIndexesToRanges(indexes), true, 'model');
172
}
173
174
private _decorationsTree = new DecorationsTree();
175
private _decorations: { [decorationId: string]: IntervalNode } = Object.create(null);
176
private _lastDecorationId: number = 0;
177
private readonly _instanceId: string;
178
public readonly id: string;
179
private _foldingRanges: FoldingRegions | null = null;
180
private _onDidFoldingStateChanged = new Emitter<void>();
181
onDidFoldingStateChanged: Event<void> = this._onDidFoldingStateChanged.event;
182
private _hiddenRanges: ICellRange[] = [];
183
private _focused: boolean = true;
184
185
get focused() {
186
return this._focused;
187
}
188
189
private _decorationIdToCellMap = new Map<string, number>();
190
private _statusBarItemIdToCellMap = new Map<string, number>();
191
192
private _lastOverviewRulerDecorationId: number = 0;
193
private _overviewRulerDecorations = new Map<string, INotebookDeltaViewZoneDecoration>();
194
195
constructor(
196
public viewType: string,
197
private _notebook: NotebookTextModel,
198
private _viewContext: ViewContext,
199
private _layoutInfo: NotebookLayoutInfo | null,
200
private _options: NotebookViewModelOptions,
201
@IInstantiationService private readonly _instantiationService: IInstantiationService,
202
@IBulkEditService private readonly _bulkEditService: IBulkEditService,
203
@IUndoRedoService private readonly _undoService: IUndoRedoService,
204
@ITextModelService private readonly _textModelService: ITextModelService,
205
@INotebookExecutionStateService private readonly notebookExecutionStateService: INotebookExecutionStateService,
206
) {
207
super();
208
209
MODEL_ID++;
210
this.id = '$notebookViewModel' + MODEL_ID;
211
this._instanceId = strings.singleLetterHash(MODEL_ID);
212
213
const compute = (changes: NotebookCellTextModelSplice<ICell>[], synchronous: boolean) => {
214
const diffs = changes.map(splice => {
215
return [splice[0], splice[1], splice[2].map(cell => {
216
return createCellViewModel(this._instantiationService, this, cell as NotebookCellTextModel, this._viewContext);
217
})] as [number, number, CellViewModel[]];
218
});
219
220
diffs.reverse().forEach(diff => {
221
const deletedCells = this._viewCells.splice(diff[0], diff[1], ...diff[2]);
222
223
this._decorationsTree.acceptReplace(diff[0], diff[1], diff[2].length, true);
224
deletedCells.forEach(cell => {
225
this._handleToViewCellMapping.delete(cell.handle);
226
// dispose the cell to release ref to the cell text document
227
cell.dispose();
228
});
229
230
diff[2].forEach(cell => {
231
this._handleToViewCellMapping.set(cell.handle, cell);
232
this._localStore.add(cell);
233
});
234
});
235
236
const selectionHandles = this.selectionHandles;
237
238
this._onDidChangeViewCells.fire({
239
synchronous: synchronous,
240
splices: diffs
241
});
242
243
let endSelectionHandles: number[] = [];
244
if (selectionHandles.length) {
245
const primaryHandle = selectionHandles[0];
246
const primarySelectionIndex = this._viewCells.indexOf(this.getCellByHandle(primaryHandle)!);
247
endSelectionHandles = [primaryHandle];
248
let delta = 0;
249
250
for (let i = 0; i < diffs.length; i++) {
251
const diff = diffs[0];
252
if (diff[0] + diff[1] <= primarySelectionIndex) {
253
delta += diff[2].length - diff[1];
254
continue;
255
}
256
257
if (diff[0] > primarySelectionIndex) {
258
endSelectionHandles = [primaryHandle];
259
break;
260
}
261
262
if (diff[0] + diff[1] > primarySelectionIndex) {
263
endSelectionHandles = [this._viewCells[diff[0] + delta].handle];
264
break;
265
}
266
}
267
}
268
269
// TODO@rebornix
270
const selectionIndexes = endSelectionHandles.map(handle => this._viewCells.findIndex(cell => cell.handle === handle));
271
this._selectionCollection.setState(cellIndexesToRanges([selectionIndexes[0]])[0], cellIndexesToRanges(selectionIndexes), true, 'model');
272
};
273
274
this._register(this._notebook.onDidChangeContent(e => {
275
for (let i = 0; i < e.rawEvents.length; i++) {
276
const change = e.rawEvents[i];
277
let changes: NotebookCellTextModelSplice<ICell>[] = [];
278
const synchronous = e.synchronous ?? true;
279
280
if (change.kind === NotebookCellsChangeType.ModelChange || change.kind === NotebookCellsChangeType.Initialize) {
281
changes = change.changes;
282
compute(changes, synchronous);
283
continue;
284
} else if (change.kind === NotebookCellsChangeType.Move) {
285
compute([[change.index, change.length, []]], synchronous);
286
compute([[change.newIdx, 0, change.cells]], synchronous);
287
} else {
288
continue;
289
}
290
}
291
}));
292
293
this._register(this._notebook.onDidChangeContent(contentChanges => {
294
contentChanges.rawEvents.forEach(e => {
295
if (e.kind === NotebookCellsChangeType.ChangeDocumentMetadata) {
296
this._viewContext.eventDispatcher.emit([new NotebookMetadataChangedEvent(this._notebook.metadata)]);
297
}
298
});
299
300
if (contentChanges.endSelectionState) {
301
this.updateSelectionsState(contentChanges.endSelectionState);
302
}
303
}));
304
305
this._register(this._viewContext.eventDispatcher.onDidChangeLayout((e) => {
306
this._layoutInfo = e.value;
307
308
this._viewCells.forEach(cell => {
309
if (cell.cellKind === CellKind.Markup) {
310
if (e.source.width || e.source.fontInfo) {
311
cell.layoutChange({ outerWidth: e.value.width, font: e.value.fontInfo });
312
}
313
} else {
314
if (e.source.width !== undefined) {
315
cell.layoutChange({ outerWidth: e.value.width, font: e.value.fontInfo });
316
}
317
}
318
});
319
}));
320
321
this._register(this._viewContext.notebookOptions.onDidChangeOptions(e => {
322
for (let i = 0; i < this.length; i++) {
323
const cell = this._viewCells[i];
324
cell.updateOptions(e);
325
}
326
}));
327
328
this._register(notebookExecutionStateService.onDidChangeExecution(e => {
329
if (e.type !== NotebookExecutionType.cell) {
330
return;
331
}
332
const cell = this.getCellByHandle(e.cellHandle);
333
334
if (cell instanceof CodeCellViewModel) {
335
cell.updateExecutionState(e);
336
}
337
}));
338
339
this._register(this._selectionCollection.onDidChangeSelection(e => {
340
this._onDidChangeSelection.fire(e);
341
}));
342
343
344
const viewCellCount = this.isRepl ? this._notebook.cells.length - 1 : this._notebook.cells.length;
345
for (let i = 0; i < viewCellCount; i++) {
346
this._viewCells.push(createCellViewModel(this._instantiationService, this, this._notebook.cells[i], this._viewContext));
347
}
348
349
350
this._viewCells.forEach(cell => {
351
this._handleToViewCellMapping.set(cell.handle, cell);
352
});
353
}
354
355
updateOptions(newOptions: Partial<NotebookViewModelOptions>) {
356
this._options = { ...this._options, ...newOptions };
357
this._viewCells.forEach(cell => cell.updateOptions({ readonly: this._options.isReadOnly }));
358
this._onDidChangeOptions.fire();
359
}
360
361
getFocus() {
362
return this._selectionCollection.focus;
363
}
364
365
getSelections() {
366
return this._selectionCollection.selections;
367
}
368
369
getMostRecentlyExecutedCell(): ICellViewModel | undefined {
370
const handle = this.notebookExecutionStateService.getLastCompletedCellForNotebook(this._notebook.uri);
371
return handle !== undefined ? this.getCellByHandle(handle) : undefined;
372
}
373
374
setEditorFocus(focused: boolean) {
375
this._focused = focused;
376
}
377
378
validateRange(cellRange: ICellRange | null | undefined): ICellRange | null {
379
if (!cellRange) {
380
return null;
381
}
382
383
const start = clamp(cellRange.start, 0, this.length);
384
const end = clamp(cellRange.end, 0, this.length);
385
386
if (start <= end) {
387
return { start, end };
388
} else {
389
return { start: end, end: start };
390
}
391
}
392
393
// selection change from list view's `setFocus` and `setSelection` should always use `source: view` to prevent events breaking the list view focus/selection change transaction
394
updateSelectionsState(state: ISelectionState, source: 'view' | 'model' = 'model') {
395
if (this._focused || source === 'model') {
396
if (state.kind === SelectionStateType.Handle) {
397
const primaryIndex = state.primary !== null ? this.getCellIndexByHandle(state.primary) : null;
398
const primarySelection = primaryIndex !== null ? this.validateRange({ start: primaryIndex, end: primaryIndex + 1 }) : null;
399
const selections = cellIndexesToRanges(state.selections.map(sel => this.getCellIndexByHandle(sel)))
400
.map(range => this.validateRange(range))
401
.filter(range => range !== null) as ICellRange[];
402
this._selectionCollection.setState(primarySelection, reduceCellRanges(selections), true, source);
403
} else {
404
const primarySelection = this.validateRange(state.focus);
405
const selections = state.selections
406
.map(range => this.validateRange(range))
407
.filter(range => range !== null) as ICellRange[];
408
this._selectionCollection.setState(primarySelection, reduceCellRanges(selections), true, source);
409
}
410
}
411
}
412
413
getFoldingStartIndex(index: number): number {
414
if (!this._foldingRanges) {
415
return -1;
416
}
417
418
const range = this._foldingRanges.findRange(index + 1);
419
const startIndex = this._foldingRanges.getStartLineNumber(range) - 1;
420
return startIndex;
421
}
422
423
getFoldingState(index: number): CellFoldingState {
424
if (!this._foldingRanges) {
425
return CellFoldingState.None;
426
}
427
428
const range = this._foldingRanges.findRange(index + 1);
429
const startIndex = this._foldingRanges.getStartLineNumber(range) - 1;
430
431
if (startIndex !== index) {
432
return CellFoldingState.None;
433
}
434
435
return this._foldingRanges.isCollapsed(range) ? CellFoldingState.Collapsed : CellFoldingState.Expanded;
436
}
437
438
getFoldedLength(index: number): number {
439
if (!this._foldingRanges) {
440
return 0;
441
}
442
443
const range = this._foldingRanges.findRange(index + 1);
444
const startIndex = this._foldingRanges.getStartLineNumber(range) - 1;
445
const endIndex = this._foldingRanges.getEndLineNumber(range) - 1;
446
447
return endIndex - startIndex;
448
}
449
450
updateFoldingRanges(ranges: FoldingRegions) {
451
this._foldingRanges = ranges;
452
let updateHiddenAreas = false;
453
const newHiddenAreas: ICellRange[] = [];
454
455
let i = 0; // index into hidden
456
let k = 0;
457
458
let lastCollapsedStart = Number.MAX_VALUE;
459
let lastCollapsedEnd = -1;
460
461
for (; i < ranges.length; i++) {
462
if (!ranges.isCollapsed(i)) {
463
continue;
464
}
465
466
const startLineNumber = ranges.getStartLineNumber(i) + 1; // the first line is not hidden
467
const endLineNumber = ranges.getEndLineNumber(i);
468
if (lastCollapsedStart <= startLineNumber && endLineNumber <= lastCollapsedEnd) {
469
// ignore ranges contained in collapsed regions
470
continue;
471
}
472
473
if (!updateHiddenAreas && k < this._hiddenRanges.length && this._hiddenRanges[k].start + 1 === startLineNumber && (this._hiddenRanges[k].end + 1) === endLineNumber) {
474
// reuse the old ranges
475
newHiddenAreas.push(this._hiddenRanges[k]);
476
k++;
477
} else {
478
updateHiddenAreas = true;
479
newHiddenAreas.push({ start: startLineNumber - 1, end: endLineNumber - 1 });
480
}
481
lastCollapsedStart = startLineNumber;
482
lastCollapsedEnd = endLineNumber;
483
}
484
485
if (updateHiddenAreas || k < this._hiddenRanges.length) {
486
this._hiddenRanges = newHiddenAreas;
487
this._onDidFoldingStateChanged.fire();
488
}
489
490
this._viewCells.forEach(cell => {
491
if (cell.cellKind === CellKind.Markup) {
492
cell.triggerFoldingStateChange();
493
}
494
});
495
}
496
497
getHiddenRanges() {
498
return this._hiddenRanges;
499
}
500
501
getOverviewRulerDecorations(): INotebookDeltaViewZoneDecoration[] {
502
return Array.from(this._overviewRulerDecorations.values());
503
}
504
505
getCellByHandle(handle: number) {
506
return this._handleToViewCellMapping.get(handle);
507
}
508
509
getCellIndexByHandle(handle: number): number {
510
return this._viewCells.findIndex(cell => cell.handle === handle);
511
}
512
513
getCellIndex(cell: ICellViewModel) {
514
return this._viewCells.indexOf(cell as CellViewModel);
515
}
516
517
cellAt(index: number): CellViewModel | undefined {
518
// if (index < 0 || index >= this.length) {
519
// throw new Error(`Invalid index ${index}`);
520
// }
521
522
return this._viewCells[index];
523
}
524
525
getCellsInRange(range?: ICellRange): ReadonlyArray<ICellViewModel> {
526
if (!range) {
527
return this._viewCells.slice(0);
528
}
529
530
const validatedRange = this.validateRange(range);
531
532
if (validatedRange) {
533
const result: ICellViewModel[] = [];
534
535
for (let i = validatedRange.start; i < validatedRange.end; i++) {
536
result.push(this._viewCells[i]);
537
}
538
539
return result;
540
}
541
542
return [];
543
}
544
545
/**
546
* If this._viewCells[index] is visible then return index
547
*/
548
getNearestVisibleCellIndexUpwards(index: number) {
549
for (let i = this._hiddenRanges.length - 1; i >= 0; i--) {
550
const cellRange = this._hiddenRanges[i];
551
const foldStart = cellRange.start - 1;
552
const foldEnd = cellRange.end;
553
554
if (foldStart > index) {
555
continue;
556
}
557
558
if (foldStart <= index && foldEnd >= index) {
559
return index;
560
}
561
562
// foldStart <= index, foldEnd < index
563
break;
564
}
565
566
return index;
567
}
568
569
getNextVisibleCellIndex(index: number) {
570
for (let i = 0; i < this._hiddenRanges.length; i++) {
571
const cellRange = this._hiddenRanges[i];
572
const foldStart = cellRange.start - 1;
573
const foldEnd = cellRange.end;
574
575
if (foldEnd < index) {
576
continue;
577
}
578
579
// foldEnd >= index
580
if (foldStart <= index) {
581
return foldEnd + 1;
582
}
583
584
break;
585
}
586
587
return index + 1;
588
}
589
590
getPreviousVisibleCellIndex(index: number) {
591
for (let i = this._hiddenRanges.length - 1; i >= 0; i--) {
592
const cellRange = this._hiddenRanges[i];
593
const foldStart = cellRange.start - 1;
594
const foldEnd = cellRange.end;
595
596
if (foldEnd < index) {
597
return index;
598
}
599
600
if (foldStart <= index) {
601
return foldStart;
602
}
603
}
604
605
return index;
606
}
607
608
hasCell(cell: ICellViewModel) {
609
return this._handleToViewCellMapping.has(cell.handle);
610
}
611
612
getVersionId() {
613
return this._notebook.versionId;
614
}
615
616
getAlternativeId() {
617
return this._notebook.alternativeVersionId;
618
}
619
620
getTrackedRange(id: string): ICellRange | null {
621
return this._getDecorationRange(id);
622
}
623
624
private _getDecorationRange(decorationId: string): ICellRange | null {
625
const node = this._decorations[decorationId];
626
if (!node) {
627
return null;
628
}
629
const versionId = this.getVersionId();
630
if (node.cachedVersionId !== versionId) {
631
this._decorationsTree.resolveNode(node, versionId);
632
}
633
if (node.range === null) {
634
return { start: node.cachedAbsoluteStart - 1, end: node.cachedAbsoluteEnd - 1 };
635
}
636
637
return { start: node.range.startLineNumber - 1, end: node.range.endLineNumber - 1 };
638
}
639
640
setTrackedRange(id: string | null, newRange: ICellRange | null, newStickiness: TrackedRangeStickiness): string | null {
641
const node = (id ? this._decorations[id] : null);
642
643
if (!node) {
644
if (!newRange) {
645
return null;
646
}
647
648
return this._deltaCellDecorationsImpl(0, [], [{ range: new Range(newRange.start + 1, 1, newRange.end + 1, 1), options: TRACKED_RANGE_OPTIONS[newStickiness] }])[0];
649
}
650
651
if (!newRange) {
652
// node exists, the request is to delete => delete node
653
this._decorationsTree.delete(node);
654
delete this._decorations[node.id];
655
return null;
656
}
657
658
this._decorationsTree.delete(node);
659
node.reset(this.getVersionId(), newRange.start, newRange.end + 1, new Range(newRange.start + 1, 1, newRange.end + 1, 1));
660
node.setOptions(TRACKED_RANGE_OPTIONS[newStickiness]);
661
this._decorationsTree.insert(node);
662
return node.id;
663
}
664
665
private _deltaCellDecorationsImpl(ownerId: number, oldDecorationsIds: string[], newDecorations: IModelDeltaDecoration[]): string[] {
666
const versionId = this.getVersionId();
667
668
const oldDecorationsLen = oldDecorationsIds.length;
669
let oldDecorationIndex = 0;
670
671
const newDecorationsLen = newDecorations.length;
672
let newDecorationIndex = 0;
673
674
const result = new Array<string>(newDecorationsLen);
675
while (oldDecorationIndex < oldDecorationsLen || newDecorationIndex < newDecorationsLen) {
676
677
let node: IntervalNode | null = null;
678
679
if (oldDecorationIndex < oldDecorationsLen) {
680
// (1) get ourselves an old node
681
do {
682
node = this._decorations[oldDecorationsIds[oldDecorationIndex++]];
683
} while (!node && oldDecorationIndex < oldDecorationsLen);
684
685
// (2) remove the node from the tree (if it exists)
686
if (node) {
687
this._decorationsTree.delete(node);
688
}
689
}
690
691
if (newDecorationIndex < newDecorationsLen) {
692
// (3) create a new node if necessary
693
if (!node) {
694
const internalDecorationId = (++this._lastDecorationId);
695
const decorationId = `${this._instanceId};${internalDecorationId}`;
696
node = new IntervalNode(decorationId, 0, 0);
697
this._decorations[decorationId] = node;
698
}
699
700
// (4) initialize node
701
const newDecoration = newDecorations[newDecorationIndex];
702
const range = newDecoration.range;
703
const options = _normalizeOptions(newDecoration.options);
704
705
node.ownerId = ownerId;
706
node.reset(versionId, range.startLineNumber, range.endLineNumber, Range.lift(range));
707
node.setOptions(options);
708
709
this._decorationsTree.insert(node);
710
711
result[newDecorationIndex] = node.id;
712
713
newDecorationIndex++;
714
} else {
715
if (node) {
716
delete this._decorations[node.id];
717
}
718
}
719
}
720
721
return result;
722
}
723
724
deltaCellDecorations(oldDecorations: string[], newDecorations: INotebookDeltaDecoration[]): string[] {
725
oldDecorations.forEach(id => {
726
const handle = this._decorationIdToCellMap.get(id);
727
728
if (handle !== undefined) {
729
const cell = this.getCellByHandle(handle);
730
cell?.deltaCellDecorations([id], []);
731
this._decorationIdToCellMap.delete(id);
732
}
733
734
if (this._overviewRulerDecorations.has(id)) {
735
this._overviewRulerDecorations.delete(id);
736
}
737
});
738
739
const result: string[] = [];
740
741
newDecorations.forEach(decoration => {
742
if (isNotebookCellDecoration(decoration)) {
743
const cell = this.getCellByHandle(decoration.handle);
744
const ret = cell?.deltaCellDecorations([], [decoration.options]) || [];
745
ret.forEach(id => {
746
this._decorationIdToCellMap.set(id, decoration.handle);
747
});
748
result.push(...ret);
749
} else {
750
const id = ++this._lastOverviewRulerDecorationId;
751
const decorationId = `_overview_${this.id};${id}`;
752
this._overviewRulerDecorations.set(decorationId, decoration);
753
result.push(decorationId);
754
}
755
756
});
757
758
return result;
759
}
760
761
deltaCellStatusBarItems(oldItems: string[], newItems: INotebookDeltaCellStatusBarItems[]): string[] {
762
const deletesByHandle = groupBy(oldItems, id => this._statusBarItemIdToCellMap.get(id) ?? -1);
763
764
const result: string[] = [];
765
newItems.forEach(itemDelta => {
766
const cell = this.getCellByHandle(itemDelta.handle);
767
const deleted = deletesByHandle[itemDelta.handle] ?? [];
768
delete deletesByHandle[itemDelta.handle];
769
deleted.forEach(id => this._statusBarItemIdToCellMap.delete(id));
770
771
const ret = cell?.deltaCellStatusBarItems(deleted, itemDelta.items) || [];
772
ret.forEach(id => {
773
this._statusBarItemIdToCellMap.set(id, itemDelta.handle);
774
});
775
776
result.push(...ret);
777
});
778
779
for (const _handle in deletesByHandle) {
780
const handle = parseInt(_handle);
781
const ids = deletesByHandle[handle];
782
const cell = this.getCellByHandle(handle);
783
cell?.deltaCellStatusBarItems(ids, []);
784
ids.forEach(id => this._statusBarItemIdToCellMap.delete(id));
785
}
786
787
return result;
788
}
789
790
nearestCodeCellIndex(index: number /* exclusive */) {
791
const nearest = this.viewCells.slice(0, index).reverse().findIndex(cell => cell.cellKind === CellKind.Code);
792
if (nearest > -1) {
793
return index - nearest - 1;
794
} else {
795
const nearestCellTheOtherDirection = this.viewCells.slice(index + 1).findIndex(cell => cell.cellKind === CellKind.Code);
796
if (nearestCellTheOtherDirection > -1) {
797
return index + 1 + nearestCellTheOtherDirection;
798
}
799
return -1;
800
}
801
}
802
803
getEditorViewState(): INotebookEditorViewState {
804
const editingCells: { [key: number]: boolean } = {};
805
const collapsedInputCells: { [key: number]: boolean } = {};
806
const collapsedOutputCells: { [key: number]: boolean } = {};
807
const cellLineNumberStates: { [key: number]: 'on' | 'off' } = {};
808
809
this._viewCells.forEach((cell, i) => {
810
if (cell.getEditState() === CellEditState.Editing) {
811
editingCells[i] = true;
812
}
813
814
if (cell.isInputCollapsed) {
815
collapsedInputCells[i] = true;
816
}
817
818
if (cell instanceof CodeCellViewModel && cell.isOutputCollapsed) {
819
collapsedOutputCells[i] = true;
820
}
821
822
if (cell.lineNumbers !== 'inherit') {
823
cellLineNumberStates[i] = cell.lineNumbers;
824
}
825
});
826
const editorViewStates: { [key: number]: editorCommon.ICodeEditorViewState } = {};
827
this._viewCells.map(cell => ({ handle: cell.model.handle, state: cell.saveEditorViewState() })).forEach((viewState, i) => {
828
if (viewState.state) {
829
editorViewStates[i] = viewState.state;
830
}
831
});
832
833
return {
834
editingCells,
835
editorViewStates,
836
cellLineNumberStates,
837
collapsedInputCells,
838
collapsedOutputCells
839
};
840
}
841
842
restoreEditorViewState(viewState: INotebookEditorViewState | undefined): void {
843
if (!viewState) {
844
return;
845
}
846
847
this._viewCells.forEach((cell, index) => {
848
const isEditing = viewState.editingCells && viewState.editingCells[index];
849
const editorViewState = viewState.editorViewStates && viewState.editorViewStates[index];
850
851
cell.updateEditState(isEditing ? CellEditState.Editing : CellEditState.Preview, 'viewState');
852
const cellHeight = viewState.cellTotalHeights ? viewState.cellTotalHeights[index] : undefined;
853
cell.restoreEditorViewState(editorViewState, cellHeight);
854
if (viewState.collapsedInputCells && viewState.collapsedInputCells[index]) {
855
cell.isInputCollapsed = true;
856
}
857
if (viewState.collapsedOutputCells && viewState.collapsedOutputCells[index] && cell instanceof CodeCellViewModel) {
858
cell.isOutputCollapsed = true;
859
}
860
if (viewState.cellLineNumberStates && viewState.cellLineNumberStates[index]) {
861
cell.lineNumbers = viewState.cellLineNumberStates[index];
862
}
863
});
864
}
865
866
/**
867
* Editor decorations across cells. For example, find decorations for multiple code cells
868
* 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.
869
*/
870
changeModelDecorations<T>(callback: (changeAccessor: IModelDecorationsChangeAccessor) => T): T | null {
871
const changeAccessor: IModelDecorationsChangeAccessor = {
872
deltaDecorations: (oldDecorations: ICellModelDecorations[], newDecorations: ICellModelDeltaDecorations[]): ICellModelDecorations[] => {
873
return this._deltaModelDecorationsImpl(oldDecorations, newDecorations);
874
}
875
};
876
877
let result: T | null = null;
878
try {
879
result = callback(changeAccessor);
880
} catch (e) {
881
onUnexpectedError(e);
882
}
883
884
changeAccessor.deltaDecorations = invalidFunc;
885
886
return result;
887
}
888
889
private _deltaModelDecorationsImpl(oldDecorations: ICellModelDecorations[], newDecorations: ICellModelDeltaDecorations[]): ICellModelDecorations[] {
890
891
const mapping = new Map<number, { cell: CellViewModel; oldDecorations: readonly string[]; newDecorations: readonly IModelDeltaDecoration[] }>();
892
oldDecorations.forEach(oldDecoration => {
893
const ownerId = oldDecoration.ownerId;
894
895
if (!mapping.has(ownerId)) {
896
const cell = this._viewCells.find(cell => cell.handle === ownerId);
897
if (cell) {
898
mapping.set(ownerId, { cell: cell, oldDecorations: [], newDecorations: [] });
899
}
900
}
901
902
const data = mapping.get(ownerId)!;
903
if (data) {
904
data.oldDecorations = oldDecoration.decorations;
905
}
906
});
907
908
newDecorations.forEach(newDecoration => {
909
const ownerId = newDecoration.ownerId;
910
911
if (!mapping.has(ownerId)) {
912
const cell = this._viewCells.find(cell => cell.handle === ownerId);
913
914
if (cell) {
915
mapping.set(ownerId, { cell: cell, oldDecorations: [], newDecorations: [] });
916
}
917
}
918
919
const data = mapping.get(ownerId)!;
920
if (data) {
921
data.newDecorations = newDecoration.decorations;
922
}
923
});
924
925
const ret: ICellModelDecorations[] = [];
926
mapping.forEach((value, ownerId) => {
927
const cellRet = value.cell.deltaModelDecorations(value.oldDecorations, value.newDecorations);
928
ret.push({
929
ownerId: ownerId,
930
decorations: cellRet
931
});
932
});
933
934
return ret;
935
}
936
937
//#region Find
938
find(value: string, options: INotebookFindOptions): CellFindMatchWithIndex[] {
939
const matches: CellFindMatchWithIndex[] = [];
940
let findCells: CellViewModel[] = [];
941
942
if (options.findScope && (options.findScope.findScopeType === NotebookFindScopeType.Cells || options.findScope.findScopeType === NotebookFindScopeType.Text)) {
943
const selectedRanges = options.findScope.selectedCellRanges?.map(range => this.validateRange(range)).filter(range => !!range) ?? [];
944
const selectedIndexes = cellRangesToIndexes(selectedRanges);
945
findCells = selectedIndexes.map(index => this._viewCells[index]);
946
} else {
947
findCells = this._viewCells;
948
}
949
950
findCells.forEach((cell, index) => {
951
const cellMatches = cell.startFind(value, options);
952
if (cellMatches) {
953
matches.push(new CellFindMatchModel(
954
cellMatches.cell,
955
index,
956
cellMatches.contentMatches,
957
[]
958
));
959
}
960
});
961
962
// filter based on options and editing state
963
964
return matches.filter(match => {
965
if (match.cell.cellKind === CellKind.Code) {
966
// code cell, we only include its match if include input is enabled
967
return options.includeCodeInput;
968
}
969
970
// markup cell, it depends on the editing state
971
if (match.cell.getEditState() === CellEditState.Editing) {
972
// editing, even if we includeMarkupPreview
973
return options.includeMarkupInput;
974
} else {
975
// cell in preview mode, we should only include it if includeMarkupPreview is false but includeMarkupInput is true
976
// if includeMarkupPreview is true, then we should include the webview match result other than this
977
return !options.includeMarkupPreview && options.includeMarkupInput;
978
}
979
}
980
);
981
}
982
983
replaceOne(cell: ICellViewModel, range: Range, text: string): Promise<void> {
984
const viewCell = cell as CellViewModel;
985
this._lastNotebookEditResource.push(viewCell.uri);
986
return viewCell.resolveTextModel().then(() => {
987
this._bulkEditService.apply(
988
[new ResourceTextEdit(cell.uri, { range, text })],
989
{ quotableLabel: 'Notebook Replace' }
990
);
991
});
992
}
993
994
async replaceAll(matches: CellFindMatchWithIndex[], texts: string[]): Promise<void> {
995
if (!matches.length) {
996
return;
997
}
998
999
const textEdits: IWorkspaceTextEdit[] = [];
1000
this._lastNotebookEditResource.push(matches[0].cell.uri);
1001
1002
matches.forEach(match => {
1003
match.contentMatches.forEach((singleMatch, index) => {
1004
textEdits.push({
1005
versionId: undefined,
1006
textEdit: { range: (singleMatch as FindMatch).range, text: texts[index] },
1007
resource: match.cell.uri
1008
});
1009
});
1010
});
1011
1012
return Promise.all(matches.map(match => {
1013
return match.cell.resolveTextModel();
1014
})).then(async () => {
1015
this._bulkEditService.apply({ edits: textEdits }, { quotableLabel: 'Notebook Replace All' });
1016
return;
1017
});
1018
}
1019
1020
//#endregion
1021
1022
//#region Undo/Redo
1023
1024
private async _withElement(element: SingleModelEditStackElement | MultiModelEditStackElement, callback: () => Promise<void>) {
1025
const viewCells = this._viewCells.filter(cell => element.matchesResource(cell.uri));
1026
const refs = await Promise.all(viewCells.map(cell => this._textModelService.createModelReference(cell.uri)));
1027
await callback();
1028
refs.forEach(ref => ref.dispose());
1029
}
1030
1031
async undo() {
1032
1033
const editStack = this._undoService.getElements(this.uri);
1034
const element = editStack.past.length ? editStack.past[editStack.past.length - 1] : undefined;
1035
1036
if (element && element instanceof SingleModelEditStackElement || element instanceof MultiModelEditStackElement) {
1037
await this._withElement(element, async () => {
1038
await this._undoService.undo(this.uri);
1039
});
1040
1041
return (element instanceof SingleModelEditStackElement) ? [element.resource] : element.resources;
1042
}
1043
1044
await this._undoService.undo(this.uri);
1045
return [];
1046
}
1047
1048
async redo() {
1049
1050
const editStack = this._undoService.getElements(this.uri);
1051
const element = editStack.future[0];
1052
1053
if (element && element instanceof SingleModelEditStackElement || element instanceof MultiModelEditStackElement) {
1054
await this._withElement(element, async () => {
1055
await this._undoService.redo(this.uri);
1056
});
1057
1058
return (element instanceof SingleModelEditStackElement) ? [element.resource] : element.resources;
1059
}
1060
1061
await this._undoService.redo(this.uri);
1062
1063
return [];
1064
}
1065
1066
//#endregion
1067
1068
equal(notebook: NotebookTextModel) {
1069
return this._notebook === notebook;
1070
}
1071
1072
override dispose() {
1073
this._localStore.clear();
1074
this._viewCells.forEach(cell => {
1075
cell.dispose();
1076
});
1077
1078
super.dispose();
1079
}
1080
}
1081
1082
export type CellViewModel = (CodeCellViewModel | MarkupCellViewModel) & ICellViewModel;
1083
1084
export function createCellViewModel(instantiationService: IInstantiationService, notebookViewModel: NotebookViewModel, cell: NotebookCellTextModel, viewContext: ViewContext) {
1085
if (cell.cellKind === CellKind.Code) {
1086
return instantiationService.createInstance(CodeCellViewModel, notebookViewModel.viewType, cell, notebookViewModel.layoutInfo, viewContext);
1087
} else {
1088
return instantiationService.createInstance(MarkupCellViewModel, notebookViewModel.viewType, cell, notebookViewModel.layoutInfo, notebookViewModel, viewContext);
1089
}
1090
}
1091
1092