Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts
5220 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 assert from 'assert';
7
import { DisposableStore } from '../../../../../base/common/lifecycle.js';
8
import { URI } from '../../../../../base/common/uri.js';
9
import { IBulkEditService } from '../../../../../editor/browser/services/bulkEditService.js';
10
import { TrackedRangeStickiness } from '../../../../../editor/common/model.js';
11
import { IModelService } from '../../../../../editor/common/services/model.js';
12
import { ILanguageService } from '../../../../../editor/common/languages/language.js';
13
import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';
14
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
15
import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';
16
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
17
import { IThemeService } from '../../../../../platform/theme/common/themeService.js';
18
import { TestThemeService } from '../../../../../platform/theme/test/common/testThemeService.js';
19
import { IUndoRedoService } from '../../../../../platform/undoRedo/common/undoRedo.js';
20
import { insertCellAtIndex, runDeleteAction } from '../../browser/controller/cellOperations.js';
21
import { NotebookEventDispatcher } from '../../browser/viewModel/eventDispatcher.js';
22
import { NotebookViewModel } from '../../browser/viewModel/notebookViewModelImpl.js';
23
import { ViewContext } from '../../browser/viewModel/viewContext.js';
24
import { NotebookTextModel } from '../../common/model/notebookTextModel.js';
25
import { CellKind, diff } from '../../common/notebookCommon.js';
26
import { NotebookOptions } from '../../browser/notebookOptions.js';
27
import { ICellRange } from '../../common/notebookRange.js';
28
import { NotebookEditorTestModel, setupInstantiationService, withTestNotebook } from './testNotebookEditor.js';
29
import { INotebookExecutionStateService } from '../../common/notebookExecutionStateService.js';
30
import { IBaseCellEditorOptions } from '../../browser/notebookBrowser.js';
31
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
32
import { mainWindow } from '../../../../../base/browser/window.js';
33
import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js';
34
import { ILanguageDetectionService } from '../../../../services/languageDetection/common/languageDetectionWorkerService.js';
35
import { INotebookLoggingService } from '../../common/notebookLoggingService.js';
36
37
suite('NotebookViewModel', () => {
38
ensureNoDisposablesAreLeakedInTestSuite();
39
40
let disposables: DisposableStore;
41
let instantiationService: TestInstantiationService;
42
let textModelService: ITextModelService;
43
let bulkEditService: IBulkEditService;
44
let undoRedoService: IUndoRedoService;
45
let modelService: IModelService;
46
let languageService: ILanguageService;
47
let languageDetectionService: ILanguageDetectionService;
48
let notebookExecutionStateService: INotebookExecutionStateService;
49
let notebookLogger: INotebookLoggingService;
50
51
suiteSetup(() => {
52
disposables = new DisposableStore();
53
instantiationService = setupInstantiationService(disposables);
54
textModelService = instantiationService.get(ITextModelService);
55
bulkEditService = instantiationService.get(IBulkEditService);
56
undoRedoService = instantiationService.get(IUndoRedoService);
57
modelService = instantiationService.get(IModelService);
58
languageService = instantiationService.get(ILanguageService);
59
languageDetectionService = instantiationService.get(ILanguageDetectionService);
60
notebookExecutionStateService = instantiationService.get(INotebookExecutionStateService);
61
notebookLogger = instantiationService.get(INotebookLoggingService);
62
63
instantiationService.stub(IConfigurationService, new TestConfigurationService());
64
instantiationService.stub(IThemeService, new TestThemeService());
65
});
66
67
suiteTeardown(() => disposables.dispose());
68
69
test('ctor', function () {
70
const notebook = new NotebookTextModel('notebook', URI.parse('test'), [], {}, { transientCellMetadata: {}, transientDocumentMetadata: {}, transientOutputs: false, cellContentMetadata: {} }, undoRedoService, modelService, languageService, languageDetectionService, notebookExecutionStateService, notebookLogger);
71
const model = new NotebookEditorTestModel(notebook);
72
const options = new NotebookOptions(mainWindow, false, undefined, instantiationService.get(IConfigurationService), instantiationService.get(INotebookExecutionStateService), instantiationService.get(ICodeEditorService));
73
const eventDispatcher = new NotebookEventDispatcher();
74
const viewContext = new ViewContext(options, eventDispatcher, () => ({} as IBaseCellEditorOptions));
75
const viewModel = new NotebookViewModel('notebook', model.notebook, viewContext, null, { isReadOnly: false }, instantiationService, bulkEditService, undoRedoService, textModelService, notebookExecutionStateService);
76
assert.strictEqual(viewModel.viewType, 'notebook');
77
notebook.dispose();
78
model.dispose();
79
options.dispose();
80
eventDispatcher.dispose();
81
viewModel.dispose();
82
});
83
84
test('insert/delete', async function () {
85
await withTestNotebook(
86
[
87
['var a = 1;', 'javascript', CellKind.Code, [], {}],
88
['var b = 2;', 'javascript', CellKind.Code, [], {}]
89
],
90
(editor, viewModel) => {
91
const cell = insertCellAtIndex(viewModel, 1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true);
92
assert.strictEqual(viewModel.length, 3);
93
assert.strictEqual(viewModel.notebookDocument.cells.length, 3);
94
assert.strictEqual(viewModel.getCellIndex(cell), 1);
95
96
runDeleteAction(editor, viewModel.cellAt(1)!);
97
assert.strictEqual(viewModel.length, 2);
98
assert.strictEqual(viewModel.notebookDocument.cells.length, 2);
99
assert.strictEqual(viewModel.getCellIndex(cell), -1);
100
101
cell.dispose();
102
cell.model.dispose();
103
}
104
);
105
});
106
107
test('index', async function () {
108
await withTestNotebook(
109
[
110
['var a = 1;', 'javascript', CellKind.Code, [], {}],
111
['var b = 2;', 'javascript', CellKind.Code, [], {}]
112
],
113
(editor, viewModel) => {
114
const firstViewCell = viewModel.cellAt(0)!;
115
const lastViewCell = viewModel.cellAt(viewModel.length - 1)!;
116
117
const insertIndex = viewModel.getCellIndex(firstViewCell) + 1;
118
const cell = insertCellAtIndex(viewModel, insertIndex, 'var c = 3;', 'javascript', CellKind.Code, {}, [], true, true);
119
120
const addedCellIndex = viewModel.getCellIndex(cell);
121
runDeleteAction(editor, viewModel.cellAt(addedCellIndex)!);
122
123
const secondInsertIndex = viewModel.getCellIndex(lastViewCell) + 1;
124
const cell2 = insertCellAtIndex(viewModel, secondInsertIndex, 'var d = 4;', 'javascript', CellKind.Code, {}, [], true, true);
125
126
assert.strictEqual(viewModel.length, 3);
127
assert.strictEqual(viewModel.notebookDocument.cells.length, 3);
128
assert.strictEqual(viewModel.getCellIndex(cell2), 2);
129
130
cell.dispose();
131
cell.model.dispose();
132
cell2.dispose();
133
cell2.model.dispose();
134
}
135
);
136
});
137
});
138
139
function getVisibleCells<T>(cells: T[], hiddenRanges: ICellRange[]) {
140
if (!hiddenRanges.length) {
141
return cells;
142
}
143
144
let start = 0;
145
let hiddenRangeIndex = 0;
146
const result: T[] = [];
147
148
while (start < cells.length && hiddenRangeIndex < hiddenRanges.length) {
149
if (start < hiddenRanges[hiddenRangeIndex].start) {
150
result.push(...cells.slice(start, hiddenRanges[hiddenRangeIndex].start));
151
}
152
153
start = hiddenRanges[hiddenRangeIndex].end + 1;
154
hiddenRangeIndex++;
155
}
156
157
if (start < cells.length) {
158
result.push(...cells.slice(start));
159
}
160
161
return result;
162
}
163
164
suite('NotebookViewModel Decorations', () => {
165
ensureNoDisposablesAreLeakedInTestSuite();
166
167
test('tracking range', async function () {
168
await withTestNotebook(
169
[
170
['var a = 1;', 'javascript', CellKind.Code, [], {}],
171
['var b = 2;', 'javascript', CellKind.Code, [], {}],
172
['var c = 3;', 'javascript', CellKind.Code, [], {}],
173
['var d = 4;', 'javascript', CellKind.Code, [], {}],
174
['var e = 5;', 'javascript', CellKind.Code, [], {}],
175
],
176
(editor, viewModel) => {
177
const trackedId = viewModel.setTrackedRange('test', { start: 1, end: 2 }, TrackedRangeStickiness.GrowsOnlyWhenTypingAfter);
178
assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), {
179
start: 1,
180
181
end: 2,
182
});
183
184
const cell1 = insertCellAtIndex(viewModel, 0, 'var d = 6;', 'javascript', CellKind.Code, {}, [], true, true);
185
assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), {
186
start: 2,
187
188
end: 3
189
});
190
191
runDeleteAction(editor, viewModel.cellAt(0)!);
192
assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), {
193
start: 1,
194
195
end: 2
196
});
197
198
const cell2 = insertCellAtIndex(viewModel, 3, 'var d = 7;', 'javascript', CellKind.Code, {}, [], true, true);
199
assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), {
200
start: 1,
201
202
end: 3
203
});
204
205
runDeleteAction(editor, viewModel.cellAt(3)!);
206
assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), {
207
start: 1,
208
209
end: 2
210
});
211
212
runDeleteAction(editor, viewModel.cellAt(1)!);
213
assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), {
214
start: 0,
215
216
end: 1
217
});
218
219
cell1.dispose();
220
cell1.model.dispose();
221
cell2.dispose();
222
cell2.model.dispose();
223
}
224
);
225
});
226
227
test('tracking range 2', async function () {
228
await withTestNotebook(
229
[
230
['var a = 1;', 'javascript', CellKind.Code, [], {}],
231
['var b = 2;', 'javascript', CellKind.Code, [], {}],
232
['var c = 3;', 'javascript', CellKind.Code, [], {}],
233
['var d = 4;', 'javascript', CellKind.Code, [], {}],
234
['var e = 5;', 'javascript', CellKind.Code, [], {}],
235
['var e = 6;', 'javascript', CellKind.Code, [], {}],
236
['var e = 7;', 'javascript', CellKind.Code, [], {}],
237
],
238
(editor, viewModel) => {
239
const trackedId = viewModel.setTrackedRange('test', { start: 1, end: 3 }, TrackedRangeStickiness.GrowsOnlyWhenTypingAfter);
240
assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), {
241
start: 1,
242
243
end: 3
244
});
245
246
insertCellAtIndex(viewModel, 5, 'var d = 9;', 'javascript', CellKind.Code, {}, [], true, true);
247
assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), {
248
start: 1,
249
250
end: 3
251
});
252
253
insertCellAtIndex(viewModel, 4, 'var d = 10;', 'javascript', CellKind.Code, {}, [], true, true);
254
assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), {
255
start: 1,
256
257
end: 4
258
});
259
}
260
);
261
});
262
263
test('diff hidden ranges', async function () {
264
assert.deepStrictEqual(getVisibleCells<number>([1, 2, 3, 4, 5], []), [1, 2, 3, 4, 5]);
265
266
assert.deepStrictEqual(
267
getVisibleCells<number>(
268
[1, 2, 3, 4, 5],
269
[{ start: 1, end: 2 }]
270
),
271
[1, 4, 5]
272
);
273
274
assert.deepStrictEqual(
275
getVisibleCells<number>(
276
[1, 2, 3, 4, 5, 6, 7, 8, 9],
277
[
278
{ start: 1, end: 2 },
279
{ start: 4, end: 5 }
280
]
281
),
282
[1, 4, 7, 8, 9]
283
);
284
285
const original = getVisibleCells<number>(
286
[1, 2, 3, 4, 5, 6, 7, 8, 9],
287
[
288
{ start: 1, end: 2 },
289
{ start: 4, end: 5 }
290
]
291
);
292
293
const modified = getVisibleCells<number>(
294
[1, 2, 3, 4, 5, 6, 7, 8, 9],
295
[
296
{ start: 2, end: 4 }
297
]
298
);
299
300
assert.deepStrictEqual(diff<number>(original, modified, (a) => {
301
return original.indexOf(a) >= 0;
302
}), [{ start: 1, deleteCount: 1, toInsert: [2, 6] }]);
303
});
304
});
305
306
suite('NotebookViewModel API', () => {
307
ensureNoDisposablesAreLeakedInTestSuite();
308
309
test('#115432, get nearest code cell', async function () {
310
await withTestNotebook(
311
[
312
['# header a', 'markdown', CellKind.Markup, [], {}],
313
['var b = 1;', 'javascript', CellKind.Code, [], {}],
314
['# header b', 'markdown', CellKind.Markup, [], {}],
315
['b = 2;', 'python', CellKind.Code, [], {}],
316
['var c = 3', 'javascript', CellKind.Code, [], {}],
317
['# header d', 'markdown', CellKind.Markup, [], {}],
318
['var e = 4;', 'TypeScript', CellKind.Code, [], {}],
319
['# header f', 'markdown', CellKind.Markup, [], {}]
320
],
321
(editor, viewModel) => {
322
assert.strictEqual(viewModel.nearestCodeCellIndex(0), 1);
323
// find the nearest code cell from above
324
assert.strictEqual(viewModel.nearestCodeCellIndex(2), 1);
325
assert.strictEqual(viewModel.nearestCodeCellIndex(4), 3);
326
assert.strictEqual(viewModel.nearestCodeCellIndex(5), 4);
327
assert.strictEqual(viewModel.nearestCodeCellIndex(6), 4);
328
}
329
);
330
});
331
332
test('#108464, get nearest code cell', async function () {
333
await withTestNotebook(
334
[
335
['# header a', 'markdown', CellKind.Markup, [], {}],
336
['var b = 1;', 'javascript', CellKind.Code, [], {}],
337
['# header b', 'markdown', CellKind.Markup, [], {}]
338
],
339
(editor, viewModel) => {
340
assert.strictEqual(viewModel.nearestCodeCellIndex(2), 1);
341
}
342
);
343
});
344
345
test('getCells', async () => {
346
await withTestNotebook(
347
[
348
['# header a', 'markdown', CellKind.Markup, [], {}],
349
['var b = 1;', 'javascript', CellKind.Code, [], {}],
350
['# header b', 'markdown', CellKind.Markup, [], {}]
351
],
352
(editor, viewModel) => {
353
assert.strictEqual(viewModel.getCellsInRange().length, 3);
354
assert.deepStrictEqual(viewModel.getCellsInRange({ start: 0, end: 1 }).map(cell => cell.getText()), ['# header a']);
355
assert.deepStrictEqual(viewModel.getCellsInRange({ start: 0, end: 2 }).map(cell => cell.getText()), ['# header a', 'var b = 1;']);
356
assert.deepStrictEqual(viewModel.getCellsInRange({ start: 0, end: 3 }).map(cell => cell.getText()), ['# header a', 'var b = 1;', '# header b']);
357
assert.deepStrictEqual(viewModel.getCellsInRange({ start: 0, end: 4 }).map(cell => cell.getText()), ['# header a', 'var b = 1;', '# header b']);
358
assert.deepStrictEqual(viewModel.getCellsInRange({ start: 1, end: 4 }).map(cell => cell.getText()), ['var b = 1;', '# header b']);
359
assert.deepStrictEqual(viewModel.getCellsInRange({ start: 2, end: 4 }).map(cell => cell.getText()), ['# header b']);
360
assert.deepStrictEqual(viewModel.getCellsInRange({ start: 3, end: 4 }).map(cell => cell.getText()), []);
361
362
// no one should use an invalid range but `getCells` should be able to handle that.
363
assert.deepStrictEqual(viewModel.getCellsInRange({ start: -1, end: 1 }).map(cell => cell.getText()), ['# header a']);
364
assert.deepStrictEqual(viewModel.getCellsInRange({ start: 3, end: 0 }).map(cell => cell.getText()), ['# header a', 'var b = 1;', '# header b']);
365
}
366
);
367
});
368
});
369
370