Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.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 { ISequence, LcsDiff } from '../../../../../base/common/diff/diff.js';
7
import { Emitter, Event, PauseableEmitter } from '../../../../../base/common/event.js';
8
import { hash } from '../../../../../base/common/hash.js';
9
import { Disposable, dispose, IDisposable } from '../../../../../base/common/lifecycle.js';
10
import { Schemas } from '../../../../../base/common/network.js';
11
import { filter } from '../../../../../base/common/objects.js';
12
import { isEqual } from '../../../../../base/common/resources.js';
13
import { isDefined } from '../../../../../base/common/types.js';
14
import { URI } from '../../../../../base/common/uri.js';
15
import { Position } from '../../../../../editor/common/core/position.js';
16
import { Range } from '../../../../../editor/common/core/range.js';
17
import { ILanguageService } from '../../../../../editor/common/languages/language.js';
18
import { FindMatch, ITextModel } from '../../../../../editor/common/model.js';
19
import { TextModel } from '../../../../../editor/common/model/textModel.js';
20
import { SearchParams } from '../../../../../editor/common/model/textModelSearch.js';
21
import { IModelService } from '../../../../../editor/common/services/model.js';
22
import { IModelContentChangedEvent } from '../../../../../editor/common/textModelEvents.js';
23
import { IResourceUndoRedoElement, IUndoRedoElement, IUndoRedoService, IWorkspaceUndoRedoElement, UndoRedoElementType, UndoRedoGroup } from '../../../../../platform/undoRedo/common/undoRedo.js';
24
import { ILanguageDetectionService } from '../../../../services/languageDetection/common/languageDetectionWorkerService.js';
25
import { SnapshotContext } from '../../../../services/workingCopy/common/fileWorkingCopy.js';
26
import { CellEditType, CellKind, CellUri, diff, ICell, ICellDto2, ICellEditOperation, ICellOutput, INotebookSnapshotOptions, INotebookTextModel, IOutputDto, IOutputItemDto, ISelectionState, NotebookCellCollapseState, NotebookCellDefaultCollapseConfig, NotebookCellExecutionState, NotebookCellInternalMetadata, NotebookCellMetadata, NotebookCellOutputsSplice, NotebookCellsChangeType, NotebookCellTextModelSplice, NotebookData, NotebookDocumentMetadata, NotebookTextModelChangedEvent, NotebookTextModelWillAddRemoveEvent, NullablePartialNotebookCellInternalMetadata, NullablePartialNotebookCellMetadata, TransientOptions } from '../notebookCommon.js';
27
import { INotebookExecutionStateService } from '../notebookExecutionStateService.js';
28
import { CellMetadataEdit, MoveCellEdit, SpliceCellsEdit } from './cellEdit.js';
29
import { NotebookCellOutputTextModel } from './notebookCellOutputTextModel.js';
30
import { NotebookCellTextModel } from './notebookCellTextModel.js';
31
32
class StackOperation implements IWorkspaceUndoRedoElement {
33
type: UndoRedoElementType.Workspace;
34
tag = 'notebookUndoRedoElement';
35
36
public get code() {
37
return this._operations.length === 1 ? this._operations[0].code : 'undoredo.notebooks.stackOperation';
38
}
39
40
private _operations: IUndoRedoElement[] = [];
41
private _beginSelectionState: ISelectionState | undefined = undefined;
42
private _resultSelectionState: ISelectionState | undefined = undefined;
43
private _beginAlternativeVersionId: string;
44
private _resultAlternativeVersionId: string;
45
public get label() {
46
return this._operations.length === 1 ? this._operations[0].label : 'edit';
47
}
48
49
constructor(
50
readonly textModel: NotebookTextModel,
51
readonly undoRedoGroup: UndoRedoGroup | undefined,
52
private readonly _pauseableEmitter: PauseableEmitter<NotebookTextModelChangedEvent>,
53
private readonly _postUndoRedo: (alternativeVersionId: string) => void,
54
selectionState: ISelectionState | undefined,
55
beginAlternativeVersionId: string
56
) {
57
this.type = UndoRedoElementType.Workspace;
58
this._beginSelectionState = selectionState;
59
this._beginAlternativeVersionId = beginAlternativeVersionId;
60
this._resultAlternativeVersionId = beginAlternativeVersionId;
61
}
62
get resources(): readonly URI[] {
63
return [this.textModel.uri];
64
}
65
66
get isEmpty(): boolean {
67
return this._operations.length === 0;
68
}
69
70
pushEndState(alternativeVersionId: string, selectionState: ISelectionState | undefined) {
71
// https://github.com/microsoft/vscode/issues/207523
72
this._resultAlternativeVersionId = alternativeVersionId;
73
this._resultSelectionState = selectionState || this._resultSelectionState;
74
}
75
76
pushEditOperation(element: IUndoRedoElement, beginSelectionState: ISelectionState | undefined, resultSelectionState: ISelectionState | undefined, alternativeVersionId: string) {
77
if (this._operations.length === 0) {
78
this._beginSelectionState = this._beginSelectionState ?? beginSelectionState;
79
}
80
this._operations.push(element);
81
this._resultSelectionState = resultSelectionState;
82
this._resultAlternativeVersionId = alternativeVersionId;
83
}
84
85
async undo(): Promise<void> {
86
this._pauseableEmitter.pause();
87
try {
88
for (let i = this._operations.length - 1; i >= 0; i--) {
89
await this._operations[i].undo();
90
}
91
this._postUndoRedo(this._beginAlternativeVersionId);
92
this._pauseableEmitter.fire({
93
rawEvents: [],
94
synchronous: undefined,
95
versionId: this.textModel.versionId,
96
endSelectionState: this._beginSelectionState
97
});
98
} finally {
99
this._pauseableEmitter.resume();
100
}
101
}
102
103
async redo(): Promise<void> {
104
this._pauseableEmitter.pause();
105
try {
106
for (let i = 0; i < this._operations.length; i++) {
107
await this._operations[i].redo();
108
}
109
this._postUndoRedo(this._resultAlternativeVersionId);
110
this._pauseableEmitter.fire({
111
rawEvents: [],
112
synchronous: undefined,
113
versionId: this.textModel.versionId,
114
endSelectionState: this._resultSelectionState
115
});
116
} finally {
117
this._pauseableEmitter.resume();
118
}
119
120
}
121
}
122
123
class NotebookOperationManager {
124
private _pendingStackOperation: StackOperation | null = null;
125
private _isAppending: boolean = false;
126
constructor(
127
private readonly _textModel: NotebookTextModel,
128
private _undoService: IUndoRedoService,
129
private _pauseableEmitter: PauseableEmitter<NotebookTextModelChangedEvent>,
130
private _postUndoRedo: (alternativeVersionId: string) => void
131
) {
132
}
133
134
isUndoStackEmpty(): boolean {
135
return this._pendingStackOperation === null || this._pendingStackOperation.isEmpty;
136
}
137
138
pushStackElement(alternativeVersionId: string, selectionState: ISelectionState | undefined) {
139
if (this._pendingStackOperation && !this._pendingStackOperation.isEmpty) {
140
this._pendingStackOperation.pushEndState(alternativeVersionId, selectionState);
141
if (!this._isAppending) {
142
this._undoService.pushElement(this._pendingStackOperation, this._pendingStackOperation.undoRedoGroup);
143
}
144
}
145
this._isAppending = false;
146
this._pendingStackOperation = null;
147
}
148
149
private _getOrCreateEditStackElement(beginSelectionState: ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined, alternativeVersionId: string) {
150
return this._pendingStackOperation ??= new StackOperation(this._textModel, undoRedoGroup, this._pauseableEmitter, this._postUndoRedo, beginSelectionState, alternativeVersionId || '');
151
}
152
153
appendPreviousOperation(): boolean {
154
const previous = this._undoService.getLastElement(this._textModel.uri) as StackOperation;
155
if (previous && previous.tag === 'notebookUndoRedoElement') {
156
this._pendingStackOperation = previous;
157
this._isAppending = true;
158
return true;
159
}
160
return false;
161
}
162
163
pushEditOperation(element: IUndoRedoElement, beginSelectionState: ISelectionState | undefined, resultSelectionState: ISelectionState | undefined, alternativeVersionId: string, undoRedoGroup: UndoRedoGroup | undefined) {
164
const pendingStackOperation = this._getOrCreateEditStackElement(beginSelectionState, undoRedoGroup, alternativeVersionId);
165
pendingStackOperation.pushEditOperation(element, beginSelectionState, resultSelectionState, alternativeVersionId);
166
}
167
}
168
169
type TransformedEdit = {
170
edit: ICellEditOperation;
171
cellIndex: number;
172
end: number | undefined;
173
originalIndex: number;
174
};
175
176
class NotebookEventEmitter extends PauseableEmitter<NotebookTextModelChangedEvent> {
177
get isEmpty() {
178
return this._eventQueue.isEmpty();
179
}
180
181
isDirtyEvent() {
182
for (const e of this._eventQueue) {
183
for (let i = 0; i < e.rawEvents.length; i++) {
184
if (!e.rawEvents[i].transient) {
185
return true;
186
}
187
}
188
}
189
190
return false;
191
}
192
}
193
194
export class NotebookTextModel extends Disposable implements INotebookTextModel {
195
196
private _isDisposed = false;
197
private readonly _onWillDispose: Emitter<void> = this._register(new Emitter<void>());
198
private readonly _onWillAddRemoveCells = this._register(new Emitter<NotebookTextModelWillAddRemoveEvent>());
199
private readonly _onDidChangeContent = this._register(new Emitter<NotebookTextModelChangedEvent>());
200
readonly onWillDispose: Event<void> = this._onWillDispose.event;
201
readonly onWillAddRemoveCells = this._onWillAddRemoveCells.event;
202
readonly onDidChangeContent = this._onDidChangeContent.event;
203
private _cellhandlePool: number = 0;
204
private readonly _cellListeners: Map<number, IDisposable> = new Map();
205
private _cells: NotebookCellTextModel[] = [];
206
private _defaultCollapseConfig: NotebookCellDefaultCollapseConfig | undefined;
207
208
metadata: NotebookDocumentMetadata = {};
209
transientOptions: TransientOptions = { transientCellMetadata: {}, transientDocumentMetadata: {}, transientOutputs: false, cellContentMetadata: {} };
210
private _versionId = 0;
211
212
/**
213
* This alternative id is only for non-cell-content changes.
214
*/
215
private _notebookSpecificAlternativeId = 0;
216
217
/**
218
* Unlike, versionId, this can go down (via undo) or go to previous values (via redo)
219
*/
220
private _alternativeVersionId: string = '1';
221
private _operationManager: NotebookOperationManager;
222
private _pauseableEmitter: NotebookEventEmitter;
223
224
get length() {
225
return this._cells.length;
226
}
227
228
get cells(): readonly NotebookCellTextModel[] {
229
return this._cells;
230
}
231
232
get versionId() {
233
return this._versionId;
234
}
235
236
get alternativeVersionId(): string {
237
return this._alternativeVersionId;
238
}
239
240
get notebookType() {
241
return this.viewType;
242
}
243
244
constructor(
245
readonly viewType: string,
246
readonly uri: URI,
247
cells: ICellDto2[],
248
metadata: NotebookDocumentMetadata,
249
options: TransientOptions,
250
@IUndoRedoService private readonly _undoService: IUndoRedoService,
251
@IModelService private readonly _modelService: IModelService,
252
@ILanguageService private readonly _languageService: ILanguageService,
253
@ILanguageDetectionService private readonly _languageDetectionService: ILanguageDetectionService,
254
@INotebookExecutionStateService private readonly _notebookExecutionStateService: INotebookExecutionStateService,
255
) {
256
super();
257
this.transientOptions = options;
258
this.metadata = metadata;
259
this._initialize(cells);
260
261
const maybeUpdateCellTextModel = (textModel: ITextModel) => {
262
if (textModel.uri.scheme === Schemas.vscodeNotebookCell && textModel instanceof TextModel) {
263
const cellUri = CellUri.parse(textModel.uri);
264
if (cellUri && isEqual(cellUri.notebook, this.uri)) {
265
const cellIdx = this._getCellIndexByHandle(cellUri.handle);
266
if (cellIdx >= 0) {
267
const cell = this.cells[cellIdx];
268
if (cell) {
269
cell.textModel = textModel;
270
}
271
}
272
}
273
}
274
};
275
this._register(_modelService.onModelAdded(e => maybeUpdateCellTextModel(e)));
276
277
this._pauseableEmitter = new NotebookEventEmitter({
278
merge: (events: NotebookTextModelChangedEvent[]) => {
279
const first = events[0];
280
281
const rawEvents = first.rawEvents;
282
let versionId = first.versionId;
283
let endSelectionState = first.endSelectionState;
284
let synchronous = first.synchronous;
285
286
for (let i = 1; i < events.length; i++) {
287
rawEvents.push(...events[i].rawEvents);
288
versionId = events[i].versionId;
289
endSelectionState = events[i].endSelectionState !== undefined ? events[i].endSelectionState : endSelectionState;
290
synchronous = events[i].synchronous !== undefined ? events[i].synchronous : synchronous;
291
}
292
293
return { rawEvents, versionId, endSelectionState, synchronous };
294
}
295
});
296
297
this._register(this._pauseableEmitter.event(e => {
298
if (e.rawEvents.length) {
299
this._onDidChangeContent.fire(e);
300
}
301
}));
302
303
this._operationManager = new NotebookOperationManager(
304
this,
305
this._undoService,
306
this._pauseableEmitter,
307
(alternativeVersionId: string) => {
308
this._increaseVersionId(true);
309
this._overwriteAlternativeVersionId(alternativeVersionId);
310
}
311
);
312
}
313
314
setCellCollapseDefault(collapseConfig: NotebookCellDefaultCollapseConfig | undefined) {
315
this._defaultCollapseConfig = collapseConfig;
316
}
317
318
_initialize(cells: ICellDto2[], triggerDirty?: boolean) {
319
this._cells = [];
320
this._versionId = 0;
321
this._notebookSpecificAlternativeId = 0;
322
323
const mainCells = cells.map(cell => {
324
const cellHandle = this._cellhandlePool++;
325
const cellUri = CellUri.generate(this.uri, cellHandle);
326
const collapseState = this._getDefaultCollapseState(cell);
327
return new NotebookCellTextModel(cellUri, cellHandle, cell.source, cell.language, cell.mime, cell.cellKind, cell.outputs, cell.metadata, cell.internalMetadata, collapseState, this.transientOptions, this._languageService,
328
this._modelService.getCreationOptions(cell.language, cellUri, false).defaultEOL, this._languageDetectionService);
329
});
330
331
for (let i = 0; i < mainCells.length; i++) {
332
const dirtyStateListener = mainCells[i].onDidChangeContent((e) => {
333
this._bindCellContentHandler(mainCells[i], e);
334
});
335
336
this._cellListeners.set(mainCells[i].handle, dirtyStateListener);
337
this._register(mainCells[i]);
338
}
339
340
this._cells.splice(0, 0, ...mainCells);
341
this._alternativeVersionId = this._generateAlternativeId();
342
343
if (triggerDirty) {
344
this._pauseableEmitter.fire({
345
rawEvents: [{ kind: NotebookCellsChangeType.Unknown, transient: false }],
346
versionId: this.versionId,
347
synchronous: true,
348
endSelectionState: undefined
349
});
350
}
351
}
352
353
private _bindCellContentHandler(cell: NotebookCellTextModel, e: 'content' | 'language' | 'mime' | { type: 'model'; event: IModelContentChangedEvent }) {
354
this._increaseVersionId(e === 'content' || (typeof e === 'object' && e.type === 'model'));
355
switch (e) {
356
case 'content':
357
this._pauseableEmitter.fire({
358
rawEvents: [{ kind: NotebookCellsChangeType.ChangeCellContent, index: this._getCellIndexByHandle(cell.handle), transient: false }],
359
versionId: this.versionId,
360
synchronous: true,
361
endSelectionState: undefined
362
});
363
break;
364
365
case 'language':
366
this._pauseableEmitter.fire({
367
rawEvents: [{ kind: NotebookCellsChangeType.ChangeCellLanguage, index: this._getCellIndexByHandle(cell.handle), language: cell.language, transient: false }],
368
versionId: this.versionId,
369
synchronous: true,
370
endSelectionState: undefined
371
});
372
break;
373
374
case 'mime':
375
this._pauseableEmitter.fire({
376
rawEvents: [{ kind: NotebookCellsChangeType.ChangeCellMime, index: this._getCellIndexByHandle(cell.handle), mime: cell.mime, transient: false }],
377
versionId: this.versionId,
378
synchronous: true,
379
endSelectionState: undefined
380
});
381
break;
382
383
default:
384
if (typeof e === 'object' && e.type === 'model') {
385
this._pauseableEmitter.fire({
386
rawEvents: [{ kind: NotebookCellsChangeType.ChangeCellContent, index: this._getCellIndexByHandle(cell.handle), transient: false }],
387
versionId: this.versionId,
388
synchronous: true,
389
endSelectionState: undefined
390
});
391
}
392
break;
393
}
394
}
395
396
private _generateAlternativeId() {
397
return `${this._notebookSpecificAlternativeId}_` + this.cells.map(cell => cell.handle + ',' + cell.alternativeId).join(';');
398
}
399
400
override dispose() {
401
if (this._isDisposed) {
402
// NotebookEditorModel can be disposed twice, don't fire onWillDispose again
403
return;
404
}
405
406
this._isDisposed = true;
407
this._onWillDispose.fire();
408
this._undoService.removeElements(this.uri);
409
410
dispose(this._cellListeners.values());
411
this._cellListeners.clear();
412
413
dispose(this._cells);
414
this._cells = [];
415
super.dispose();
416
}
417
418
pushStackElement() {
419
// https://github.com/microsoft/vscode/issues/207523
420
}
421
422
private _getCellIndexByHandle(handle: number) {
423
return this.cells.findIndex(c => c.handle === handle);
424
}
425
426
private _getCellIndexWithOutputIdHandleFromEdits(outputId: string, rawEdits: ICellEditOperation[]) {
427
const edit = rawEdits.find(e => 'outputs' in e && e.outputs.some(o => o.outputId === outputId));
428
if (edit) {
429
if ('index' in edit) {
430
return edit.index;
431
} else if ('handle' in edit) {
432
const cellIndex = this._getCellIndexByHandle(edit.handle);
433
this._assertIndex(cellIndex);
434
return cellIndex;
435
}
436
}
437
438
return -1;
439
}
440
441
private _getCellIndexWithOutputIdHandle(outputId: string) {
442
return this.cells.findIndex(c => !!c.outputs.find(o => o.outputId === outputId));
443
}
444
445
reset(cells: ICellDto2[], metadata: NotebookDocumentMetadata, transientOptions: TransientOptions): void {
446
this.transientOptions = transientOptions;
447
const executions = this._notebookExecutionStateService.getCellExecutionsForNotebook(this.uri);
448
const executingCellHandles = executions.filter(exe => exe.state === NotebookCellExecutionState.Executing).map(exe => exe.cellHandle);
449
const edits = NotebookTextModel.computeEdits(this, cells, executingCellHandles);
450
451
this.applyEdits(
452
[
453
...edits,
454
{ editType: CellEditType.DocumentMetadata, metadata }
455
],
456
true,
457
undefined, () => undefined,
458
undefined,
459
false
460
);
461
}
462
463
createSnapshot(options: INotebookSnapshotOptions): NotebookData {
464
const transientOptions = options.transientOptions ?? this.transientOptions;
465
const data: NotebookData = {
466
metadata: filter(this.metadata, key => !transientOptions.transientDocumentMetadata[key]),
467
cells: [],
468
};
469
470
let outputSize = 0;
471
for (const cell of this.cells) {
472
const cellData: ICellDto2 = {
473
cellKind: cell.cellKind,
474
language: cell.language,
475
mime: cell.mime,
476
source: cell.getValue(),
477
outputs: [],
478
internalMetadata: cell.internalMetadata
479
};
480
481
if (options.context === SnapshotContext.Backup && options.outputSizeLimit > 0) {
482
cell.outputs.forEach(output => {
483
output.outputs.forEach(item => {
484
outputSize += item.data.byteLength;
485
});
486
});
487
if (outputSize > options.outputSizeLimit) {
488
throw new Error('Notebook too large to backup');
489
}
490
}
491
492
cellData.outputs = !transientOptions.transientOutputs ? cell.outputs : [];
493
cellData.metadata = filter(cell.metadata, key => !transientOptions.transientCellMetadata[key]);
494
495
data.cells.push(cellData);
496
}
497
498
return data;
499
}
500
501
restoreSnapshot(snapshot: NotebookData, transientOptions?: TransientOptions): void {
502
this.reset(snapshot.cells, snapshot.metadata, transientOptions ?? this.transientOptions);
503
}
504
505
static computeEdits(model: NotebookTextModel, cells: ICellDto2[], executingHandles: number[] = []): ICellEditOperation[] {
506
const edits: ICellEditOperation[] = [];
507
const isExecuting = (cell: NotebookCellTextModel) => executingHandles.includes(cell.handle);
508
509
const commonPrefix = this._commonPrefix(model.cells, model.cells.length, 0, cells, cells.length, 0, isExecuting);
510
511
if (commonPrefix > 0) {
512
for (let i = 0; i < commonPrefix; i++) {
513
edits.push(
514
{
515
editType: CellEditType.Metadata,
516
index: i,
517
metadata: cells[i].metadata ?? {}
518
},
519
...this._computeOutputEdit(i, model.cells[i].outputs, cells[i].outputs)
520
);
521
}
522
}
523
524
if (model.cells.length === cells.length && commonPrefix === model.cells.length) {
525
return edits;
526
}
527
528
const commonSuffix = this._commonSuffix(model.cells, model.cells.length - commonPrefix, commonPrefix, cells, cells.length - commonPrefix, commonPrefix, isExecuting);
529
530
if (commonSuffix > 0) {
531
edits.push({ editType: CellEditType.Replace, index: commonPrefix, count: model.cells.length - commonPrefix - commonSuffix, cells: cells.slice(commonPrefix, cells.length - commonSuffix) });
532
} else if (commonPrefix > 0) {
533
edits.push({ editType: CellEditType.Replace, index: commonPrefix, count: model.cells.length - commonPrefix, cells: cells.slice(commonPrefix) });
534
} else {
535
edits.push({ editType: CellEditType.Replace, index: 0, count: model.cells.length, cells });
536
}
537
538
if (commonSuffix > 0) {
539
// has same suffix
540
for (let i = commonSuffix; i > 0; i--) {
541
edits.push(
542
{
543
editType: CellEditType.Metadata,
544
index: model.cells.length - i,
545
metadata: cells[cells.length - i].metadata ?? {}
546
},
547
...this._computeOutputEdit(model.cells.length - i, model.cells[model.cells.length - i].outputs, cells[cells.length - i].outputs)
548
);
549
}
550
}
551
552
return edits;
553
}
554
555
private static _computeOutputEdit(index: number, a: ICellOutput[], b: IOutputDto[]): ICellEditOperation[] {
556
if (a.length !== b.length) {
557
return [
558
{
559
editType: CellEditType.Output,
560
index: index,
561
outputs: b,
562
append: false
563
}
564
];
565
}
566
567
if (a.length === 0) {
568
// no output
569
return [];
570
}
571
572
// same length
573
return b.map((output, i) => {
574
return {
575
editType: CellEditType.OutputItems,
576
outputId: a[i].outputId,
577
items: output.outputs,
578
append: false
579
};
580
});
581
}
582
583
private static _commonPrefix(a: readonly NotebookCellTextModel[], aLen: number, aDelta: number, b: ICellDto2[], bLen: number, bDelta: number, isExecuting: (cell: NotebookCellTextModel) => boolean): number {
584
const maxResult = Math.min(aLen, bLen);
585
let result = 0;
586
for (let i = 0; i < maxResult && a[aDelta + i].fastEqual(b[bDelta + i], isExecuting(a[aDelta + i])); i++) {
587
result++;
588
}
589
590
return result;
591
}
592
593
private static _commonSuffix(a: readonly NotebookCellTextModel[], aLen: number, aDelta: number, b: ICellDto2[], bLen: number, bDelta: number, isExecuting: (cell: NotebookCellTextModel) => boolean): number {
594
const maxResult = Math.min(aLen, bLen);
595
let result = 0;
596
for (let i = 0; i < maxResult && a[aDelta + aLen - i - 1].fastEqual(b[bDelta + bLen - i - 1], isExecuting(a[aDelta + aLen - i - 1])); i++) {
597
result++;
598
}
599
return result;
600
}
601
602
private newCellsFromLastEdit = new Set<number>();
603
private isOnlyEditingMetadataOnNewCells(rawEdits: ICellEditOperation[]): boolean {
604
for (const edit of rawEdits) {
605
if (edit.editType === CellEditType.PartialInternalMetadata) {
606
continue;
607
}
608
if (edit.editType !== CellEditType.Metadata && edit.editType !== CellEditType.PartialMetadata) {
609
return false;
610
}
611
612
if (('index' in edit) && !this.newCellsFromLastEdit.has(this.cells[edit.index].handle)) {
613
return false;
614
}
615
if ('handle' in edit && !this.newCellsFromLastEdit.has(edit.handle)) {
616
return false;
617
}
618
}
619
620
return true;
621
}
622
623
applyEdits(rawEdits: ICellEditOperation[], synchronous: boolean, beginSelectionState: ISelectionState | undefined, endSelectionsComputer: () => ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined, computeUndoRedo: boolean): boolean {
624
this._pauseableEmitter.pause();
625
try {
626
this._operationManager.pushStackElement(this._alternativeVersionId, undefined);
627
628
if (computeUndoRedo && this.isOnlyEditingMetadataOnNewCells(rawEdits)) {
629
if (!this._operationManager.appendPreviousOperation()) {
630
// we can't append the previous operation, so just don't compute undo/redo
631
computeUndoRedo = false;
632
}
633
} else if (computeUndoRedo) {
634
this.newCellsFromLastEdit.clear();
635
}
636
637
try {
638
this._doApplyEdits(rawEdits, synchronous, computeUndoRedo, beginSelectionState, undoRedoGroup);
639
return true;
640
} finally {
641
if (!this._pauseableEmitter.isEmpty) {
642
// Update selection and versionId after applying edits.
643
const endSelections = endSelectionsComputer();
644
this._increaseVersionId(this._operationManager.isUndoStackEmpty() && !this._pauseableEmitter.isDirtyEvent());
645
646
// Finalize undo element
647
this._operationManager.pushStackElement(this._alternativeVersionId, endSelections);
648
649
// Broadcast changes
650
this._pauseableEmitter.fire({ rawEvents: [], versionId: this.versionId, synchronous: synchronous, endSelectionState: endSelections });
651
}
652
}
653
} finally {
654
this._pauseableEmitter.resume();
655
}
656
}
657
658
private _doApplyEdits(rawEdits: ICellEditOperation[], synchronous: boolean, computeUndoRedo: boolean, beginSelectionState: ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined): void {
659
const editsWithDetails = rawEdits.map((edit, index) => {
660
let cellIndex: number = -1;
661
if ('index' in edit) {
662
cellIndex = edit.index;
663
} else if ('handle' in edit) {
664
cellIndex = this._getCellIndexByHandle(edit.handle);
665
this._assertIndex(cellIndex);
666
} else if ('outputId' in edit) {
667
cellIndex = this._getCellIndexWithOutputIdHandle(edit.outputId);
668
if (this._indexIsInvalid(cellIndex)) {
669
// The referenced output may have been created in this batch of edits
670
cellIndex = this._getCellIndexWithOutputIdHandleFromEdits(edit.outputId, rawEdits.slice(0, index));
671
}
672
673
if (this._indexIsInvalid(cellIndex)) {
674
// It's possible for an edit to refer to an output which was just cleared, ignore it without throwing
675
return null;
676
}
677
} else if (edit.editType !== CellEditType.DocumentMetadata) {
678
throw new Error('Invalid cell edit');
679
}
680
681
return {
682
edit,
683
cellIndex,
684
end:
685
(edit.editType === CellEditType.DocumentMetadata)
686
? undefined
687
: (edit.editType === CellEditType.Replace ? edit.index + edit.count : cellIndex),
688
originalIndex: index
689
};
690
}).filter(isDefined);
691
692
// compress all edits which have no side effects on cell index
693
const edits = this._mergeCellEdits(editsWithDetails)
694
.sort((a, b) => {
695
if (a.end === undefined) {
696
return -1;
697
}
698
699
if (b.end === undefined) {
700
return -1;
701
}
702
703
return b.end - a.end || b.originalIndex - a.originalIndex;
704
}).reduce((prev, curr) => {
705
if (!prev.length) {
706
// empty
707
prev.push([curr]);
708
} else {
709
const last = prev[prev.length - 1];
710
const index = last[0].cellIndex;
711
712
if (curr.cellIndex === index) {
713
last.push(curr);
714
} else {
715
prev.push([curr]);
716
}
717
}
718
719
return prev;
720
}, [] as TransformedEdit[][]).map(editsOnSameIndex => {
721
const replaceEdits: TransformedEdit[] = [];
722
const otherEdits: TransformedEdit[] = [];
723
724
editsOnSameIndex.forEach(edit => {
725
if (edit.edit.editType === CellEditType.Replace) {
726
replaceEdits.push(edit);
727
} else {
728
otherEdits.push(edit);
729
}
730
});
731
732
return [...otherEdits.reverse(), ...replaceEdits];
733
});
734
735
const flattenEdits = edits.flat();
736
737
for (const { edit, cellIndex } of flattenEdits) {
738
switch (edit.editType) {
739
case CellEditType.Replace:
740
this._replaceCells(edit.index, edit.count, edit.cells, synchronous, computeUndoRedo, beginSelectionState, undoRedoGroup);
741
break;
742
case CellEditType.Output: {
743
this._assertIndex(cellIndex);
744
const cell = this._cells[cellIndex];
745
if (edit.append) {
746
this._spliceNotebookCellOutputs(cell, { start: cell.outputs.length, deleteCount: 0, newOutputs: edit.outputs.map(op => new NotebookCellOutputTextModel(op)) }, true, computeUndoRedo);
747
} else {
748
this._spliceNotebookCellOutputs2(cell, edit.outputs, computeUndoRedo);
749
}
750
break;
751
}
752
case CellEditType.OutputItems:
753
{
754
this._assertIndex(cellIndex);
755
const cell = this._cells[cellIndex];
756
if (edit.append) {
757
this._appendNotebookCellOutputItems(cell, edit.outputId, edit.items);
758
} else {
759
this._replaceNotebookCellOutputItems(cell, edit.outputId, edit.items);
760
}
761
}
762
break;
763
764
case CellEditType.Metadata:
765
this._assertIndex(edit.index);
766
this._changeCellMetadata(this._cells[edit.index], edit.metadata, computeUndoRedo, beginSelectionState, undoRedoGroup);
767
break;
768
case CellEditType.PartialMetadata:
769
this._assertIndex(cellIndex);
770
this._changeCellMetadataPartial(this._cells[cellIndex], edit.metadata, computeUndoRedo, beginSelectionState, undoRedoGroup);
771
break;
772
case CellEditType.PartialInternalMetadata:
773
this._assertIndex(cellIndex);
774
this._changeCellInternalMetadataPartial(this._cells[cellIndex], edit.internalMetadata);
775
break;
776
case CellEditType.CellLanguage:
777
this._assertIndex(edit.index);
778
this._changeCellLanguage(this._cells[edit.index], edit.language, computeUndoRedo, beginSelectionState, undoRedoGroup);
779
break;
780
case CellEditType.DocumentMetadata:
781
this._updateNotebookCellMetadata(edit.metadata, computeUndoRedo, beginSelectionState, undoRedoGroup);
782
break;
783
case CellEditType.Move:
784
this._moveCellToIdx(edit.index, edit.length, edit.newIdx, synchronous, computeUndoRedo, beginSelectionState, undefined, undoRedoGroup);
785
break;
786
}
787
}
788
}
789
790
private _mergeCellEdits(rawEdits: TransformedEdit[]): TransformedEdit[] {
791
const mergedEdits: TransformedEdit[] = [];
792
793
rawEdits.forEach(edit => {
794
if (mergedEdits.length) {
795
const last = mergedEdits[mergedEdits.length - 1];
796
797
if (last.edit.editType === CellEditType.Output
798
&& last.edit.append
799
&& edit.edit.editType === CellEditType.Output
800
&& edit.edit.append
801
&& last.cellIndex === edit.cellIndex
802
) {
803
last.edit.outputs = [...last.edit.outputs, ...edit.edit.outputs];
804
} else if (last.edit.editType === CellEditType.Output
805
&& !last.edit.append // last cell is not append
806
&& last.edit.outputs.length === 0 // last cell is clear outputs
807
&& edit.edit.editType === CellEditType.Output
808
&& edit.edit.append
809
&& last.cellIndex === edit.cellIndex
810
) {
811
last.edit.append = false;
812
last.edit.outputs = edit.edit.outputs;
813
} else {
814
mergedEdits.push(edit);
815
}
816
} else {
817
mergedEdits.push(edit);
818
}
819
});
820
821
return mergedEdits;
822
}
823
824
private _getDefaultCollapseState(cellDto: ICellDto2): NotebookCellCollapseState | undefined {
825
const defaultConfig = cellDto.cellKind === CellKind.Code ? this._defaultCollapseConfig?.codeCell : this._defaultCollapseConfig?.markupCell;
826
return cellDto.collapseState ?? (defaultConfig ?? undefined);
827
}
828
829
private _replaceCells(index: number, count: number, cellDtos: ICellDto2[], synchronous: boolean, computeUndoRedo: boolean, beginSelectionState: ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined): void {
830
831
if (count === 0 && cellDtos.length === 0) {
832
return;
833
}
834
835
const oldViewCells = this._cells.slice(0);
836
const oldSet = new Set();
837
oldViewCells.forEach(cell => {
838
oldSet.add(cell.handle);
839
});
840
841
// prepare remove
842
for (let i = index; i < Math.min(index + count, this._cells.length); i++) {
843
const cell = this._cells[i];
844
this._cellListeners.get(cell.handle)?.dispose();
845
this._cellListeners.delete(cell.handle);
846
}
847
848
// prepare add
849
const cells = cellDtos.map(cellDto => {
850
const cellHandle = this._cellhandlePool++;
851
const cellUri = CellUri.generate(this.uri, cellHandle);
852
const collapseState = this._getDefaultCollapseState(cellDto);
853
const cell = new NotebookCellTextModel(
854
cellUri, cellHandle,
855
cellDto.source, cellDto.language, cellDto.mime, cellDto.cellKind, cellDto.outputs || [], cellDto.metadata, cellDto.internalMetadata, collapseState, this.transientOptions,
856
this._languageService,
857
this._modelService.getCreationOptions(cellDto.language, cellUri, false).defaultEOL,
858
this._languageDetectionService
859
);
860
const textModel = this._modelService.getModel(cellUri);
861
if (textModel && textModel instanceof TextModel) {
862
cell.textModel = textModel;
863
cell.language = cellDto.language;
864
cell.textModel.setValue(cellDto.source);
865
cell.resetTextBuffer(cell.textModel.getTextBuffer());
866
}
867
const dirtyStateListener = cell.onDidChangeContent((e) => {
868
this._bindCellContentHandler(cell, e);
869
});
870
871
this.newCellsFromLastEdit.add(cell.handle);
872
this._cellListeners.set(cell.handle, dirtyStateListener);
873
this._register(cell);
874
return cell;
875
});
876
877
// compute change
878
const cellsCopy = this._cells.slice(0);
879
cellsCopy.splice(index, count, ...cells);
880
const diffs = diff(this._cells, cellsCopy, cell => {
881
return oldSet.has(cell.handle);
882
}).map(diff => {
883
return [diff.start, diff.deleteCount, diff.toInsert] as [number, number, NotebookCellTextModel[]];
884
});
885
this._onWillAddRemoveCells.fire({ rawEvent: { kind: NotebookCellsChangeType.ModelChange, changes: diffs } });
886
887
// make change
888
this._cells = cellsCopy;
889
890
const undoDiff = diffs.map(diff => {
891
const deletedCells = oldViewCells.slice(diff[0], diff[0] + diff[1]);
892
893
return [diff[0], deletedCells, diff[2]] as [number, NotebookCellTextModel[], NotebookCellTextModel[]];
894
});
895
896
if (computeUndoRedo) {
897
this._operationManager.pushEditOperation(new SpliceCellsEdit(this.uri, undoDiff, {
898
insertCell: (index, cell, endSelections) => { this._insertNewCell(index, [cell], true, endSelections); },
899
deleteCell: (index, endSelections) => { this._removeCell(index, 1, true, endSelections); },
900
replaceCell: (index, count, cells, endSelections) => { this._replaceNewCells(index, count, cells, true, endSelections); },
901
}, undefined, undefined), beginSelectionState, undefined, this._alternativeVersionId, undoRedoGroup);
902
}
903
904
// should be deferred
905
this._pauseableEmitter.fire({
906
rawEvents: [{ kind: NotebookCellsChangeType.ModelChange, changes: diffs, transient: false }],
907
versionId: this.versionId,
908
synchronous: synchronous,
909
endSelectionState: undefined
910
});
911
}
912
913
private _increaseVersionId(transient: boolean): void {
914
this._versionId = this._versionId + 1;
915
if (!transient) {
916
this._notebookSpecificAlternativeId = this._versionId;
917
}
918
this._alternativeVersionId = this._generateAlternativeId();
919
}
920
921
private _overwriteAlternativeVersionId(newAlternativeVersionId: string): void {
922
this._alternativeVersionId = newAlternativeVersionId;
923
this._notebookSpecificAlternativeId = Number(newAlternativeVersionId.substring(0, newAlternativeVersionId.indexOf('_')));
924
}
925
926
private _updateNotebookCellMetadata(metadata: NotebookDocumentMetadata, computeUndoRedo: boolean, beginSelectionState: ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined) {
927
const oldMetadata = this.metadata;
928
const triggerDirtyChange = this._isDocumentMetadataChanged(this.metadata, metadata);
929
930
if (triggerDirtyChange) {
931
if (computeUndoRedo) {
932
const that = this;
933
this._operationManager.pushEditOperation(new class implements IResourceUndoRedoElement {
934
readonly type: UndoRedoElementType.Resource = UndoRedoElementType.Resource;
935
get resource() {
936
return that.uri;
937
}
938
readonly label = 'Update Cell Metadata';
939
readonly code = 'undoredo.textBufferEdit';
940
undo() {
941
that._updateNotebookCellMetadata(oldMetadata, false, beginSelectionState, undoRedoGroup);
942
}
943
redo() {
944
that._updateNotebookCellMetadata(metadata, false, beginSelectionState, undoRedoGroup);
945
}
946
}(), beginSelectionState, undefined, this._alternativeVersionId, undoRedoGroup);
947
}
948
}
949
950
this.metadata = metadata;
951
this._pauseableEmitter.fire({
952
rawEvents: [{ kind: NotebookCellsChangeType.ChangeDocumentMetadata, metadata: this.metadata, transient: !triggerDirtyChange }],
953
versionId: this.versionId,
954
synchronous: true,
955
endSelectionState: undefined
956
});
957
}
958
959
private _insertNewCell(index: number, cells: NotebookCellTextModel[], synchronous: boolean, endSelections: ISelectionState | undefined): void {
960
for (let i = 0; i < cells.length; i++) {
961
const dirtyStateListener = cells[i].onDidChangeContent((e) => {
962
this._bindCellContentHandler(cells[i], e);
963
});
964
965
this._cellListeners.set(cells[i].handle, dirtyStateListener);
966
}
967
968
const changes: NotebookCellTextModelSplice<ICell>[] = [[index, 0, cells]];
969
this._onWillAddRemoveCells.fire({ rawEvent: { kind: NotebookCellsChangeType.ModelChange, changes } });
970
this._cells.splice(index, 0, ...cells);
971
this._pauseableEmitter.fire({
972
rawEvents: [{ kind: NotebookCellsChangeType.ModelChange, changes, transient: false }],
973
versionId: this.versionId,
974
synchronous: synchronous,
975
endSelectionState: endSelections
976
});
977
978
return;
979
}
980
981
private _removeCell(index: number, count: number, synchronous: boolean, endSelections: ISelectionState | undefined) {
982
for (let i = index; i < index + count; i++) {
983
const cell = this._cells[i];
984
this._cellListeners.get(cell.handle)?.dispose();
985
this._cellListeners.delete(cell.handle);
986
}
987
const changes: NotebookCellTextModelSplice<ICell>[] = [[index, count, []]];
988
this._onWillAddRemoveCells.fire({ rawEvent: { kind: NotebookCellsChangeType.ModelChange, changes } });
989
this._cells.splice(index, count);
990
this._pauseableEmitter.fire({
991
rawEvents: [{ kind: NotebookCellsChangeType.ModelChange, changes, transient: false }],
992
versionId: this.versionId,
993
synchronous: synchronous,
994
endSelectionState: endSelections
995
});
996
}
997
998
private _replaceNewCells(index: number, count: number, cells: NotebookCellTextModel[], synchronous: boolean, endSelections: ISelectionState | undefined) {
999
for (let i = index; i < index + count; i++) {
1000
const cell = this._cells[i];
1001
this._cellListeners.get(cell.handle)?.dispose();
1002
this._cellListeners.delete(cell.handle);
1003
}
1004
1005
for (let i = 0; i < cells.length; i++) {
1006
const dirtyStateListener = cells[i].onDidChangeContent((e) => {
1007
this._bindCellContentHandler(cells[i], e);
1008
});
1009
1010
this._cellListeners.set(cells[i].handle, dirtyStateListener);
1011
}
1012
1013
const changes: NotebookCellTextModelSplice<ICell>[] = [[index, count, cells]];
1014
this._onWillAddRemoveCells.fire({ rawEvent: { kind: NotebookCellsChangeType.ModelChange, changes } });
1015
this._cells.splice(index, count, ...cells);
1016
this._pauseableEmitter.fire({
1017
rawEvents: [{ kind: NotebookCellsChangeType.ModelChange, changes, transient: false }],
1018
versionId: this.versionId,
1019
synchronous: synchronous,
1020
endSelectionState: endSelections
1021
});
1022
}
1023
1024
private _isDocumentMetadataChanged(a: NotebookDocumentMetadata, b: NotebookDocumentMetadata) {
1025
const keys = new Set([...Object.keys(a || {}), ...Object.keys(b || {})]);
1026
for (const key of keys) {
1027
if (key === 'custom') {
1028
if (!this._customMetadataEqual(a[key], b[key])
1029
&&
1030
!(this.transientOptions.transientDocumentMetadata[key as keyof NotebookDocumentMetadata])
1031
) {
1032
return true;
1033
}
1034
} else if (
1035
(a[key as keyof NotebookDocumentMetadata] !== b[key as keyof NotebookDocumentMetadata])
1036
&&
1037
!(this.transientOptions.transientDocumentMetadata[key as keyof NotebookDocumentMetadata])
1038
) {
1039
return true;
1040
}
1041
}
1042
1043
return false;
1044
}
1045
1046
private _isCellMetadataChanged(a: NotebookCellMetadata, b: NotebookCellMetadata) {
1047
const keys = new Set([...Object.keys(a || {}), ...Object.keys(b || {})]);
1048
for (const key of keys) {
1049
if (
1050
(a[key as keyof NotebookCellMetadata] !== b[key as keyof NotebookCellMetadata])
1051
&&
1052
!(this.transientOptions.transientCellMetadata[key as keyof NotebookCellMetadata])
1053
) {
1054
return true;
1055
}
1056
}
1057
1058
return false;
1059
}
1060
1061
private _customMetadataEqual(a: any, b: any) {
1062
if (!a && !b) {
1063
// both of them are nullish or undefined
1064
return true;
1065
}
1066
1067
if (!a || !b) {
1068
return false;
1069
}
1070
1071
const aProps = Object.getOwnPropertyNames(a);
1072
const bProps = Object.getOwnPropertyNames(b);
1073
1074
if (aProps.length !== bProps.length) {
1075
return false;
1076
}
1077
1078
for (let i = 0; i < aProps.length; i++) {
1079
const propName = aProps[i];
1080
if (a[propName] !== b[propName]) {
1081
return false;
1082
}
1083
}
1084
1085
return true;
1086
}
1087
1088
private _changeCellMetadataPartial(cell: NotebookCellTextModel, metadata: NullablePartialNotebookCellMetadata, computeUndoRedo: boolean, beginSelectionState: ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined) {
1089
const newMetadata: NotebookCellMetadata = {
1090
...cell.metadata
1091
};
1092
let k: keyof NullablePartialNotebookCellMetadata;
1093
for (k in metadata) {
1094
const value = metadata[k] ?? undefined;
1095
newMetadata[k] = value as any;
1096
}
1097
1098
return this._changeCellMetadata(cell, newMetadata, computeUndoRedo, beginSelectionState, undoRedoGroup);
1099
}
1100
1101
private _changeCellMetadata(cell: NotebookCellTextModel, metadata: NotebookCellMetadata, computeUndoRedo: boolean, beginSelectionState: ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined) {
1102
const triggerDirtyChange = this._isCellMetadataChanged(cell.metadata, metadata);
1103
1104
if (triggerDirtyChange) {
1105
if (computeUndoRedo) {
1106
const index = this._cells.indexOf(cell);
1107
this._operationManager.pushEditOperation(new CellMetadataEdit(this.uri, index, Object.freeze(cell.metadata), Object.freeze(metadata), {
1108
updateCellMetadata: (index, newMetadata) => {
1109
const cell = this._cells[index];
1110
if (!cell) {
1111
return;
1112
}
1113
this._changeCellMetadata(cell, newMetadata, false, beginSelectionState, undoRedoGroup);
1114
}
1115
}), beginSelectionState, undefined, this._alternativeVersionId, undoRedoGroup);
1116
}
1117
}
1118
1119
// should be deferred
1120
cell.metadata = metadata;
1121
this._pauseableEmitter.fire({
1122
rawEvents: [{ kind: NotebookCellsChangeType.ChangeCellMetadata, index: this._cells.indexOf(cell), metadata: cell.metadata, transient: !triggerDirtyChange }],
1123
versionId: this.versionId,
1124
synchronous: true,
1125
endSelectionState: undefined
1126
});
1127
}
1128
1129
private _changeCellInternalMetadataPartial(cell: NotebookCellTextModel, internalMetadata: NullablePartialNotebookCellInternalMetadata) {
1130
const newInternalMetadata: NotebookCellInternalMetadata = {
1131
...cell.internalMetadata
1132
};
1133
let k: keyof NotebookCellInternalMetadata;
1134
for (k in internalMetadata) {
1135
const value = internalMetadata[k] ?? undefined;
1136
newInternalMetadata[k] = value as any;
1137
}
1138
1139
cell.internalMetadata = newInternalMetadata;
1140
this._pauseableEmitter.fire({
1141
rawEvents: [{ kind: NotebookCellsChangeType.ChangeCellInternalMetadata, index: this._cells.indexOf(cell), internalMetadata: cell.internalMetadata, transient: true }],
1142
versionId: this.versionId,
1143
synchronous: true,
1144
endSelectionState: undefined
1145
});
1146
}
1147
1148
private _changeCellLanguage(cell: NotebookCellTextModel, languageId: string, computeUndoRedo: boolean, beginSelectionState: ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined) {
1149
if (cell.language === languageId) {
1150
return;
1151
}
1152
1153
const oldLanguage = cell.language;
1154
cell.language = languageId;
1155
1156
if (computeUndoRedo) {
1157
const that = this;
1158
this._operationManager.pushEditOperation(new class implements IResourceUndoRedoElement {
1159
readonly type: UndoRedoElementType.Resource = UndoRedoElementType.Resource;
1160
get resource() {
1161
return that.uri;
1162
}
1163
readonly label = 'Update Cell Language';
1164
readonly code = 'undoredo.textBufferEdit';
1165
undo() {
1166
that._changeCellLanguage(cell, oldLanguage, false, beginSelectionState, undoRedoGroup);
1167
}
1168
redo() {
1169
that._changeCellLanguage(cell, languageId, false, beginSelectionState, undoRedoGroup);
1170
}
1171
}(), beginSelectionState, undefined, this._alternativeVersionId, undoRedoGroup);
1172
}
1173
1174
this._pauseableEmitter.fire({
1175
rawEvents: [{ kind: NotebookCellsChangeType.ChangeCellLanguage, index: this._cells.indexOf(cell), language: languageId, transient: false }],
1176
versionId: this.versionId,
1177
synchronous: true,
1178
endSelectionState: undefined
1179
});
1180
}
1181
1182
private _spliceNotebookCellOutputs2(cell: NotebookCellTextModel, outputs: IOutputDto[], computeUndoRedo: boolean): void {
1183
if (outputs.length === 0 && cell.outputs.length === 0) {
1184
return;
1185
}
1186
1187
if (outputs.length <= 1) {
1188
this._spliceNotebookCellOutputs(cell, { start: 0, deleteCount: cell.outputs.length, newOutputs: outputs.map(op => new NotebookCellOutputTextModel(op)) }, false, computeUndoRedo);
1189
return;
1190
}
1191
1192
const diff = new LcsDiff(new OutputSequence(cell.outputs), new OutputSequence(outputs));
1193
const diffResult = diff.ComputeDiff(false);
1194
const splices: NotebookCellOutputsSplice[] = diffResult.changes.map(change => ({
1195
start: change.originalStart,
1196
deleteCount: change.originalLength,
1197
// create cell output text model only when it's inserted into the notebook document
1198
newOutputs: outputs.slice(change.modifiedStart, change.modifiedStart + change.modifiedLength).map(op => new NotebookCellOutputTextModel(op))
1199
}));
1200
splices.reverse().forEach(splice => {
1201
this._spliceNotebookCellOutputs(cell, splice, false, computeUndoRedo);
1202
});
1203
}
1204
1205
private _spliceNotebookCellOutputs(cell: NotebookCellTextModel, splice: NotebookCellOutputsSplice, append: boolean, computeUndoRedo: boolean): void {
1206
cell.spliceNotebookCellOutputs(splice);
1207
this._pauseableEmitter.fire({
1208
rawEvents: [{
1209
kind: NotebookCellsChangeType.Output,
1210
index: this._cells.indexOf(cell),
1211
outputs: cell.outputs.map(output => output.asDto()) ?? [],
1212
append,
1213
transient: this.transientOptions.transientOutputs,
1214
}],
1215
versionId: this.versionId,
1216
synchronous: true,
1217
endSelectionState: undefined
1218
});
1219
}
1220
1221
private _appendNotebookCellOutputItems(cell: NotebookCellTextModel, outputId: string, items: IOutputItemDto[]) {
1222
if (cell.changeOutputItems(outputId, true, items)) {
1223
this._pauseableEmitter.fire({
1224
rawEvents: [{
1225
kind: NotebookCellsChangeType.OutputItem,
1226
index: this._cells.indexOf(cell),
1227
outputId: outputId,
1228
outputItems: items,
1229
append: true,
1230
transient: this.transientOptions.transientOutputs
1231
1232
}],
1233
versionId: this.versionId,
1234
synchronous: true,
1235
endSelectionState: undefined
1236
});
1237
}
1238
}
1239
1240
private _replaceNotebookCellOutputItems(cell: NotebookCellTextModel, outputId: string, items: IOutputItemDto[]) {
1241
if (cell.changeOutputItems(outputId, false, items)) {
1242
this._pauseableEmitter.fire({
1243
rawEvents: [{
1244
kind: NotebookCellsChangeType.OutputItem,
1245
index: this._cells.indexOf(cell),
1246
outputId: outputId,
1247
outputItems: items,
1248
append: false,
1249
transient: this.transientOptions.transientOutputs
1250
1251
}],
1252
versionId: this.versionId,
1253
synchronous: true,
1254
endSelectionState: undefined
1255
});
1256
}
1257
}
1258
1259
private _moveCellToIdx(index: number, length: number, newIdx: number, synchronous: boolean, pushedToUndoStack: boolean, beforeSelections: ISelectionState | undefined, endSelections: ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined): boolean {
1260
if (pushedToUndoStack) {
1261
this._operationManager.pushEditOperation(new MoveCellEdit(this.uri, index, length, newIdx, {
1262
moveCell: (fromIndex: number, length: number, toIndex: number, beforeSelections: ISelectionState | undefined, endSelections: ISelectionState | undefined) => {
1263
this._moveCellToIdx(fromIndex, length, toIndex, true, false, beforeSelections, endSelections, undoRedoGroup);
1264
},
1265
}, beforeSelections, endSelections), beforeSelections, endSelections, this._alternativeVersionId, undoRedoGroup);
1266
}
1267
1268
this._assertIndex(index);
1269
this._assertIndex(newIdx);
1270
1271
const cells = this._cells.splice(index, length);
1272
this._cells.splice(newIdx, 0, ...cells);
1273
this._pauseableEmitter.fire({
1274
rawEvents: [{ kind: NotebookCellsChangeType.Move, index, length, newIdx, cells, transient: false }],
1275
versionId: this.versionId,
1276
synchronous: synchronous,
1277
endSelectionState: endSelections
1278
});
1279
1280
return true;
1281
}
1282
1283
private _assertIndex(index: number) {
1284
if (this._indexIsInvalid(index)) {
1285
throw new Error(`model index out of range ${index}`);
1286
}
1287
}
1288
1289
private _indexIsInvalid(index: number): boolean {
1290
return index < 0 || index >= this._cells.length;
1291
}
1292
1293
//#region Find
1294
findNextMatch(searchString: string, searchStart: { cellIndex: number; position: Position }, isRegex: boolean, matchCase: boolean, wordSeparators: string | null, searchEnd?: { cellIndex: number; position: Position }): { cell: NotebookCellTextModel; match: FindMatch } | null {
1295
// check if search cell index is valid
1296
this._assertIndex(searchStart.cellIndex);
1297
const searchParams = new SearchParams(searchString, isRegex, matchCase, wordSeparators);
1298
const searchData = searchParams.parseSearchRequest();
1299
1300
if (!searchData) {
1301
return null;
1302
}
1303
1304
let cellIndex = searchStart.cellIndex;
1305
let searchStartPosition = searchStart.position;
1306
1307
let searchEndCell = this._cells.length;
1308
1309
while (cellIndex < searchEndCell) {
1310
const cell = this._cells[cellIndex];
1311
1312
// if we have wrapped back to the point of the initial search cell, we search from beginning to the provided searchEnd position
1313
const wrapFlag = searchEnd && cellIndex === searchEnd.cellIndex && searchStartPosition.isBefore(searchEnd.position);
1314
const searchRange = new Range(
1315
searchStartPosition.lineNumber,
1316
searchStartPosition.column,
1317
(wrapFlag) ? searchEnd.position.lineNumber : cell.textBuffer.getLineCount(),
1318
(wrapFlag) ? searchEnd.position.column : cell.textBuffer.getLineMaxColumn(cell.textBuffer.getLineCount())
1319
);
1320
1321
const result = cell.textBuffer.findMatchesLineByLine(searchRange, searchData, false, 1);
1322
if (result.length > 0) {
1323
return { cell, match: result[0] };
1324
} else if (wrapFlag) { // this means there are no more valid matches in the notebook
1325
break;
1326
}
1327
1328
// Move to the next cell
1329
cellIndex++;
1330
1331
// wrap if a searchEnd is provided and we are past the end of the notebook
1332
if (searchEnd && cellIndex >= this._cells.length) {
1333
cellIndex = 0;
1334
searchEndCell = searchEnd.cellIndex + 1;
1335
}
1336
1337
searchStartPosition = new Position(1, 1); // Reset position to start of the next cell
1338
}
1339
1340
return null;
1341
}
1342
1343
findMatches(searchString: string, isRegex: boolean, matchCase: boolean, wordSeparators: string | null): { cell: NotebookCellTextModel; matches: FindMatch[] }[] {
1344
const searchParams = new SearchParams(searchString, isRegex, matchCase, wordSeparators);
1345
const searchData = searchParams.parseSearchRequest();
1346
1347
if (!searchData) {
1348
return [];
1349
}
1350
1351
const results: { cell: NotebookCellTextModel; matches: FindMatch[] }[] = [];
1352
for (const cell of this._cells) {
1353
const searchRange = new Range(1, 1, cell.textBuffer.getLineCount(), cell.textBuffer.getLineMaxColumn(cell.textBuffer.getLineCount()));
1354
const matches = cell.textBuffer.findMatchesLineByLine(searchRange, searchData, false, 1000);
1355
1356
if (matches.length > 0) {
1357
results.push({ cell, matches: matches });
1358
}
1359
}
1360
1361
return results;
1362
}
1363
//#endregion
1364
}
1365
1366
class OutputSequence implements ISequence {
1367
constructor(readonly outputs: IOutputDto[]) {
1368
}
1369
1370
getElements(): Int32Array | number[] | string[] {
1371
return this.outputs.map(output => {
1372
return hash(output.outputs.map(output => ({
1373
mime: output.mime,
1374
data: output.data
1375
})));
1376
});
1377
}
1378
1379
}
1380
1381