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