Path: blob/main/src/vs/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts
5220 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { DisposableStore } from '../../../../../base/common/lifecycle.js';7import { URI } from '../../../../../base/common/uri.js';8import { IBulkEditService } from '../../../../../editor/browser/services/bulkEditService.js';9import { TrackedRangeStickiness } from '../../../../../editor/common/model.js';10import { IModelService } from '../../../../../editor/common/services/model.js';11import { ILanguageService } from '../../../../../editor/common/languages/language.js';12import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';13import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';14import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';15import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';16import { IThemeService } from '../../../../../platform/theme/common/themeService.js';17import { TestThemeService } from '../../../../../platform/theme/test/common/testThemeService.js';18import { IUndoRedoService } from '../../../../../platform/undoRedo/common/undoRedo.js';19import { insertCellAtIndex, runDeleteAction } from '../../browser/controller/cellOperations.js';20import { NotebookEventDispatcher } from '../../browser/viewModel/eventDispatcher.js';21import { NotebookViewModel } from '../../browser/viewModel/notebookViewModelImpl.js';22import { ViewContext } from '../../browser/viewModel/viewContext.js';23import { NotebookTextModel } from '../../common/model/notebookTextModel.js';24import { CellKind, diff } from '../../common/notebookCommon.js';25import { NotebookOptions } from '../../browser/notebookOptions.js';26import { ICellRange } from '../../common/notebookRange.js';27import { NotebookEditorTestModel, setupInstantiationService, withTestNotebook } from './testNotebookEditor.js';28import { INotebookExecutionStateService } from '../../common/notebookExecutionStateService.js';29import { IBaseCellEditorOptions } from '../../browser/notebookBrowser.js';30import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';31import { mainWindow } from '../../../../../base/browser/window.js';32import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js';33import { ILanguageDetectionService } from '../../../../services/languageDetection/common/languageDetectionWorkerService.js';34import { INotebookLoggingService } from '../../common/notebookLoggingService.js';3536suite('NotebookViewModel', () => {37ensureNoDisposablesAreLeakedInTestSuite();3839let disposables: DisposableStore;40let instantiationService: TestInstantiationService;41let textModelService: ITextModelService;42let bulkEditService: IBulkEditService;43let undoRedoService: IUndoRedoService;44let modelService: IModelService;45let languageService: ILanguageService;46let languageDetectionService: ILanguageDetectionService;47let notebookExecutionStateService: INotebookExecutionStateService;48let notebookLogger: INotebookLoggingService;4950suiteSetup(() => {51disposables = new DisposableStore();52instantiationService = setupInstantiationService(disposables);53textModelService = instantiationService.get(ITextModelService);54bulkEditService = instantiationService.get(IBulkEditService);55undoRedoService = instantiationService.get(IUndoRedoService);56modelService = instantiationService.get(IModelService);57languageService = instantiationService.get(ILanguageService);58languageDetectionService = instantiationService.get(ILanguageDetectionService);59notebookExecutionStateService = instantiationService.get(INotebookExecutionStateService);60notebookLogger = instantiationService.get(INotebookLoggingService);6162instantiationService.stub(IConfigurationService, new TestConfigurationService());63instantiationService.stub(IThemeService, new TestThemeService());64});6566suiteTeardown(() => disposables.dispose());6768test('ctor', function () {69const notebook = new NotebookTextModel('notebook', URI.parse('test'), [], {}, { transientCellMetadata: {}, transientDocumentMetadata: {}, transientOutputs: false, cellContentMetadata: {} }, undoRedoService, modelService, languageService, languageDetectionService, notebookExecutionStateService, notebookLogger);70const model = new NotebookEditorTestModel(notebook);71const options = new NotebookOptions(mainWindow, false, undefined, instantiationService.get(IConfigurationService), instantiationService.get(INotebookExecutionStateService), instantiationService.get(ICodeEditorService));72const eventDispatcher = new NotebookEventDispatcher();73const viewContext = new ViewContext(options, eventDispatcher, () => ({} as IBaseCellEditorOptions));74const viewModel = new NotebookViewModel('notebook', model.notebook, viewContext, null, { isReadOnly: false }, instantiationService, bulkEditService, undoRedoService, textModelService, notebookExecutionStateService);75assert.strictEqual(viewModel.viewType, 'notebook');76notebook.dispose();77model.dispose();78options.dispose();79eventDispatcher.dispose();80viewModel.dispose();81});8283test('insert/delete', async function () {84await withTestNotebook(85[86['var a = 1;', 'javascript', CellKind.Code, [], {}],87['var b = 2;', 'javascript', CellKind.Code, [], {}]88],89(editor, viewModel) => {90const cell = insertCellAtIndex(viewModel, 1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true);91assert.strictEqual(viewModel.length, 3);92assert.strictEqual(viewModel.notebookDocument.cells.length, 3);93assert.strictEqual(viewModel.getCellIndex(cell), 1);9495runDeleteAction(editor, viewModel.cellAt(1)!);96assert.strictEqual(viewModel.length, 2);97assert.strictEqual(viewModel.notebookDocument.cells.length, 2);98assert.strictEqual(viewModel.getCellIndex(cell), -1);99100cell.dispose();101cell.model.dispose();102}103);104});105106test('index', async function () {107await withTestNotebook(108[109['var a = 1;', 'javascript', CellKind.Code, [], {}],110['var b = 2;', 'javascript', CellKind.Code, [], {}]111],112(editor, viewModel) => {113const firstViewCell = viewModel.cellAt(0)!;114const lastViewCell = viewModel.cellAt(viewModel.length - 1)!;115116const insertIndex = viewModel.getCellIndex(firstViewCell) + 1;117const cell = insertCellAtIndex(viewModel, insertIndex, 'var c = 3;', 'javascript', CellKind.Code, {}, [], true, true);118119const addedCellIndex = viewModel.getCellIndex(cell);120runDeleteAction(editor, viewModel.cellAt(addedCellIndex)!);121122const secondInsertIndex = viewModel.getCellIndex(lastViewCell) + 1;123const cell2 = insertCellAtIndex(viewModel, secondInsertIndex, 'var d = 4;', 'javascript', CellKind.Code, {}, [], true, true);124125assert.strictEqual(viewModel.length, 3);126assert.strictEqual(viewModel.notebookDocument.cells.length, 3);127assert.strictEqual(viewModel.getCellIndex(cell2), 2);128129cell.dispose();130cell.model.dispose();131cell2.dispose();132cell2.model.dispose();133}134);135});136});137138function getVisibleCells<T>(cells: T[], hiddenRanges: ICellRange[]) {139if (!hiddenRanges.length) {140return cells;141}142143let start = 0;144let hiddenRangeIndex = 0;145const result: T[] = [];146147while (start < cells.length && hiddenRangeIndex < hiddenRanges.length) {148if (start < hiddenRanges[hiddenRangeIndex].start) {149result.push(...cells.slice(start, hiddenRanges[hiddenRangeIndex].start));150}151152start = hiddenRanges[hiddenRangeIndex].end + 1;153hiddenRangeIndex++;154}155156if (start < cells.length) {157result.push(...cells.slice(start));158}159160return result;161}162163suite('NotebookViewModel Decorations', () => {164ensureNoDisposablesAreLeakedInTestSuite();165166test('tracking range', async function () {167await withTestNotebook(168[169['var a = 1;', 'javascript', CellKind.Code, [], {}],170['var b = 2;', 'javascript', CellKind.Code, [], {}],171['var c = 3;', 'javascript', CellKind.Code, [], {}],172['var d = 4;', 'javascript', CellKind.Code, [], {}],173['var e = 5;', 'javascript', CellKind.Code, [], {}],174],175(editor, viewModel) => {176const trackedId = viewModel.setTrackedRange('test', { start: 1, end: 2 }, TrackedRangeStickiness.GrowsOnlyWhenTypingAfter);177assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), {178start: 1,179180end: 2,181});182183const cell1 = insertCellAtIndex(viewModel, 0, 'var d = 6;', 'javascript', CellKind.Code, {}, [], true, true);184assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), {185start: 2,186187end: 3188});189190runDeleteAction(editor, viewModel.cellAt(0)!);191assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), {192start: 1,193194end: 2195});196197const cell2 = insertCellAtIndex(viewModel, 3, 'var d = 7;', 'javascript', CellKind.Code, {}, [], true, true);198assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), {199start: 1,200201end: 3202});203204runDeleteAction(editor, viewModel.cellAt(3)!);205assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), {206start: 1,207208end: 2209});210211runDeleteAction(editor, viewModel.cellAt(1)!);212assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), {213start: 0,214215end: 1216});217218cell1.dispose();219cell1.model.dispose();220cell2.dispose();221cell2.model.dispose();222}223);224});225226test('tracking range 2', async function () {227await withTestNotebook(228[229['var a = 1;', 'javascript', CellKind.Code, [], {}],230['var b = 2;', 'javascript', CellKind.Code, [], {}],231['var c = 3;', 'javascript', CellKind.Code, [], {}],232['var d = 4;', 'javascript', CellKind.Code, [], {}],233['var e = 5;', 'javascript', CellKind.Code, [], {}],234['var e = 6;', 'javascript', CellKind.Code, [], {}],235['var e = 7;', 'javascript', CellKind.Code, [], {}],236],237(editor, viewModel) => {238const trackedId = viewModel.setTrackedRange('test', { start: 1, end: 3 }, TrackedRangeStickiness.GrowsOnlyWhenTypingAfter);239assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), {240start: 1,241242end: 3243});244245insertCellAtIndex(viewModel, 5, 'var d = 9;', 'javascript', CellKind.Code, {}, [], true, true);246assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), {247start: 1,248249end: 3250});251252insertCellAtIndex(viewModel, 4, 'var d = 10;', 'javascript', CellKind.Code, {}, [], true, true);253assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), {254start: 1,255256end: 4257});258}259);260});261262test('diff hidden ranges', async function () {263assert.deepStrictEqual(getVisibleCells<number>([1, 2, 3, 4, 5], []), [1, 2, 3, 4, 5]);264265assert.deepStrictEqual(266getVisibleCells<number>(267[1, 2, 3, 4, 5],268[{ start: 1, end: 2 }]269),270[1, 4, 5]271);272273assert.deepStrictEqual(274getVisibleCells<number>(275[1, 2, 3, 4, 5, 6, 7, 8, 9],276[277{ start: 1, end: 2 },278{ start: 4, end: 5 }279]280),281[1, 4, 7, 8, 9]282);283284const original = getVisibleCells<number>(285[1, 2, 3, 4, 5, 6, 7, 8, 9],286[287{ start: 1, end: 2 },288{ start: 4, end: 5 }289]290);291292const modified = getVisibleCells<number>(293[1, 2, 3, 4, 5, 6, 7, 8, 9],294[295{ start: 2, end: 4 }296]297);298299assert.deepStrictEqual(diff<number>(original, modified, (a) => {300return original.indexOf(a) >= 0;301}), [{ start: 1, deleteCount: 1, toInsert: [2, 6] }]);302});303});304305suite('NotebookViewModel API', () => {306ensureNoDisposablesAreLeakedInTestSuite();307308test('#115432, get nearest code cell', async function () {309await withTestNotebook(310[311['# header a', 'markdown', CellKind.Markup, [], {}],312['var b = 1;', 'javascript', CellKind.Code, [], {}],313['# header b', 'markdown', CellKind.Markup, [], {}],314['b = 2;', 'python', CellKind.Code, [], {}],315['var c = 3', 'javascript', CellKind.Code, [], {}],316['# header d', 'markdown', CellKind.Markup, [], {}],317['var e = 4;', 'TypeScript', CellKind.Code, [], {}],318['# header f', 'markdown', CellKind.Markup, [], {}]319],320(editor, viewModel) => {321assert.strictEqual(viewModel.nearestCodeCellIndex(0), 1);322// find the nearest code cell from above323assert.strictEqual(viewModel.nearestCodeCellIndex(2), 1);324assert.strictEqual(viewModel.nearestCodeCellIndex(4), 3);325assert.strictEqual(viewModel.nearestCodeCellIndex(5), 4);326assert.strictEqual(viewModel.nearestCodeCellIndex(6), 4);327}328);329});330331test('#108464, get nearest code cell', async function () {332await withTestNotebook(333[334['# header a', 'markdown', CellKind.Markup, [], {}],335['var b = 1;', 'javascript', CellKind.Code, [], {}],336['# header b', 'markdown', CellKind.Markup, [], {}]337],338(editor, viewModel) => {339assert.strictEqual(viewModel.nearestCodeCellIndex(2), 1);340}341);342});343344test('getCells', async () => {345await withTestNotebook(346[347['# header a', 'markdown', CellKind.Markup, [], {}],348['var b = 1;', 'javascript', CellKind.Code, [], {}],349['# header b', 'markdown', CellKind.Markup, [], {}]350],351(editor, viewModel) => {352assert.strictEqual(viewModel.getCellsInRange().length, 3);353assert.deepStrictEqual(viewModel.getCellsInRange({ start: 0, end: 1 }).map(cell => cell.getText()), ['# header a']);354assert.deepStrictEqual(viewModel.getCellsInRange({ start: 0, end: 2 }).map(cell => cell.getText()), ['# header a', 'var b = 1;']);355assert.deepStrictEqual(viewModel.getCellsInRange({ start: 0, end: 3 }).map(cell => cell.getText()), ['# header a', 'var b = 1;', '# header b']);356assert.deepStrictEqual(viewModel.getCellsInRange({ start: 0, end: 4 }).map(cell => cell.getText()), ['# header a', 'var b = 1;', '# header b']);357assert.deepStrictEqual(viewModel.getCellsInRange({ start: 1, end: 4 }).map(cell => cell.getText()), ['var b = 1;', '# header b']);358assert.deepStrictEqual(viewModel.getCellsInRange({ start: 2, end: 4 }).map(cell => cell.getText()), ['# header b']);359assert.deepStrictEqual(viewModel.getCellsInRange({ start: 3, end: 4 }).map(cell => cell.getText()), []);360361// no one should use an invalid range but `getCells` should be able to handle that.362assert.deepStrictEqual(viewModel.getCellsInRange({ start: -1, end: 1 }).map(cell => cell.getText()), ['# header a']);363assert.deepStrictEqual(viewModel.getCellsInRange({ start: 3, end: 0 }).map(cell => cell.getText()), ['# header a', 'var b = 1;', '# header b']);364}365);366});367});368369370