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
5240 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
readonly 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
this._overviewRulerDecorations.delete(id);
735
});
736
737
const result: string[] = [];
738
739
newDecorations.forEach(decoration => {
740
if (isNotebookCellDecoration(decoration)) {
741
const cell = this.getCellByHandle(decoration.handle);
742
const ret = cell?.deltaCellDecorations([], [decoration.options]) || [];
743
ret.forEach(id => {
744
this._decorationIdToCellMap.set(id, decoration.handle);
745
});
746
result.push(...ret);
747
} else {
748
const id = ++this._lastOverviewRulerDecorationId;
749
const decorationId = `_overview_${this.id};${id}`;
750
this._overviewRulerDecorations.set(decorationId, decoration);
751
result.push(decorationId);
752
}
753
754
});
755
756
return result;
757
}
758
759
deltaCellStatusBarItems(oldItems: string[], newItems: INotebookDeltaCellStatusBarItems[]): string[] {
760
const deletesByHandle = groupBy(oldItems, id => this._statusBarItemIdToCellMap.get(id) ?? -1);
761
762
const result: string[] = [];
763
newItems.forEach(itemDelta => {
764
const cell = this.getCellByHandle(itemDelta.handle);
765
const deleted = deletesByHandle[itemDelta.handle] ?? [];
766
delete deletesByHandle[itemDelta.handle];
767
deleted.forEach(id => this._statusBarItemIdToCellMap.delete(id));
768
769
const ret = cell?.deltaCellStatusBarItems(deleted, itemDelta.items) || [];
770
ret.forEach(id => {
771
this._statusBarItemIdToCellMap.set(id, itemDelta.handle);
772
});
773
774
result.push(...ret);
775
});
776
777
for (const _handle in deletesByHandle) {
778
const handle = parseInt(_handle);
779
const ids = deletesByHandle[handle]!;
780
const cell = this.getCellByHandle(handle);
781
cell?.deltaCellStatusBarItems(ids, []);
782
ids.forEach(id => this._statusBarItemIdToCellMap.delete(id));
783
}
784
785
return result;
786
}
787
788
nearestCodeCellIndex(index: number /* exclusive */) {
789
const nearest = this.viewCells.slice(0, index).reverse().findIndex(cell => cell.cellKind === CellKind.Code);
790
if (nearest > -1) {
791
return index - nearest - 1;
792
} else {
793
const nearestCellTheOtherDirection = this.viewCells.slice(index + 1).findIndex(cell => cell.cellKind === CellKind.Code);
794
if (nearestCellTheOtherDirection > -1) {
795
return index + 1 + nearestCellTheOtherDirection;
796
}
797
return -1;
798
}
799
}
800
801
getEditorViewState(): INotebookEditorViewState {
802
const editingCells: { [key: number]: boolean } = {};
803
const collapsedInputCells: { [key: number]: boolean } = {};
804
const collapsedOutputCells: { [key: number]: boolean } = {};
805
const cellLineNumberStates: { [key: number]: 'on' | 'off' } = {};
806
807
this._viewCells.forEach((cell, i) => {
808
if (cell.getEditState() === CellEditState.Editing) {
809
editingCells[i] = true;
810
}
811
812
if (cell.isInputCollapsed) {
813
collapsedInputCells[i] = true;
814
}
815
816
if (cell instanceof CodeCellViewModel && cell.isOutputCollapsed) {
817
collapsedOutputCells[i] = true;
818
}
819
820
if (cell.lineNumbers !== 'inherit') {
821
cellLineNumberStates[i] = cell.lineNumbers;
822
}
823
});
824
const editorViewStates: { [key: number]: editorCommon.ICodeEditorViewState } = {};
825
this._viewCells.map(cell => ({ handle: cell.model.handle, state: cell.saveEditorViewState() })).forEach((viewState, i) => {
826
if (viewState.state) {
827
editorViewStates[i] = viewState.state;
828
}
829
});
830
831
return {
832
editingCells,
833
editorViewStates,
834
cellLineNumberStates,
835
collapsedInputCells,
836
collapsedOutputCells
837
};
838
}
839
840
restoreEditorViewState(viewState: INotebookEditorViewState | undefined): void {
841
if (!viewState) {
842
return;
843
}
844
845
this._viewCells.forEach((cell, index) => {
846
const isEditing = viewState.editingCells && viewState.editingCells[index];
847
const editorViewState = viewState.editorViewStates && viewState.editorViewStates[index];
848
849
cell.updateEditState(isEditing ? CellEditState.Editing : CellEditState.Preview, 'viewState');
850
const cellHeight = viewState.cellTotalHeights ? viewState.cellTotalHeights[index] : undefined;
851
cell.restoreEditorViewState(editorViewState, cellHeight);
852
if (viewState.collapsedInputCells && viewState.collapsedInputCells[index]) {
853
cell.isInputCollapsed = true;
854
}
855
if (viewState.collapsedOutputCells && viewState.collapsedOutputCells[index] && cell instanceof CodeCellViewModel) {
856
cell.isOutputCollapsed = true;
857
}
858
if (viewState.cellLineNumberStates && viewState.cellLineNumberStates[index]) {
859
cell.lineNumbers = viewState.cellLineNumberStates[index];
860
}
861
});
862
}
863
864
/**
865
* Editor decorations across cells. For example, find decorations for multiple code cells
866
* 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.
867
*/
868
changeModelDecorations<T>(callback: (changeAccessor: IModelDecorationsChangeAccessor) => T): T | null {
869
const changeAccessor: IModelDecorationsChangeAccessor = {
870
deltaDecorations: (oldDecorations: ICellModelDecorations[], newDecorations: ICellModelDeltaDecorations[]): ICellModelDecorations[] => {
871
return this._deltaModelDecorationsImpl(oldDecorations, newDecorations);
872
}
873
};
874
875
let result: T | null = null;
876
try {
877
result = callback(changeAccessor);
878
} catch (e) {
879
onUnexpectedError(e);
880
}
881
882
changeAccessor.deltaDecorations = invalidFunc;
883
884
return result;
885
}
886
887
private _deltaModelDecorationsImpl(oldDecorations: ICellModelDecorations[], newDecorations: ICellModelDeltaDecorations[]): ICellModelDecorations[] {
888
889
const mapping = new Map<number, { cell: CellViewModel; oldDecorations: readonly string[]; newDecorations: readonly IModelDeltaDecoration[] }>();
890
oldDecorations.forEach(oldDecoration => {
891
const ownerId = oldDecoration.ownerId;
892
893
if (!mapping.has(ownerId)) {
894
const cell = this._viewCells.find(cell => cell.handle === ownerId);
895
if (cell) {
896
mapping.set(ownerId, { cell: cell, oldDecorations: [], newDecorations: [] });
897
}
898
}
899
900
const data = mapping.get(ownerId)!;
901
if (data) {
902
data.oldDecorations = oldDecoration.decorations;
903
}
904
});
905
906
newDecorations.forEach(newDecoration => {
907
const ownerId = newDecoration.ownerId;
908
909
if (!mapping.has(ownerId)) {
910
const cell = this._viewCells.find(cell => cell.handle === ownerId);
911
912
if (cell) {
913
mapping.set(ownerId, { cell: cell, oldDecorations: [], newDecorations: [] });
914
}
915
}
916
917
const data = mapping.get(ownerId)!;
918
if (data) {
919
data.newDecorations = newDecoration.decorations;
920
}
921
});
922
923
const ret: ICellModelDecorations[] = [];
924
mapping.forEach((value, ownerId) => {
925
const cellRet = value.cell.deltaModelDecorations(value.oldDecorations, value.newDecorations);
926
ret.push({
927
ownerId: ownerId,
928
decorations: cellRet
929
});
930
});
931
932
return ret;
933
}
934
935
//#region Find
936
find(value: string, options: INotebookFindOptions): CellFindMatchWithIndex[] {
937
const matches: CellFindMatchWithIndex[] = [];
938
let findCells: CellViewModel[] = [];
939
940
if (options.findScope && (options.findScope.findScopeType === NotebookFindScopeType.Cells || options.findScope.findScopeType === NotebookFindScopeType.Text)) {
941
const selectedRanges = options.findScope.selectedCellRanges?.map(range => this.validateRange(range)).filter(range => !!range) ?? [];
942
const selectedIndexes = cellRangesToIndexes(selectedRanges);
943
findCells = selectedIndexes.map(index => this._viewCells[index]);
944
} else {
945
findCells = this._viewCells;
946
}
947
948
findCells.forEach((cell, index) => {
949
const cellMatches = cell.startFind(value, options);
950
if (cellMatches) {
951
matches.push(new CellFindMatchModel(
952
cellMatches.cell,
953
index,
954
cellMatches.contentMatches,
955
[]
956
));
957
}
958
});
959
960
// filter based on options and editing state
961
962
return matches.filter(match => {
963
if (match.cell.cellKind === CellKind.Code) {
964
// code cell, we only include its match if include input is enabled
965
return options.includeCodeInput;
966
}
967
968
// markup cell, it depends on the editing state
969
if (match.cell.getEditState() === CellEditState.Editing) {
970
// editing, even if we includeMarkupPreview
971
return options.includeMarkupInput;
972
} else {
973
// cell in preview mode, we should only include it if includeMarkupPreview is false but includeMarkupInput is true
974
// if includeMarkupPreview is true, then we should include the webview match result other than this
975
return !options.includeMarkupPreview && options.includeMarkupInput;
976
}
977
}
978
);
979
}
980
981
replaceOne(cell: ICellViewModel, range: Range, text: string): Promise<void> {
982
const viewCell = cell as CellViewModel;
983
this._lastNotebookEditResource.push(viewCell.uri);
984
return viewCell.resolveTextModel().then(() => {
985
this._bulkEditService.apply(
986
[new ResourceTextEdit(cell.uri, { range, text })],
987
{ quotableLabel: 'Notebook Replace' }
988
);
989
});
990
}
991
992
async replaceAll(matches: CellFindMatchWithIndex[], texts: string[]): Promise<void> {
993
if (!matches.length) {
994
return;
995
}
996
997
const textEdits: IWorkspaceTextEdit[] = [];
998
this._lastNotebookEditResource.push(matches[0].cell.uri);
999
1000
matches.forEach(match => {
1001
match.contentMatches.forEach((singleMatch, index) => {
1002
textEdits.push({
1003
versionId: undefined,
1004
textEdit: { range: (singleMatch as FindMatch).range, text: texts[index] },
1005
resource: match.cell.uri
1006
});
1007
});
1008
});
1009
1010
return Promise.all(matches.map(match => {
1011
return match.cell.resolveTextModel();
1012
})).then(async () => {
1013
this._bulkEditService.apply({ edits: textEdits }, { quotableLabel: 'Notebook Replace All' });
1014
return;
1015
});
1016
}
1017
1018
//#endregion
1019
1020
//#region Undo/Redo
1021
1022
private async _withElement(element: SingleModelEditStackElement | MultiModelEditStackElement, callback: () => Promise<void>) {
1023
const viewCells = this._viewCells.filter(cell => element.matchesResource(cell.uri));
1024
const refs = await Promise.all(viewCells.map(cell => this._textModelService.createModelReference(cell.uri)));
1025
await callback();
1026
refs.forEach(ref => ref.dispose());
1027
}
1028
1029
async undo() {
1030
1031
const editStack = this._undoService.getElements(this.uri);
1032
const element = editStack.past.length ? editStack.past[editStack.past.length - 1] : undefined;
1033
1034
if (element && element instanceof SingleModelEditStackElement || element instanceof MultiModelEditStackElement) {
1035
await this._withElement(element, async () => {
1036
await this._undoService.undo(this.uri);
1037
});
1038
1039
return (element instanceof SingleModelEditStackElement) ? [element.resource] : element.resources;
1040
}
1041
1042
await this._undoService.undo(this.uri);
1043
return [];
1044
}
1045
1046
async redo() {
1047
1048
const editStack = this._undoService.getElements(this.uri);
1049
const element = editStack.future[0];
1050
1051
if (element && element instanceof SingleModelEditStackElement || element instanceof MultiModelEditStackElement) {
1052
await this._withElement(element, async () => {
1053
await this._undoService.redo(this.uri);
1054
});
1055
1056
return (element instanceof SingleModelEditStackElement) ? [element.resource] : element.resources;
1057
}
1058
1059
await this._undoService.redo(this.uri);
1060
1061
return [];
1062
}
1063
1064
//#endregion
1065
1066
equal(notebook: NotebookTextModel) {
1067
return this._notebook === notebook;
1068
}
1069
1070
override dispose() {
1071
this._localStore.clear();
1072
this._viewCells.forEach(cell => {
1073
cell.dispose();
1074
});
1075
1076
super.dispose();
1077
}
1078
}
1079
1080
export type CellViewModel = (CodeCellViewModel | MarkupCellViewModel) & ICellViewModel;
1081
1082
export function createCellViewModel(instantiationService: IInstantiationService, notebookViewModel: NotebookViewModel, cell: NotebookCellTextModel, viewContext: ViewContext) {
1083
if (cell.cellKind === CellKind.Code) {
1084
return instantiationService.createInstance(CodeCellViewModel, notebookViewModel.viewType, cell, notebookViewModel.layoutInfo, viewContext);
1085
} else {
1086
return instantiationService.createInstance(MarkupCellViewModel, notebookViewModel.viewType, cell, notebookViewModel.layoutInfo, notebookViewModel, viewContext);
1087
}
1088
}
1089
1090