Path: blob/main/src/vs/workbench/contrib/notebook/test/browser/notebookSelection.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 { ILanguageService } from '../../../../../editor/common/languages/language.js';7import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';8import { FoldingModel, updateFoldingStateAtIndex } from '../../browser/viewModel/foldingModel.js';9import { runDeleteAction } from '../../browser/controller/cellOperations.js';10import { NotebookCellSelectionCollection } from '../../browser/viewModel/cellSelectionCollection.js';11import { CellEditType, CellKind, SelectionStateType } from '../../common/notebookCommon.js';12import { createNotebookCellList, setupInstantiationService, TestCell, withTestNotebook } from './testNotebookEditor.js';13import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';14import { DisposableStore } from '../../../../../base/common/lifecycle.js';1516suite('NotebookSelection', () => {17const store = ensureNoDisposablesAreLeakedInTestSuite();1819test('focus is never empty', function () {20const selectionCollection = new NotebookCellSelectionCollection();21assert.deepStrictEqual(selectionCollection.focus, { start: 0, end: 0 });2223selectionCollection.setState(null, [], true, 'model');24assert.deepStrictEqual(selectionCollection.focus, { start: 0, end: 0 });25selectionCollection.dispose();26});2728test('selection is never empty', function () {29const selectionCollection = new NotebookCellSelectionCollection();30assert.deepStrictEqual(selectionCollection.selections, [{ start: 0, end: 0 }]);3132selectionCollection.setState(null, [], true, 'model');33assert.deepStrictEqual(selectionCollection.selections, [{ start: 0, end: 0 }]);34selectionCollection.dispose();35});3637test('selections does not change when setting to empty', function () {38const selectionCollection = new NotebookCellSelectionCollection();39let changed = false;40store.add(selectionCollection.onDidChangeSelection(() => {41changed = true;42}));4344selectionCollection.setState(null, [], false, 'model');45assert.strictEqual(changed, false);46selectionCollection.setState({ start: 0, end: 0 }, [], false, 'model');47assert.strictEqual(changed, false);48selectionCollection.setState({ start: 0, end: 0 }, [{ start: 0, end: 0 }], false, 'model');49assert.strictEqual(changed, false);50selectionCollection.setState(null, [], false, 'model');51assert.strictEqual(changed, false);52selectionCollection.dispose();53});5455test('event fires when selection or focus changes', function () {56const selectionCollection = new NotebookCellSelectionCollection();57let eventCount = 0;58store.add(selectionCollection.onDidChangeSelection(() => {59eventCount++;60}));6162// Change focus63selectionCollection.setState({ start: 1, end: 1 }, [{ start: 1, end: 2 }], false, 'model');64assert.strictEqual(eventCount, 1);6566// Change selections67selectionCollection.setState({ start: 1, end: 1 }, [{ start: 1, end: 2 }, { start: 2, end: 3 }], false, 'model');68assert.strictEqual(eventCount, 2);6970// no change71selectionCollection.setState({ start: 1, end: 1 }, [{ start: 1, end: 2 }, { start: 2, end: 3 }], false, 'model');72assert.strictEqual(eventCount, 2);7374// change to empty focus75selectionCollection.setState({ start: 0, end: 0 }, [{ start: 4, end: 5 }], false, 'model');76assert.strictEqual(eventCount, 3);7778// change to empty selections79selectionCollection.setState({ start: 0, end: 0 }, [], false, 'model');80assert.strictEqual(eventCount, 4);8182selectionCollection.dispose();83});8485});8687suite('NotebookCellList focus/selection', () => {88let disposables: DisposableStore;89let instantiationService: TestInstantiationService;90let languageService: ILanguageService;9192teardown(() => {93disposables.dispose();94});9596ensureNoDisposablesAreLeakedInTestSuite();9798setup(() => {99disposables = new DisposableStore();100instantiationService = setupInstantiationService(disposables);101languageService = instantiationService.get(ILanguageService);102});103104105test('notebook cell list setFocus', async function () {106await withTestNotebook(107[108['var a = 1;', 'javascript', CellKind.Code, [], {}],109['var b = 2;', 'javascript', CellKind.Code, [], {}]110],111(editor, viewModel, ds) => {112const cellList = createNotebookCellList(instantiationService, ds);113cellList.attachViewModel(viewModel);114115assert.strictEqual(cellList.length, 2);116cellList.setFocus([0]);117assert.deepStrictEqual(viewModel.getFocus(), { start: 0, end: 1 });118119cellList.setFocus([1]);120assert.deepStrictEqual(viewModel.getFocus(), { start: 1, end: 2 });121cellList.detachViewModel();122});123});124125test('notebook cell list setSelections', async function () {126await withTestNotebook(127[128['var a = 1;', 'javascript', CellKind.Code, [], {}],129['var b = 2;', 'javascript', CellKind.Code, [], {}]130],131(editor, viewModel, ds) => {132const cellList = createNotebookCellList(instantiationService, ds);133cellList.attachViewModel(viewModel);134135assert.strictEqual(cellList.length, 2);136cellList.setSelection([0]);137// the only selection is also the focus138assert.deepStrictEqual(viewModel.getSelections(), [{ start: 0, end: 1 }]);139140// set selection does not modify focus141cellList.setSelection([1]);142assert.deepStrictEqual(viewModel.getSelections(), [{ start: 1, end: 2 }]);143});144});145146test('notebook cell list setFocus2', async function () {147await withTestNotebook(148[149['var a = 1;', 'javascript', CellKind.Code, [], {}],150['var b = 2;', 'javascript', CellKind.Code, [], {}]151],152(editor, viewModel, ds) => {153const cellList = createNotebookCellList(instantiationService, ds);154cellList.attachViewModel(viewModel);155156assert.strictEqual(cellList.length, 2);157cellList.setFocus([0]);158assert.deepStrictEqual(viewModel.getFocus(), { start: 0, end: 1 });159160cellList.setFocus([1]);161assert.deepStrictEqual(viewModel.getFocus(), { start: 1, end: 2 });162163cellList.setSelection([1]);164assert.deepStrictEqual(viewModel.getSelections(), [{ start: 1, end: 2 }]);165cellList.detachViewModel();166});167});168169170test('notebook cell list focus/selection from UI', async function () {171await withTestNotebook(172[173['# header a', 'markdown', CellKind.Markup, [], {}],174['var b = 1;', 'javascript', CellKind.Code, [], {}],175['# header b', 'markdown', CellKind.Markup, [], {}],176['var b = 2;', 'javascript', CellKind.Code, [], {}],177['# header c', 'markdown', CellKind.Markup, [], {}]178],179(editor, viewModel, ds) => {180const cellList = createNotebookCellList(instantiationService, ds);181cellList.attachViewModel(viewModel);182assert.deepStrictEqual(viewModel.getFocus(), { start: 0, end: 1 });183assert.deepStrictEqual(viewModel.getSelections(), [{ start: 0, end: 1 }]);184185// arrow down, move both focus and selections186cellList.setFocus([1], new KeyboardEvent('keydown'), undefined);187cellList.setSelection([1], new KeyboardEvent('keydown'), undefined);188assert.deepStrictEqual(viewModel.getFocus(), { start: 1, end: 2 });189assert.deepStrictEqual(viewModel.getSelections(), [{ start: 1, end: 2 }]);190191// shift+arrow down, expands selection192cellList.setFocus([2], new KeyboardEvent('keydown'), undefined);193cellList.setSelection([1, 2]);194assert.deepStrictEqual(viewModel.getFocus(), { start: 2, end: 3 });195assert.deepStrictEqual(viewModel.getSelections(), [{ start: 1, end: 3 }]);196197// arrow down, will move focus but not expand selection198cellList.setFocus([3], new KeyboardEvent('keydown'), undefined);199assert.deepStrictEqual(viewModel.getFocus(), { start: 3, end: 4 });200assert.deepStrictEqual(viewModel.getSelections(), [{ start: 1, end: 3 }]);201});202});203204205test('notebook cell list focus/selection with folding regions', async function () {206await withTestNotebook(207[208['# header a', 'markdown', CellKind.Markup, [], {}],209['var b = 1;', 'javascript', CellKind.Code, [], {}],210['# header b', 'markdown', CellKind.Markup, [], {}],211['var b = 2;', 'javascript', CellKind.Code, [], {}],212['# header c', 'markdown', CellKind.Markup, [], {}]213],214(editor, viewModel, ds) => {215const foldingModel = ds.add(new FoldingModel());216foldingModel.attachViewModel(viewModel);217218const cellList = createNotebookCellList(instantiationService, ds);219cellList.attachViewModel(viewModel);220assert.strictEqual(cellList.length, 5);221assert.deepStrictEqual(viewModel.getFocus(), { start: 0, end: 1 });222assert.deepStrictEqual(viewModel.getSelections(), [{ start: 0, end: 1 }]);223cellList.setFocus([0]);224225updateFoldingStateAtIndex(foldingModel, 0, true);226updateFoldingStateAtIndex(foldingModel, 2, true);227viewModel.updateFoldingRanges(foldingModel.regions);228cellList.setHiddenAreas(viewModel.getHiddenRanges(), true);229assert.strictEqual(cellList.length, 3);230231// currently, focus on a folded cell will only focus the cell itself, excluding its "inner" cells232assert.deepStrictEqual(viewModel.getFocus(), { start: 0, end: 1 });233assert.deepStrictEqual(viewModel.getSelections(), [{ start: 0, end: 1 }]);234235cellList.focusNext(1, false);236// focus next should skip the folded items237assert.deepStrictEqual(viewModel.getFocus(), { start: 2, end: 3 });238assert.deepStrictEqual(viewModel.getSelections(), [{ start: 0, end: 1 }]);239240// unfold241updateFoldingStateAtIndex(foldingModel, 2, false);242viewModel.updateFoldingRanges(foldingModel.regions);243cellList.setHiddenAreas(viewModel.getHiddenRanges(), true);244assert.strictEqual(cellList.length, 4);245assert.deepStrictEqual(viewModel.getFocus(), { start: 2, end: 3 });246});247});248249test('notebook cell list focus/selection with folding regions and applyEdits', async function () {250await withTestNotebook(251[252['# header a', 'markdown', CellKind.Markup, [], {}],253['var b = 1;', 'javascript', CellKind.Code, [], {}],254['# header b', 'markdown', CellKind.Markup, [], {}],255['var b = 2;', 'javascript', CellKind.Code, [], {}],256['var c = 3', 'javascript', CellKind.Markup, [], {}],257['# header d', 'markdown', CellKind.Markup, [], {}],258['var e = 4;', 'javascript', CellKind.Code, [], {}],259],260(editor, viewModel, ds) => {261const foldingModel = ds.add(new FoldingModel());262foldingModel.attachViewModel(viewModel);263264const cellList = createNotebookCellList(instantiationService, ds);265cellList.attachViewModel(viewModel);266cellList.setFocus([0]);267cellList.setSelection([0]);268269updateFoldingStateAtIndex(foldingModel, 0, true);270updateFoldingStateAtIndex(foldingModel, 2, true);271viewModel.updateFoldingRanges(foldingModel.regions);272cellList.setHiddenAreas(viewModel.getHiddenRanges(), true);273assert.strictEqual(cellList.getModelIndex2(0), 0);274assert.strictEqual(cellList.getModelIndex2(1), 2);275276editor.textModel.applyEdits([{277editType: CellEditType.Replace, index: 0, count: 2, cells: []278}], true, undefined, () => undefined, undefined, false);279viewModel.updateFoldingRanges(foldingModel.regions);280cellList.setHiddenAreas(viewModel.getHiddenRanges(), true);281282assert.strictEqual(cellList.getModelIndex2(0), 0);283assert.strictEqual(cellList.getModelIndex2(1), 3);284285// mimic undo286editor.textModel.applyEdits([{287editType: CellEditType.Replace, index: 0, count: 0, cells: [288ds.add(new TestCell(viewModel.viewType, 7, '# header f', 'markdown', CellKind.Code, [], languageService)),289ds.add(new TestCell(viewModel.viewType, 8, 'var g = 5;', 'javascript', CellKind.Code, [], languageService))290]291}], true, undefined, () => undefined, undefined, false);292viewModel.updateFoldingRanges(foldingModel.regions);293cellList.setHiddenAreas(viewModel.getHiddenRanges(), true);294assert.strictEqual(cellList.getModelIndex2(0), 0);295assert.strictEqual(cellList.getModelIndex2(1), 1);296assert.strictEqual(cellList.getModelIndex2(2), 2);297});298});299300test('notebook cell list getModelIndex', async function () {301await withTestNotebook(302[303['# header a', 'markdown', CellKind.Markup, [], {}],304['var b = 1;', 'javascript', CellKind.Code, [], {}],305['# header b', 'markdown', CellKind.Markup, [], {}],306['var b = 2;', 'javascript', CellKind.Code, [], {}],307['# header c', 'markdown', CellKind.Markup, [], {}]308],309(editor, viewModel, ds) => {310const foldingModel = ds.add(new FoldingModel());311foldingModel.attachViewModel(viewModel);312313const cellList = createNotebookCellList(instantiationService, ds);314cellList.attachViewModel(viewModel);315316updateFoldingStateAtIndex(foldingModel, 0, true);317updateFoldingStateAtIndex(foldingModel, 2, true);318viewModel.updateFoldingRanges(foldingModel.regions);319cellList.setHiddenAreas(viewModel.getHiddenRanges(), true);320321assert.deepStrictEqual(cellList.getModelIndex2(-1), 0);322assert.deepStrictEqual(cellList.getModelIndex2(0), 0);323assert.deepStrictEqual(cellList.getModelIndex2(1), 2);324assert.deepStrictEqual(cellList.getModelIndex2(2), 4);325});326});327328329test('notebook validate range', async () => {330await withTestNotebook(331[332['# header a', 'markdown', CellKind.Markup, [], {}],333['var b = 1;', 'javascript', CellKind.Code, [], {}]334],335(editor, viewModel) => {336assert.deepStrictEqual(viewModel.validateRange(null), null);337assert.deepStrictEqual(viewModel.validateRange(undefined), null);338assert.deepStrictEqual(viewModel.validateRange({ start: 0, end: 0 }), { start: 0, end: 0 });339assert.deepStrictEqual(viewModel.validateRange({ start: 0, end: 2 }), { start: 0, end: 2 });340assert.deepStrictEqual(viewModel.validateRange({ start: 0, end: 3 }), { start: 0, end: 2 });341assert.deepStrictEqual(viewModel.validateRange({ start: -1, end: 3 }), { start: 0, end: 2 });342assert.deepStrictEqual(viewModel.validateRange({ start: -1, end: 1 }), { start: 0, end: 1 });343assert.deepStrictEqual(viewModel.validateRange({ start: 2, end: 1 }), { start: 1, end: 2 });344assert.deepStrictEqual(viewModel.validateRange({ start: 2, end: -1 }), { start: 0, end: 2 });345});346});347348test('notebook updateSelectionState', async function () {349await withTestNotebook(350[351['# header a', 'markdown', CellKind.Markup, [], {}],352['var b = 1;', 'javascript', CellKind.Code, [], {}]353],354(editor, viewModel) => {355viewModel.updateSelectionsState({ kind: SelectionStateType.Index, focus: { start: 1, end: 2 }, selections: [{ start: 1, end: 2 }, { start: -1, end: 0 }] });356assert.deepStrictEqual(viewModel.getSelections(), [{ start: 1, end: 2 }]);357});358});359360test('notebook cell selection w/ cell deletion', async function () {361await withTestNotebook(362[363['# header a', 'markdown', CellKind.Markup, [], {}],364['var b = 1;', 'javascript', CellKind.Code, [], {}]365],366(editor, viewModel) => {367viewModel.updateSelectionsState({ kind: SelectionStateType.Index, focus: { start: 1, end: 2 }, selections: [{ start: 1, end: 2 }] });368runDeleteAction(editor, viewModel.cellAt(1)!);369// viewModel.deleteCell(1, true, false);370assert.deepStrictEqual(viewModel.getFocus(), { start: 0, end: 1 });371assert.deepStrictEqual(viewModel.getSelections(), [{ start: 0, end: 1 }]);372runDeleteAction(editor, viewModel.cellAt(0)!);373assert.deepStrictEqual(viewModel.getFocus(), { start: 0, end: 0 });374assert.deepStrictEqual(viewModel.getSelections(), [{ start: 0, end: 0 }]);375});376});377378test('notebook cell selection w/ cell deletion from applyEdits', async function () {379await withTestNotebook(380[381['# header a', 'markdown', CellKind.Markup, [], {}],382['var b = 1;', 'javascript', CellKind.Code, [], {}],383['var c = 2;', 'javascript', CellKind.Code, [], {}]384],385async (editor, viewModel) => {386viewModel.updateSelectionsState({ kind: SelectionStateType.Index, focus: { start: 1, end: 2 }, selections: [{ start: 1, end: 2 }] });387editor.textModel.applyEdits([{388editType: CellEditType.Replace,389index: 1,390count: 1,391cells: []392}], true, undefined, () => undefined, undefined, true);393assert.deepStrictEqual(viewModel.getFocus(), { start: 1, end: 2 });394assert.deepStrictEqual(viewModel.getSelections(), [{ start: 1, end: 2 }]);395});396});397});398399400