Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts
5240 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { Emitter, Event } from '../../../../../base/common/event.js';
7
import { hash, StringSHA1 } from '../../../../../base/common/hash.js';
8
import { Disposable, DisposableStore, dispose } from '../../../../../base/common/lifecycle.js';
9
import { URI } from '../../../../../base/common/uri.js';
10
import * as UUID from '../../../../../base/common/uuid.js';
11
import { Range } from '../../../../../editor/common/core/range.js';
12
import * as model from '../../../../../editor/common/model.js';
13
import { PieceTreeTextBuffer } from '../../../../../editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.js';
14
import { createTextBuffer, TextModel } from '../../../../../editor/common/model/textModel.js';
15
import { PLAINTEXT_LANGUAGE_ID } from '../../../../../editor/common/languages/modesRegistry.js';
16
import { ILanguageService } from '../../../../../editor/common/languages/language.js';
17
import { NotebookCellOutputTextModel } from './notebookCellOutputTextModel.js';
18
import { CellInternalMetadataChangedEvent, CellKind, ICell, ICellDto2, ICellOutput, IOutputItemDto, NotebookCellCollapseState, NotebookCellDefaultCollapseConfig, NotebookCellInternalMetadata, NotebookCellMetadata, NotebookCellOutputsSplice, TransientCellMetadata, TransientOptions } from '../notebookCommon.js';
19
import { ThrottledDelayer } from '../../../../../base/common/async.js';
20
import { ILanguageDetectionService } from '../../../../services/languageDetection/common/languageDetectionWorkerService.js';
21
import { toFormattedString } from '../../../../../base/common/jsonFormatter.js';
22
import { IModelContentChangedEvent } from '../../../../../editor/common/textModelEvents.js';
23
import { splitLines } from '../../../../../base/common/strings.js';
24
import { INotebookLoggingService } from '../notebookLoggingService.js';
25
26
export class NotebookCellTextModel extends Disposable implements ICell {
27
private readonly _onDidChangeTextModel = this._register(new Emitter<void>());
28
readonly onDidChangeTextModel: Event<void> = this._onDidChangeTextModel.event;
29
private readonly _onDidChangeOutputs = this._register(new Emitter<NotebookCellOutputsSplice>());
30
readonly onDidChangeOutputs: Event<NotebookCellOutputsSplice> = this._onDidChangeOutputs.event;
31
32
private readonly _onDidChangeOutputItems = this._register(new Emitter<void>());
33
readonly onDidChangeOutputItems: Event<void> = this._onDidChangeOutputItems.event;
34
35
private readonly _onDidChangeContent = this._register(new Emitter<'content' | 'language' | 'mime' | { type: 'model'; event: IModelContentChangedEvent }>());
36
readonly onDidChangeContent: Event<'content' | 'language' | 'mime' | { type: 'model'; event: IModelContentChangedEvent }> = this._onDidChangeContent.event;
37
38
private readonly _onDidChangeMetadata = this._register(new Emitter<void>());
39
readonly onDidChangeMetadata: Event<void> = this._onDidChangeMetadata.event;
40
41
private readonly _onDidChangeInternalMetadata = this._register(new Emitter<CellInternalMetadataChangedEvent>());
42
readonly onDidChangeInternalMetadata: Event<CellInternalMetadataChangedEvent> = this._onDidChangeInternalMetadata.event;
43
44
private readonly _onDidChangeLanguage = this._register(new Emitter<string>());
45
readonly onDidChangeLanguage: Event<string> = this._onDidChangeLanguage.event;
46
47
private _outputs: NotebookCellOutputTextModel[];
48
49
get outputs(): ICellOutput[] {
50
return this._outputs;
51
}
52
53
private _metadata: NotebookCellMetadata;
54
55
get metadata() {
56
return this._metadata;
57
}
58
59
set metadata(newMetadata: NotebookCellMetadata) {
60
this._metadata = newMetadata;
61
this._hash = null;
62
this._onDidChangeMetadata.fire();
63
}
64
65
private _internalMetadata: NotebookCellInternalMetadata;
66
67
get internalMetadata() {
68
return this._internalMetadata;
69
}
70
71
set internalMetadata(newInternalMetadata: NotebookCellInternalMetadata) {
72
const lastRunSuccessChanged = this._internalMetadata.lastRunSuccess !== newInternalMetadata.lastRunSuccess;
73
newInternalMetadata = {
74
...newInternalMetadata,
75
...{ runStartTimeAdjustment: computeRunStartTimeAdjustment(this._internalMetadata, newInternalMetadata) }
76
};
77
this._internalMetadata = newInternalMetadata;
78
this._hash = null;
79
this._onDidChangeInternalMetadata.fire({ lastRunSuccessChanged });
80
}
81
82
get language() {
83
return this._language;
84
}
85
86
set language(newLanguage: string) {
87
if (this._textModel
88
// 1. the language update is from workspace edit, checking if it's the same as text model's mode
89
&& this._textModel.getLanguageId() === this._languageService.getLanguageIdByLanguageName(newLanguage)
90
// 2. the text model's mode might be the same as the `this.language`, even if the language friendly name is not the same, we should not trigger an update
91
&& this._textModel.getLanguageId() === this._languageService.getLanguageIdByLanguageName(this.language)) {
92
return;
93
}
94
95
96
this._hasLanguageSetExplicitly = true;
97
this._setLanguageInternal(newLanguage);
98
}
99
100
public get mime(): string | undefined {
101
return this._mime;
102
}
103
104
public set mime(newMime: string | undefined) {
105
if (this._mime === newMime) {
106
return;
107
}
108
this._mime = newMime;
109
this._hash = null;
110
this._onDidChangeContent.fire('mime');
111
}
112
113
private _textBuffer!: model.ITextBuffer;
114
115
get textBuffer() {
116
if (this._textBuffer) {
117
return this._textBuffer;
118
}
119
120
this._textBuffer = this._register(createTextBuffer(this._source, this._defaultEOL).textBuffer);
121
122
this._register(this._textBuffer.onDidChangeContent(() => {
123
this._hash = null;
124
if (!this._textModel) {
125
this._onDidChangeContent.fire('content');
126
}
127
this.autoDetectLanguage();
128
}));
129
130
return this._textBuffer;
131
}
132
133
private _textBufferHash: string | null = null;
134
private _hash: number | null = null;
135
136
private _versionId: number = 1;
137
private _alternativeId: number = 1;
138
get alternativeId(): number {
139
return this._alternativeId;
140
}
141
142
private readonly _textModelDisposables = this._register(new DisposableStore());
143
private _textModel: TextModel | undefined = undefined;
144
get textModel(): TextModel | undefined {
145
return this._textModel;
146
}
147
148
set textModel(m: TextModel | undefined) {
149
if (this._textModel === m) {
150
return;
151
}
152
153
this._textModelDisposables.clear();
154
this._textModel = m;
155
if (this._textModel) {
156
this.setRegisteredLanguage(this._languageService, this._textModel.getLanguageId(), this.language);
157
158
// Listen to language changes on the model
159
this._textModelDisposables.add(this._textModel.onDidChangeLanguage((e) => this.setRegisteredLanguage(this._languageService, e.newLanguage, this.language)));
160
this._textModelDisposables.add(this._textModel.onWillDispose(() => this.textModel = undefined));
161
this._textModelDisposables.add(this._textModel.onDidChangeContent((e) => {
162
if (this._textModel) {
163
this._versionId = this._textModel.getVersionId();
164
this._alternativeId = this._textModel.getAlternativeVersionId();
165
}
166
this._textBufferHash = null;
167
this._onDidChangeContent.fire('content');
168
this._onDidChangeContent.fire({ type: 'model', event: e });
169
}));
170
171
this._textModel._overwriteVersionId(this._versionId);
172
this._textModel._overwriteAlternativeVersionId(this._versionId);
173
this._onDidChangeTextModel.fire();
174
}
175
}
176
177
private setRegisteredLanguage(languageService: ILanguageService, newLanguage: string, currentLanguage: string) {
178
// The language defined in the cell might not be supported in the editor so the text model might be using the default fallback
179
// If so let's not modify the language
180
const isFallBackLanguage = (newLanguage === PLAINTEXT_LANGUAGE_ID || newLanguage === 'jupyter');
181
if (!languageService.isRegisteredLanguageId(currentLanguage) && isFallBackLanguage) {
182
// notify to display warning, but don't change the language
183
this._onDidChangeLanguage.fire(currentLanguage);
184
} else {
185
this.language = newLanguage;
186
}
187
}
188
private static readonly AUTO_DETECT_LANGUAGE_THROTTLE_DELAY = 600;
189
private readonly autoDetectLanguageThrottler = this._register(new ThrottledDelayer<void>(NotebookCellTextModel.AUTO_DETECT_LANGUAGE_THROTTLE_DELAY));
190
private _autoLanguageDetectionEnabled: boolean = false;
191
private _hasLanguageSetExplicitly: boolean = false;
192
get hasLanguageSetExplicitly(): boolean { return this._hasLanguageSetExplicitly; }
193
194
private _source: string;
195
private _language: string;
196
private _mime: string | undefined;
197
public readonly cellKind: CellKind;
198
public readonly collapseState: NotebookCellCollapseState | undefined;
199
200
constructor(
201
readonly uri: URI,
202
public readonly handle: number,
203
cell: ICellDto2,
204
public readonly transientOptions: TransientOptions,
205
private readonly _languageService: ILanguageService,
206
private readonly _defaultEOL: model.DefaultEndOfLine,
207
defaultCollapseConfig: NotebookCellDefaultCollapseConfig | undefined,
208
private readonly _languageDetectionService: ILanguageDetectionService | undefined = undefined,
209
private readonly _notebookLoggingService: INotebookLoggingService
210
) {
211
super();
212
this._source = cell.source;
213
this._language = cell.language;
214
this._mime = cell.mime;
215
this.cellKind = cell.cellKind;
216
// Compute collapse state: use cell's state if provided, otherwise use default config for this cell kind
217
const defaultConfig = cell.cellKind === CellKind.Code ? defaultCollapseConfig?.codeCell : defaultCollapseConfig?.markupCell;
218
this.collapseState = cell.collapseState ?? (defaultConfig ?? undefined);
219
this._outputs = cell.outputs.map(op => new NotebookCellOutputTextModel(op));
220
this._metadata = cell.metadata ?? {};
221
this._internalMetadata = cell.internalMetadata ?? {};
222
}
223
224
enableAutoLanguageDetection() {
225
this._autoLanguageDetectionEnabled = true;
226
this.autoDetectLanguage();
227
}
228
229
async autoDetectLanguage(): Promise<void> {
230
if (this._autoLanguageDetectionEnabled) {
231
this.autoDetectLanguageThrottler.trigger(() => this._doAutoDetectLanguage());
232
}
233
}
234
235
private async _doAutoDetectLanguage(): Promise<void> {
236
if (this.hasLanguageSetExplicitly) {
237
return;
238
}
239
240
const newLanguage = await this._languageDetectionService?.detectLanguage(this.uri);
241
if (!newLanguage) {
242
return;
243
}
244
245
if (this._textModel
246
&& this._textModel.getLanguageId() === this._languageService.getLanguageIdByLanguageName(newLanguage)
247
&& this._textModel.getLanguageId() === this._languageService.getLanguageIdByLanguageName(this.language)) {
248
return;
249
}
250
251
this._setLanguageInternal(newLanguage);
252
}
253
254
private _setLanguageInternal(newLanguage: string) {
255
const newLanguageId = this._languageService.getLanguageIdByLanguageName(newLanguage);
256
257
if (newLanguageId === null) {
258
return;
259
}
260
261
if (this._textModel) {
262
const languageId = this._languageService.createById(newLanguageId);
263
this._textModel.setLanguage(languageId.languageId);
264
}
265
266
if (this._language === newLanguage) {
267
return;
268
}
269
270
this._language = newLanguage;
271
this._hash = null;
272
this._onDidChangeLanguage.fire(newLanguage);
273
this._onDidChangeContent.fire('language');
274
}
275
276
resetTextBuffer(textBuffer: model.ITextBuffer) {
277
this._textBuffer = textBuffer;
278
}
279
280
getValue(): string {
281
const fullRange = this.getFullModelRange();
282
const eol = this.textBuffer.getEOL();
283
if (eol === '\n') {
284
return this.textBuffer.getValueInRange(fullRange, model.EndOfLinePreference.LF);
285
} else {
286
return this.textBuffer.getValueInRange(fullRange, model.EndOfLinePreference.CRLF);
287
}
288
}
289
290
getTextBufferHash() {
291
if (this._textBufferHash !== null) {
292
return this._textBufferHash;
293
}
294
295
const shaComputer = new StringSHA1();
296
const snapshot = this.textBuffer.createSnapshot(false);
297
let text: string | null;
298
while ((text = snapshot.read())) {
299
shaComputer.update(text);
300
}
301
this._textBufferHash = shaComputer.digest();
302
return this._textBufferHash;
303
}
304
305
getHashValue(): number {
306
if (this._hash !== null) {
307
return this._hash;
308
}
309
310
this._hash = hash([hash(this.language), this.getTextBufferHash(), this._getPersisentMetadata(), this.transientOptions.transientOutputs ? [] : this._outputs.map(op => ({
311
outputs: op.outputs.map(output => ({
312
mime: output.mime,
313
data: Array.from(output.data.buffer)
314
})),
315
metadata: op.metadata
316
}))]);
317
return this._hash;
318
}
319
320
private _getPersisentMetadata() {
321
return getFormattedMetadataJSON(this.transientOptions.transientCellMetadata, this.metadata, this.language);
322
}
323
324
getTextLength(): number {
325
return this.textBuffer.getLength();
326
}
327
328
getFullModelRange() {
329
const lineCount = this.textBuffer.getLineCount();
330
return new Range(1, 1, lineCount, this.textBuffer.getLineLength(lineCount) + 1);
331
}
332
333
spliceNotebookCellOutputs(splice: NotebookCellOutputsSplice): void {
334
this._notebookLoggingService.trace('textModelEdits', `splicing outputs at ${splice.start} length: ${splice.deleteCount} with ${splice.newOutputs.length} new outputs`);
335
if (splice.deleteCount > 0 && splice.newOutputs.length > 0) {
336
const commonLen = Math.min(splice.deleteCount, splice.newOutputs.length);
337
// update
338
for (let i = 0; i < commonLen; i++) {
339
const currentOutput = this.outputs[splice.start + i];
340
const newOutput = splice.newOutputs[i];
341
342
this.replaceOutput(currentOutput.outputId, newOutput);
343
}
344
345
const removed = this.outputs.splice(splice.start + commonLen, splice.deleteCount - commonLen, ...splice.newOutputs.slice(commonLen));
346
removed.forEach(output => output.dispose());
347
this._onDidChangeOutputs.fire({ start: splice.start + commonLen, deleteCount: splice.deleteCount - commonLen, newOutputs: splice.newOutputs.slice(commonLen) });
348
} else {
349
const removed = this.outputs.splice(splice.start, splice.deleteCount, ...splice.newOutputs);
350
removed.forEach(output => output.dispose());
351
this._onDidChangeOutputs.fire(splice);
352
}
353
}
354
355
replaceOutput(outputId: string, newOutputItem: ICellOutput) {
356
const outputIndex = this.outputs.findIndex(output => output.outputId === outputId);
357
358
if (outputIndex < 0) {
359
return false;
360
}
361
362
this._notebookLoggingService.trace('textModelEdits', `replacing an output item at index ${outputIndex}`);
363
const output = this.outputs[outputIndex];
364
// convert to dto and dispose the cell output model
365
output.replaceData({
366
outputs: newOutputItem.outputs,
367
outputId: newOutputItem.outputId,
368
metadata: newOutputItem.metadata
369
});
370
newOutputItem.dispose();
371
this._onDidChangeOutputItems.fire();
372
return true;
373
}
374
375
changeOutputItems(outputId: string, append: boolean, items: IOutputItemDto[]): boolean {
376
const outputIndex = this.outputs.findIndex(output => output.outputId === outputId);
377
378
if (outputIndex < 0) {
379
return false;
380
}
381
382
const output = this.outputs[outputIndex];
383
this._notebookLoggingService.trace('textModelEdits', `${append ? 'appending' : 'replacing'} ${items.length} output items to for output index ${outputIndex}`);
384
if (append) {
385
output.appendData(items);
386
} else {
387
output.replaceData({ outputId: outputId, outputs: items, metadata: output.metadata });
388
}
389
this._onDidChangeOutputItems.fire();
390
return true;
391
}
392
393
private _outputNotEqualFastCheck(left: ICellOutput[], right: ICellOutput[]) {
394
if (left.length !== right.length) {
395
return false;
396
}
397
398
for (let i = 0; i < this.outputs.length; i++) {
399
const l = left[i];
400
const r = right[i];
401
402
if (l.outputs.length !== r.outputs.length) {
403
return false;
404
}
405
406
for (let k = 0; k < l.outputs.length; k++) {
407
if (l.outputs[k].mime !== r.outputs[k].mime) {
408
return false;
409
}
410
411
if (l.outputs[k].data.byteLength !== r.outputs[k].data.byteLength) {
412
return false;
413
}
414
}
415
}
416
417
return true;
418
}
419
420
equal(b: NotebookCellTextModel): boolean {
421
if (this.language !== b.language) {
422
return false;
423
}
424
425
if (this.outputs.length !== b.outputs.length) {
426
return false;
427
}
428
429
if (this.getTextLength() !== b.getTextLength()) {
430
return false;
431
}
432
433
if (!this.transientOptions.transientOutputs) {
434
// compare outputs
435
436
if (!this._outputNotEqualFastCheck(this.outputs, b.outputs)) {
437
return false;
438
}
439
}
440
441
return this.getHashValue() === b.getHashValue();
442
}
443
444
/**
445
* Only compares
446
* - language
447
* - mime
448
* - cellKind
449
* - internal metadata (conditionally)
450
* - source
451
*/
452
fastEqual(b: ICellDto2, ignoreMetadata: boolean): boolean {
453
if (this.language !== b.language) {
454
return false;
455
}
456
457
if (this.mime !== b.mime) {
458
return false;
459
}
460
461
if (this.cellKind !== b.cellKind) {
462
return false;
463
}
464
465
if (!ignoreMetadata) {
466
if (this.internalMetadata?.executionOrder !== b.internalMetadata?.executionOrder
467
|| this.internalMetadata?.lastRunSuccess !== b.internalMetadata?.lastRunSuccess
468
|| this.internalMetadata?.runStartTime !== b.internalMetadata?.runStartTime
469
|| this.internalMetadata?.runStartTimeAdjustment !== b.internalMetadata?.runStartTimeAdjustment
470
|| this.internalMetadata?.runEndTime !== b.internalMetadata?.runEndTime) {
471
return false;
472
}
473
}
474
475
// Once we attach the cell text buffer to an editor, the source of truth is the text buffer instead of the original source
476
if (this._textBuffer) {
477
if (!NotebookCellTextModel.linesAreEqual(this.textBuffer.getLinesContent(), b.source)) {
478
return false;
479
}
480
} else if (this._source !== b.source) {
481
return false;
482
}
483
484
return true;
485
}
486
487
private static linesAreEqual(aLines: string[], b: string) {
488
const bLines = splitLines(b);
489
if (aLines.length !== bLines.length) {
490
return false;
491
}
492
for (let i = 0; i < aLines.length; i++) {
493
if (aLines[i] !== bLines[i]) {
494
return false;
495
}
496
}
497
return true;
498
}
499
500
override dispose() {
501
dispose(this._outputs);
502
// Manually release reference to previous text buffer to avoid large leaks
503
// in case someone leaks a CellTextModel reference
504
const emptyDisposedTextBuffer = new PieceTreeTextBuffer([], '', '\n', false, false, true, true);
505
emptyDisposedTextBuffer.dispose();
506
this._textBuffer = emptyDisposedTextBuffer;
507
super.dispose();
508
}
509
}
510
511
export function cloneNotebookCellTextModel(cell: NotebookCellTextModel) {
512
return {
513
source: cell.getValue(),
514
language: cell.language,
515
mime: cell.mime,
516
cellKind: cell.cellKind,
517
outputs: cell.outputs.map(output => ({
518
outputs: output.outputs,
519
/* paste should generate new outputId */ outputId: UUID.generateUuid()
520
})),
521
metadata: {}
522
};
523
}
524
525
function computeRunStartTimeAdjustment(oldMetadata: NotebookCellInternalMetadata, newMetadata: NotebookCellInternalMetadata): number | undefined {
526
if (oldMetadata.runStartTime !== newMetadata.runStartTime && typeof newMetadata.runStartTime === 'number') {
527
const offset = Date.now() - newMetadata.runStartTime;
528
return offset < 0 ? Math.abs(offset) : 0;
529
} else {
530
return newMetadata.runStartTimeAdjustment;
531
}
532
}
533
534
535
export function getFormattedMetadataJSON(transientCellMetadata: TransientCellMetadata | undefined, metadata: NotebookCellMetadata, language?: string, sortKeys?: boolean): string {
536
let filteredMetadata: { [key: string]: any } = {};
537
538
if (transientCellMetadata) {
539
const keys = new Set([...Object.keys(metadata)]);
540
for (const key of keys) {
541
if (!(transientCellMetadata[key as keyof NotebookCellMetadata])
542
) {
543
filteredMetadata[key] = metadata[key as keyof NotebookCellMetadata];
544
}
545
}
546
} else {
547
filteredMetadata = metadata;
548
}
549
550
const obj = {
551
language,
552
...filteredMetadata
553
};
554
// Give preference to the language we have been given.
555
// Metadata can contain `language` due to round-tripping of cell metadata.
556
// I.e. we add it here, and then from SCM when we revert the cell, we get this same metadata back with the `language` property.
557
if (language) {
558
obj.language = language;
559
}
560
const metadataSource = toFormattedString(sortKeys ? sortObjectPropertiesRecursively(obj) : obj, {});
561
562
return metadataSource;
563
}
564
565
566
/**
567
* Sort the JSON to ensure when diffing, the JSON keys are sorted & matched correctly in diff view.
568
*/
569
export function sortObjectPropertiesRecursively(obj: any): any {
570
if (Array.isArray(obj)) {
571
return obj.map(sortObjectPropertiesRecursively);
572
}
573
if (obj !== undefined && obj !== null && typeof obj === 'object' && Object.keys(obj).length > 0) {
574
return (
575
Object.keys(obj)
576
.sort()
577
.reduce<Record<string, any>>((sortedObj, prop) => {
578
sortedObj[prop] = sortObjectPropertiesRecursively(obj[prop]);
579
return sortedObj;
580
}, {})
581
);
582
}
583
return obj;
584
}
585
586