Path: blob/main/src/vs/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts
3296 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';3435suite('NotebookViewModel', () => {36ensureNoDisposablesAreLeakedInTestSuite();3738let disposables: DisposableStore;39let instantiationService: TestInstantiationService;40let textModelService: ITextModelService;41let bulkEditService: IBulkEditService;42let undoRedoService: IUndoRedoService;43let modelService: IModelService;44let languageService: ILanguageService;45let languageDetectionService: ILanguageDetectionService;46let notebookExecutionStateService: INotebookExecutionStateService;4748suiteSetup(() => {49disposables = new DisposableStore();50instantiationService = setupInstantiationService(disposables);51textModelService = instantiationService.get(ITextModelService);52bulkEditService = instantiationService.get(IBulkEditService);53undoRedoService = instantiationService.get(IUndoRedoService);54modelService = instantiationService.get(IModelService);55languageService = instantiationService.get(ILanguageService);56languageDetectionService = instantiationService.get(ILanguageDetectionService);57notebookExecutionStateService = instantiationService.get(INotebookExecutionStateService);5859instantiationService.stub(IConfigurationService, new TestConfigurationService());60instantiationService.stub(IThemeService, new TestThemeService());61});6263suiteTeardown(() => disposables.dispose());6465test('ctor', function () {66const notebook = new NotebookTextModel('notebook', URI.parse('test'), [], {}, { transientCellMetadata: {}, transientDocumentMetadata: {}, transientOutputs: false, cellContentMetadata: {} }, undoRedoService, modelService, languageService, languageDetectionService, notebookExecutionStateService);67const model = new NotebookEditorTestModel(notebook);68const options = new NotebookOptions(mainWindow, false, undefined, instantiationService.get(IConfigurationService), instantiationService.get(INotebookExecutionStateService), instantiationService.get(ICodeEditorService));69const eventDispatcher = new NotebookEventDispatcher();70const viewContext = new ViewContext(options, eventDispatcher, () => ({} as IBaseCellEditorOptions));71const viewModel = new NotebookViewModel('notebook', model.notebook, viewContext, null, { isReadOnly: false }, instantiationService, bulkEditService, undoRedoService, textModelService, notebookExecutionStateService);72assert.strictEqual(viewModel.viewType, 'notebook');73notebook.dispose();74model.dispose();75options.dispose();76eventDispatcher.dispose();77viewModel.dispose();78});7980test('insert/delete', async function () {81await withTestNotebook(82[83['var a = 1;', 'javascript', CellKind.Code, [], {}],84['var b = 2;', 'javascript', CellKind.Code, [], {}]85],86(editor, viewModel) => {87const cell = insertCellAtIndex(viewModel, 1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true);88assert.strictEqual(viewModel.length, 3);89assert.strictEqual(viewModel.notebookDocument.cells.length, 3);90assert.strictEqual(viewModel.getCellIndex(cell), 1);9192runDeleteAction(editor, viewModel.cellAt(1)!);93assert.strictEqual(viewModel.length, 2);94assert.strictEqual(viewModel.notebookDocument.cells.length, 2);95assert.strictEqual(viewModel.getCellIndex(cell), -1);9697cell.dispose();98cell.model.dispose();99}100);101});102103test('index', async function () {104await withTestNotebook(105[106['var a = 1;', 'javascript', CellKind.Code, [], {}],107['var b = 2;', 'javascript', CellKind.Code, [], {}]108],109(editor, viewModel) => {110const firstViewCell = viewModel.cellAt(0)!;111const lastViewCell = viewModel.cellAt(viewModel.length - 1)!;112113const insertIndex = viewModel.getCellIndex(firstViewCell) + 1;114const cell = insertCellAtIndex(viewModel, insertIndex, 'var c = 3;', 'javascript', CellKind.Code, {}, [], true, true);115116const addedCellIndex = viewModel.getCellIndex(cell);117runDeleteAction(editor, viewModel.cellAt(addedCellIndex)!);118119const secondInsertIndex = viewModel.getCellIndex(lastViewCell) + 1;120const cell2 = insertCellAtIndex(viewModel, secondInsertIndex, 'var d = 4;', 'javascript', CellKind.Code, {}, [], true, true);121122assert.strictEqual(viewModel.length, 3);123assert.strictEqual(viewModel.notebookDocument.cells.length, 3);124assert.strictEqual(viewModel.getCellIndex(cell2), 2);125126cell.dispose();127cell.model.dispose();128cell2.dispose();129cell2.model.dispose();130}131);132});133});134135function getVisibleCells<T>(cells: T[], hiddenRanges: ICellRange[]) {136if (!hiddenRanges.length) {137return cells;138}139140let start = 0;141let hiddenRangeIndex = 0;142const result: T[] = [];143144while (start < cells.length && hiddenRangeIndex < hiddenRanges.length) {145if (start < hiddenRanges[hiddenRangeIndex].start) {146result.push(...cells.slice(start, hiddenRanges[hiddenRangeIndex].start));147}148149start = hiddenRanges[hiddenRangeIndex].end + 1;150hiddenRangeIndex++;151}152153if (start < cells.length) {154result.push(...cells.slice(start));155}156157return result;158}159160suite('NotebookViewModel Decorations', () => {161ensureNoDisposablesAreLeakedInTestSuite();162163test('tracking range', async function () {164await withTestNotebook(165[166['var a = 1;', 'javascript', CellKind.Code, [], {}],167['var b = 2;', 'javascript', CellKind.Code, [], {}],168['var c = 3;', 'javascript', CellKind.Code, [], {}],169['var d = 4;', 'javascript', CellKind.Code, [], {}],170['var e = 5;', 'javascript', CellKind.Code, [], {}],171],172(editor, viewModel) => {173const trackedId = viewModel.setTrackedRange('test', { start: 1, end: 2 }, TrackedRangeStickiness.GrowsOnlyWhenTypingAfter);174assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), {175start: 1,176177end: 2,178});179180const cell1 = insertCellAtIndex(viewModel, 0, 'var d = 6;', 'javascript', CellKind.Code, {}, [], true, true);181assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), {182start: 2,183184end: 3185});186187runDeleteAction(editor, viewModel.cellAt(0)!);188assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), {189start: 1,190191end: 2192});193194const cell2 = insertCellAtIndex(viewModel, 3, 'var d = 7;', 'javascript', CellKind.Code, {}, [], true, true);195assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), {196start: 1,197198end: 3199});200201runDeleteAction(editor, viewModel.cellAt(3)!);202assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), {203start: 1,204205end: 2206});207208runDeleteAction(editor, viewModel.cellAt(1)!);209assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), {210start: 0,211212end: 1213});214215cell1.dispose();216cell1.model.dispose();217cell2.dispose();218cell2.model.dispose();219}220);221});222223test('tracking range 2', async function () {224await withTestNotebook(225[226['var a = 1;', 'javascript', CellKind.Code, [], {}],227['var b = 2;', 'javascript', CellKind.Code, [], {}],228['var c = 3;', 'javascript', CellKind.Code, [], {}],229['var d = 4;', 'javascript', CellKind.Code, [], {}],230['var e = 5;', 'javascript', CellKind.Code, [], {}],231['var e = 6;', 'javascript', CellKind.Code, [], {}],232['var e = 7;', 'javascript', CellKind.Code, [], {}],233],234(editor, viewModel) => {235const trackedId = viewModel.setTrackedRange('test', { start: 1, end: 3 }, TrackedRangeStickiness.GrowsOnlyWhenTypingAfter);236assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), {237start: 1,238239end: 3240});241242insertCellAtIndex(viewModel, 5, 'var d = 9;', 'javascript', CellKind.Code, {}, [], true, true);243assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), {244start: 1,245246end: 3247});248249insertCellAtIndex(viewModel, 4, 'var d = 10;', 'javascript', CellKind.Code, {}, [], true, true);250assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), {251start: 1,252253end: 4254});255}256);257});258259test('diff hidden ranges', async function () {260assert.deepStrictEqual(getVisibleCells<number>([1, 2, 3, 4, 5], []), [1, 2, 3, 4, 5]);261262assert.deepStrictEqual(263getVisibleCells<number>(264[1, 2, 3, 4, 5],265[{ start: 1, end: 2 }]266),267[1, 4, 5]268);269270assert.deepStrictEqual(271getVisibleCells<number>(272[1, 2, 3, 4, 5, 6, 7, 8, 9],273[274{ start: 1, end: 2 },275{ start: 4, end: 5 }276]277),278[1, 4, 7, 8, 9]279);280281const original = getVisibleCells<number>(282[1, 2, 3, 4, 5, 6, 7, 8, 9],283[284{ start: 1, end: 2 },285{ start: 4, end: 5 }286]287);288289const modified = getVisibleCells<number>(290[1, 2, 3, 4, 5, 6, 7, 8, 9],291[292{ start: 2, end: 4 }293]294);295296assert.deepStrictEqual(diff<number>(original, modified, (a) => {297return original.indexOf(a) >= 0;298}), [{ start: 1, deleteCount: 1, toInsert: [2, 6] }]);299});300});301302suite('NotebookViewModel API', () => {303ensureNoDisposablesAreLeakedInTestSuite();304305test('#115432, get nearest code cell', async function () {306await withTestNotebook(307[308['# header a', 'markdown', CellKind.Markup, [], {}],309['var b = 1;', 'javascript', CellKind.Code, [], {}],310['# header b', 'markdown', CellKind.Markup, [], {}],311['b = 2;', 'python', CellKind.Code, [], {}],312['var c = 3', 'javascript', CellKind.Code, [], {}],313['# header d', 'markdown', CellKind.Markup, [], {}],314['var e = 4;', 'TypeScript', CellKind.Code, [], {}],315['# header f', 'markdown', CellKind.Markup, [], {}]316],317(editor, viewModel) => {318assert.strictEqual(viewModel.nearestCodeCellIndex(0), 1);319// find the nearest code cell from above320assert.strictEqual(viewModel.nearestCodeCellIndex(2), 1);321assert.strictEqual(viewModel.nearestCodeCellIndex(4), 3);322assert.strictEqual(viewModel.nearestCodeCellIndex(5), 4);323assert.strictEqual(viewModel.nearestCodeCellIndex(6), 4);324}325);326});327328test('#108464, get nearest code cell', async function () {329await withTestNotebook(330[331['# header a', 'markdown', CellKind.Markup, [], {}],332['var b = 1;', 'javascript', CellKind.Code, [], {}],333['# header b', 'markdown', CellKind.Markup, [], {}]334],335(editor, viewModel) => {336assert.strictEqual(viewModel.nearestCodeCellIndex(2), 1);337}338);339});340341test('getCells', async () => {342await withTestNotebook(343[344['# header a', 'markdown', CellKind.Markup, [], {}],345['var b = 1;', 'javascript', CellKind.Code, [], {}],346['# header b', 'markdown', CellKind.Markup, [], {}]347],348(editor, viewModel) => {349assert.strictEqual(viewModel.getCellsInRange().length, 3);350assert.deepStrictEqual(viewModel.getCellsInRange({ start: 0, end: 1 }).map(cell => cell.getText()), ['# header a']);351assert.deepStrictEqual(viewModel.getCellsInRange({ start: 0, end: 2 }).map(cell => cell.getText()), ['# header a', 'var b = 1;']);352assert.deepStrictEqual(viewModel.getCellsInRange({ start: 0, end: 3 }).map(cell => cell.getText()), ['# header a', 'var b = 1;', '# header b']);353assert.deepStrictEqual(viewModel.getCellsInRange({ start: 0, end: 4 }).map(cell => cell.getText()), ['# header a', 'var b = 1;', '# header b']);354assert.deepStrictEqual(viewModel.getCellsInRange({ start: 1, end: 4 }).map(cell => cell.getText()), ['var b = 1;', '# header b']);355assert.deepStrictEqual(viewModel.getCellsInRange({ start: 2, end: 4 }).map(cell => cell.getText()), ['# header b']);356assert.deepStrictEqual(viewModel.getCellsInRange({ start: 3, end: 4 }).map(cell => cell.getText()), []);357358// no one should use an invalid range but `getCells` should be able to handle that.359assert.deepStrictEqual(viewModel.getCellsInRange({ start: -1, end: 1 }).map(cell => cell.getText()), ['# header a']);360assert.deepStrictEqual(viewModel.getCellsInRange({ start: 3, end: 0 }).map(cell => cell.getText()), ['# header a', 'var b = 1;', '# header b']);361}362);363});364});365366367