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