diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts index 517033df83e..01c682f5678 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts @@ -286,28 +286,9 @@ suite('Notebook API tests', function () { ] }); - const secondCell = vscode.window.activeNotebookEditor!.document.cells[1]; - const moveCellEvent = asPromise(vscode.notebook.onDidChangeNotebookCells); await vscode.commands.executeCommand('notebook.cell.moveUp'); - const moveCellEventRet = await moveCellEvent; - assert.deepStrictEqual(moveCellEventRet, { - document: vscode.window.activeNotebookEditor!.document, - changes: [ - { - start: 1, - deletedCount: 1, - deletedItems: [secondCell], - items: [] - }, - { - start: 0, - deletedCount: 0, - deletedItems: [], - items: [vscode.window.activeNotebookEditor?.document.cells[0]] - } - ] - }); + await moveCellEvent; const cellOutputChange = asPromise(vscode.notebook.onDidChangeCellOutputs); await vscode.commands.executeCommand('notebook.cell.execute'); @@ -350,25 +331,7 @@ suite('Notebook API tests', function () { assert.strictEqual(vscode.window.activeNotebookEditor!.document.cells.indexOf(activeCell!), 0); const moveChange = asPromise(vscode.notebook.onDidChangeNotebookCells); await vscode.commands.executeCommand('notebook.cell.moveDown'); - const ret = await moveChange; - assert.deepStrictEqual(ret, { - document: vscode.window.activeNotebookEditor?.document, - changes: [ - { - start: 0, - deletedCount: 1, - deletedItems: [activeCell], - items: [] - }, - { - start: 1, - deletedCount: 0, - deletedItems: [], - items: [activeCell] - } - ] - }); - + await moveChange; await saveAllEditors(); await closeAllEditors(); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellOperations/cellOperations.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellOperations/cellOperations.ts new file mode 100644 index 00000000000..714b2eb4c0a --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellOperations/cellOperations.ts @@ -0,0 +1,244 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { localize } from 'vs/nls'; +import { registerAction2 } from 'vs/platform/actions/common/actions'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { InputFocusedContext } from 'vs/platform/contextkey/common/contextkeys'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { INotebookCellActionContext, NotebookCellAction } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; +import { expandCellRangesWithHiddenCells, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { CellEditType, cellRangeContains, cellRangesToIndexes, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { cloneNotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; + +const MOVE_CELL_UP_COMMAND_ID = 'notebook.cell.moveUp'; +const MOVE_CELL_DOWN_COMMAND_ID = 'notebook.cell.moveDown'; +const COPY_CELL_UP_COMMAND_ID = 'notebook.cell.copyUp'; +const COPY_CELL_DOWN_COMMAND_ID = 'notebook.cell.copyDown'; + +registerAction2(class extends NotebookCellAction { + constructor() { + super( + { + id: MOVE_CELL_UP_COMMAND_ID, + title: localize('notebookActions.moveCellUp', "Move Cell Up"), + icon: icons.moveUpIcon, + keybinding: { + primary: KeyMod.Alt | KeyCode.UpArrow, + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext.toNegated()), + weight: KeybindingWeight.WorkbenchContrib + } + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { + return moveCellRange(context, 'up'); + } +}); + +registerAction2(class extends NotebookCellAction { + constructor() { + super( + { + id: MOVE_CELL_DOWN_COMMAND_ID, + title: localize('notebookActions.moveCellDown', "Move Cell Down"), + icon: icons.moveDownIcon, + keybinding: { + primary: KeyMod.Alt | KeyCode.DownArrow, + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext.toNegated()), + weight: KeybindingWeight.WorkbenchContrib + } + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { + return moveCellRange(context, 'down'); + } +}); + + +async function moveCellRange(context: INotebookCellActionContext, direction: 'up' | 'down'): Promise { + const viewModel = context.notebookEditor.viewModel; + if (!viewModel) { + return; + } + + if (!viewModel.metadata.editable) { + return; + } + + const selections = context.notebookEditor.getSelections(); + const modelRanges = expandCellRangesWithHiddenCells(context.notebookEditor, context.notebookEditor.viewModel!, selections); + const range = modelRanges[0]; + if (!range || range.start === range.end) { + return; + } + + if (direction === 'up') { + if (range.start === 0) { + return; + } + + const indexAbove = range.start - 1; + const finalSelection = { start: range.start - 1, end: range.end - 1 }; + const focus = context.notebookEditor.getFocus(); + const newFocus = cellRangeContains(range, focus) ? { start: focus.start - 1, end: focus.end - 1 } : { start: range.start - 1, end: range.start }; + viewModel.notebookDocument.applyEdits([ + { + editType: CellEditType.Move, + index: indexAbove, + length: 1, + newIdx: range.end - 1 + }], + true, + { + kind: SelectionStateType.Index, + focus: viewModel.getFocus(), + selections: viewModel.getSelections() + }, + () => ({ kind: SelectionStateType.Index, focus: newFocus, selections: [finalSelection] }), + undefined + ); + const focusRange = viewModel.getSelections()[0] ?? viewModel.getFocus(); + context.notebookEditor.revealCellRangeInView(focusRange); + } else { + if (range.end >= viewModel.length) { + return; + } + + const indexBelow = range.end; + const finalSelection = { start: range.start + 1, end: range.end + 1 }; + const focus = context.notebookEditor.getFocus(); + const newFocus = cellRangeContains(range, focus) ? { start: focus.start + 1, end: focus.end + 1 } : { start: range.start + 1, end: range.start + 2 }; + + viewModel.notebookDocument.applyEdits([ + { + editType: CellEditType.Move, + index: indexBelow, + length: 1, + newIdx: range.start + }], + true, + { + kind: SelectionStateType.Index, + focus: viewModel.getFocus(), + selections: viewModel.getSelections() + }, + () => ({ kind: SelectionStateType.Index, focus: newFocus, selections: [finalSelection] }), + undefined + ); + + const focusRange = viewModel.getSelections()[0] ?? viewModel.getFocus(); + context.notebookEditor.revealCellRangeInView(focusRange); + } +} + +registerAction2(class extends NotebookCellAction { + constructor() { + super( + { + id: COPY_CELL_UP_COMMAND_ID, + title: localize('notebookActions.copyCellUp', "Copy Cell Up"), + keybinding: { + primary: KeyMod.Alt | KeyMod.Shift | KeyCode.UpArrow, + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext.toNegated()), + weight: KeybindingWeight.WorkbenchContrib + } + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { + return copyCellRange(context, 'up'); + } +}); + +registerAction2(class extends NotebookCellAction { + constructor() { + super( + { + id: COPY_CELL_DOWN_COMMAND_ID, + title: localize('notebookActions.copyCellDown', "Copy Cell Down"), + keybinding: { + primary: KeyMod.Alt | KeyMod.Shift | KeyCode.DownArrow, + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext.toNegated()), + weight: KeybindingWeight.WorkbenchContrib + } + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { + return copyCellRange(context, 'down'); + } +}); + +async function copyCellRange(context: INotebookCellActionContext, direction: 'up' | 'down'): Promise { + const viewModel = context.notebookEditor.viewModel; + if (!viewModel) { + return; + } + + if (!viewModel.metadata.editable) { + return; + } + + const selections = context.notebookEditor.getSelections(); + const modelRanges = expandCellRangesWithHiddenCells(context.notebookEditor, context.notebookEditor.viewModel!, selections); + const range = modelRanges[0]; + if (!range || range.start === range.end) { + return; + } + + if (direction === 'up') { + // insert up, without changing focus and selections + const focus = viewModel.getFocus(); + const selections = viewModel.getSelections(); + viewModel.notebookDocument.applyEdits([ + { + editType: CellEditType.Replace, + index: range.end, + count: 0, + cells: cellRangesToIndexes([range]).map(index => cloneNotebookCellTextModel(viewModel.viewCells[index].model)) + }], + true, + { + kind: SelectionStateType.Index, + focus: focus, + selections: selections + }, + () => ({ kind: SelectionStateType.Index, focus: focus, selections: selections }), + undefined + ); + } else { + // insert down, move selections + const focus = viewModel.getFocus(); + const selections = viewModel.getSelections(); + const newCells = cellRangesToIndexes([range]).map(index => cloneNotebookCellTextModel(viewModel.viewCells[index].model)); + const countDelta = newCells.length; + const newFocus = { start: focus.start + countDelta, end: focus.end + countDelta }; + const newSelections = [{ start: range.start + countDelta, end: range.end + countDelta }]; + viewModel.notebookDocument.applyEdits([ + { + editType: CellEditType.Replace, + index: range.end, + count: 0, + cells: cellRangesToIndexes([range]).map(index => cloneNotebookCellTextModel(viewModel.viewCells[index].model)) + }], + true, + { + kind: SelectionStateType.Index, + focus: focus, + selections: selections + }, + () => ({ kind: SelectionStateType.Index, focus: newFocus, selections: newSelections }), + undefined + ); + + const focusRange = viewModel.getSelections()[0] ?? viewModel.getFocus(); + context.notebookEditor.revealCellRangeInView(focusRange); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts b/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts index 0d759a8d57d..1c8864a87a2 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts @@ -8,13 +8,12 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { getNotebookEditorFromEditorPane, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import * as UUID from 'vs/base/common/uuid'; +import { expandCellRangesWithHiddenCells, getNotebookEditorFromEditorPane, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CopyAction, CutAction, PasteAction } from 'vs/editor/contrib/clipboard/clipboard'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { CellEditType, ICellEditOperation, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; +import { cloneNotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { CellEditType, ICellEditOperation, ICellRange, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; class NotebookClipboardContribution extends Disposable { @@ -46,6 +45,10 @@ class NotebookClipboardContribution extends Disposable { return false; } + if (!editor.hasModel()) { + return false; + } + if (editor.hasOutputTextSelection()) { document.execCommand('copy'); return true; @@ -53,7 +56,8 @@ class NotebookClipboardContribution extends Disposable { const clipboardService = accessor.get(IClipboardService); const notebookService = accessor.get(INotebookService); - const selectedCells = editor.getSelectionViewModels(); + const selectionRanges = expandCellRangesWithHiddenCells(editor, editor.viewModel, editor.viewModel.getSelections()); + const selectedCells = this._cellRangeToViewCells(editor.viewModel, selectionRanges); if (!selectedCells.length) { return false; @@ -91,35 +95,11 @@ class NotebookClipboardContribution extends Disposable { return false; } - const cloneMetadata = (cell: NotebookCellTextModel) => { - return { - editable: cell.metadata?.editable, - breakpointMargin: cell.metadata?.breakpointMargin, - hasExecutionOrder: cell.metadata?.hasExecutionOrder, - inputCollapsed: cell.metadata?.inputCollapsed, - outputCollapsed: cell.metadata?.outputCollapsed, - custom: cell.metadata?.custom - }; - }; - - const cloneCell = (cell: NotebookCellTextModel) => { - return { - source: cell.getValue(), - language: cell.language, - cellKind: cell.cellKind, - outputs: cell.outputs.map(output => ({ - outputs: output.outputs, - /* paste should generate new outputId */ outputId: UUID.generateUuid() - })), - metadata: cloneMetadata(cell) - }; - }; - if (activeCell) { const currCellIndex = viewModel.getCellIndex(activeCell); let topPastedCell: CellViewModel | undefined = undefined; - pasteCells.items.reverse().map(cell => cloneCell(cell)).forEach(pasteCell => { + pasteCells.items.reverse().map(cell => cloneNotebookCellTextModel(cell)).forEach(pasteCell => { const newIdx = typeof currCellIndex === 'number' ? currCellIndex + 1 : 0; topPastedCell = viewModel.createCell(newIdx, pasteCell.source, pasteCell.language, pasteCell.cellKind, pasteCell.metadata, pasteCell.outputs, true); }); @@ -133,7 +113,7 @@ class NotebookClipboardContribution extends Disposable { } let topPastedCell: CellViewModel | undefined = undefined; - pasteCells.items.reverse().map(cell => cloneCell(cell)).forEach(pasteCell => { + pasteCells.items.reverse().map(cell => cloneNotebookCellTextModel(cell)).forEach(pasteCell => { topPastedCell = viewModel.createCell(0, pasteCell.source, pasteCell.language, pasteCell.cellKind, pasteCell.metadata, pasteCell.outputs, true); }); @@ -167,23 +147,30 @@ class NotebookClipboardContribution extends Disposable { const clipboardService = accessor.get(IClipboardService); const notebookService = accessor.get(INotebookService); - const selectedCells = editor.getSelectionViewModels(); + const selectionRanges = expandCellRangesWithHiddenCells(editor, viewModel, viewModel.getSelections()); + const selectedCells = this._cellRangeToViewCells(viewModel, selectionRanges); if (!selectedCells.length) { return false; } clipboardService.writeText(selectedCells.map(cell => cell.getText()).join('\n')); - const selectionIndexes = selectedCells.map(cell => [cell, viewModel.getCellIndex(cell)] as [ICellViewModel, number]).sort((a, b) => b[1] - a[1]); - const edits: ICellEditOperation[] = selectionIndexes.map(value => ({ editType: CellEditType.Replace, index: value[1], count: 1, cells: [] })); - const firstSelectIndex = selectionIndexes.sort((a, b) => a[1] - b[1])[0][1]; + const edits: ICellEditOperation[] = selectionRanges.map(range => ({ editType: CellEditType.Replace, index: range.start, count: range.end - range.start, cells: [] })); + const firstSelectIndex = selectionRanges[0].start; + + /** + * If we have cells, 0, 1, 2, 3, 4, 5, 6 + * and cells 1, 2 are selected, and then we delete cells 1 and 2 + * the new focused cell should still be at index 1 + */ const newFocusedCellIndex = firstSelectIndex < viewModel.notebookDocument.cells.length ? firstSelectIndex : viewModel.notebookDocument.cells.length - 1; - viewModel.notebookDocument.applyEdits(edits, true, { kind: SelectionStateType.Index, selections: viewModel.getSelections() }, () => { + viewModel.notebookDocument.applyEdits(edits, true, { kind: SelectionStateType.Index, focus: viewModel.getFocus(), selections: selectionRanges }, () => { return { kind: SelectionStateType.Index, + focus: { start: newFocusedCellIndex, end: newFocusedCellIndex + 1 }, selections: [{ start: newFocusedCellIndex, end: newFocusedCellIndex + 1 }] }; }, undefined, true); @@ -193,6 +180,15 @@ class NotebookClipboardContribution extends Disposable { }); } } + + private _cellRangeToViewCells(viewModel: NotebookViewModel, ranges: ICellRange[]) { + const cells: ICellViewModel[] = []; + ranges.forEach(range => { + cells.push(...viewModel.viewCells.slice(range.start, range.end)); + }); + + return cells; + } } const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index 3ee68e379b5..b33fcb33b14 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -59,14 +59,10 @@ const EDIT_CELL_COMMAND_ID = 'notebook.cell.edit'; const QUIT_EDIT_CELL_COMMAND_ID = 'notebook.cell.quitEdit'; const DELETE_CELL_COMMAND_ID = 'notebook.cell.delete'; -const MOVE_CELL_UP_COMMAND_ID = 'notebook.cell.moveUp'; -const MOVE_CELL_DOWN_COMMAND_ID = 'notebook.cell.moveDown'; const COPY_CELL_COMMAND_ID = 'notebook.cell.copy'; const CUT_CELL_COMMAND_ID = 'notebook.cell.cut'; const PASTE_CELL_COMMAND_ID = 'notebook.cell.paste'; const PASTE_CELL_ABOVE_COMMAND_ID = 'notebook.cell.pasteAbove'; -const COPY_CELL_UP_COMMAND_ID = 'notebook.cell.copyUp'; -const COPY_CELL_DOWN_COMMAND_ID = 'notebook.cell.copyDown'; const SPLIT_CELL_COMMAND_ID = 'notebook.cell.split'; const JOIN_CELL_ABOVE_COMMAND_ID = 'notebook.cell.joinAbove'; const JOIN_CELL_BELOW_COMMAND_ID = 'notebook.cell.joinBelow'; @@ -223,7 +219,7 @@ abstract class NotebookAction extends Action2 { } } -abstract class NotebookCellAction extends NotebookAction { +export abstract class NotebookCellAction extends NotebookAction { protected isCellActionContext(context?: unknown): context is INotebookCellActionContext { return !!context && !!(context as INotebookCellActionContext).notebookEditor && !!(context as INotebookCellActionContext).cell; } @@ -1038,66 +1034,6 @@ registerAction2(class extends NotebookCellAction { } }); -async function moveCell(context: INotebookCellActionContext, direction: 'up' | 'down'): Promise { - const result = direction === 'up' ? - await context.notebookEditor.moveCellUp(context.cell) : - await context.notebookEditor.moveCellDown(context.cell); - - if (result) { - // move cell command only works when the cell container has focus - context.notebookEditor.focusNotebookCell(result, 'container'); - } -} - -async function copyCell(context: INotebookCellActionContext, direction: 'up' | 'down'): Promise { - const text = context.cell.getText(); - const newCellDirection = direction === 'up' ? 'above' : 'below'; - const newCell = context.notebookEditor.insertNotebookCell(context.cell, context.cell.cellKind, newCellDirection, text); - if (newCell) { - context.notebookEditor.focusNotebookCell(newCell, 'container'); - } -} - -registerAction2(class extends NotebookCellAction { - constructor() { - super( - { - id: MOVE_CELL_UP_COMMAND_ID, - title: localize('notebookActions.moveCellUp', "Move Cell Up"), - icon: icons.moveUpIcon, - keybinding: { - primary: KeyMod.Alt | KeyCode.UpArrow, - when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext.toNegated()), - weight: KeybindingWeight.WorkbenchContrib - } - }); - } - - async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { - return moveCell(context, 'up'); - } -}); - -registerAction2(class extends NotebookCellAction { - constructor() { - super( - { - id: MOVE_CELL_DOWN_COMMAND_ID, - title: localize('notebookActions.moveCellDown', "Move Cell Down"), - icon: icons.moveDownIcon, - keybinding: { - primary: KeyMod.Alt | KeyCode.DownArrow, - when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext.toNegated()), - weight: KeybindingWeight.WorkbenchContrib - } - }); - } - - async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { - return moveCell(context, 'down'); - } -}); - registerAction2(class extends NotebookCellAction { constructor() { super( @@ -1287,44 +1223,6 @@ registerAction2(class extends NotebookCellAction { } }); -registerAction2(class extends NotebookCellAction { - constructor() { - super( - { - id: COPY_CELL_UP_COMMAND_ID, - title: localize('notebookActions.copyCellUp', "Copy Cell Up"), - keybinding: { - primary: KeyMod.Alt | KeyMod.Shift | KeyCode.UpArrow, - when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext.toNegated()), - weight: KeybindingWeight.WorkbenchContrib - } - }); - } - - async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { - return copyCell(context, 'up'); - } -}); - -registerAction2(class extends NotebookCellAction { - constructor() { - super( - { - id: COPY_CELL_DOWN_COMMAND_ID, - title: localize('notebookActions.copyCellDown', "Copy Cell Down"), - keybinding: { - primary: KeyMod.Alt | KeyMod.Shift | KeyCode.DownArrow, - when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext.toNegated()), - weight: KeybindingWeight.WorkbenchContrib - } - }); - } - - async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { - return copyCell(context, 'down'); - } -}); - registerAction2(class extends NotebookCellAction { constructor() { super({ diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts index 1d789cf6d59..979399fdf6d 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts @@ -390,8 +390,8 @@ export class NotebookCellOutline implements IOutline { includeCodeCells = this._configurationService.getValue('notebook.breadcrumbs.showCodeCells'); } - const selectedCellIndex = viewModel.getSelection().start; - const selected = viewModel.getCellByIndex(selectedCellIndex)?.handle; + const focusedCellIndex = viewModel.getFocus().start; + const focused = viewModel.getCellByIndex(focusedCellIndex)?.handle; const entries: OutlineEntry[] = []; for (let i = 0; i < viewModel.viewCells.length; i++) { @@ -434,7 +434,7 @@ export class NotebookCellOutline implements IOutline { entries.push(new OutlineEntry(entries.length, 7, cell, preview, isMarkdown ? Codicon.markdown : Codicon.code)); } - if (cell.handle === selected) { + if (cell.handle === focused) { this._activeEntry = entries[entries.length - 1]; } @@ -518,7 +518,7 @@ export class NotebookCellOutline implements IOutline { const { viewModel } = this._editor; if (viewModel) { - const cell = viewModel.getCellByIndex(viewModel.getSelection().start); + const cell = viewModel.getCellByIndex(viewModel.getFocus().start); if (cell) { for (let entry of this._entries) { newActive = entry.find(cell, []); diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 67c129064b9..8f277faeb04 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -66,6 +66,7 @@ import 'vs/workbench/contrib/notebook/browser/contrib/marker/markerProvider'; import 'vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline'; import 'vs/workbench/contrib/notebook/browser/contrib/status/editorStatus'; import 'vs/workbench/contrib/notebook/browser/contrib/undoRedo/notebookUndoRedo'; +import 'vs/workbench/contrib/notebook/browser/contrib/cellOperations/cellOperations'; import 'vs/workbench/contrib/notebook/browser/contrib/viewportCustomMarkdown/viewportCustomMarkdown'; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index ecee2ca9f0f..256431c9aef 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -22,7 +22,7 @@ import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/outpu import { RunStateRenderer, TimerRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; import { CellViewModel, IModelDecorationsChangeAccessor, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { CellKind, NotebookCellMetadata, NotebookDocumentMetadata, INotebookKernel, ICellRange, IOrderedMimeType, INotebookRendererInfo, ICellOutput, IOutputItemDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, NotebookCellMetadata, NotebookDocumentMetadata, INotebookKernel, ICellRange, IOrderedMimeType, INotebookRendererInfo, ICellOutput, IOutputItemDto, cellRangesToIndexes, reduceRanges } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { IMenu } from 'vs/platform/actions/common/actions'; @@ -231,6 +231,7 @@ export interface ICellViewModel extends IGenericCellViewModel { readonly model: NotebookCellTextModel; readonly id: string; readonly textBuffer: IReadonlyTextBuffer; + readonly layoutInfo: { totalHeight: number; }; dragging: boolean; handle: number; uri: URI; @@ -318,8 +319,7 @@ export interface INotebookEditorCreationOptions { export interface IActiveNotebookEditor extends INotebookEditor { viewModel: NotebookViewModel; - // selection is never undefined when the editor is attached to a document. - getSelection(): ICellRange; + getFocus(): ICellRange; } export const NOTEBOOK_EDITOR_ID = 'workbench.editor.notebook'; @@ -331,7 +331,6 @@ export interface INotebookEditor extends ICommonNotebookEditor { // from the old IEditor readonly onDidChangeVisibleRanges: Event; readonly onDidChangeSelection: Event; - getSelection(): ICellRange | undefined; getSelections(): ICellRange[]; visibleRanges: ICellRange[]; textModel?: NotebookTextModel; @@ -571,6 +570,23 @@ export interface INotebookEditor extends ICommonNotebookEditor { */ revealRangeInCenterIfOutsideViewportAsync(cell: ICellViewModel, range: Range): Promise; + /** + * Get the view index of a cell + */ + getViewIndex(cell: ICellViewModel): number; + + /** + * @param startIndex Inclusive + * @param endIndex Exclusive + */ + getCellRangeFromViewRange(startIndex: number, endIndex: number): ICellRange | undefined; + + /** + * @param startIndex Inclusive + * @param endIndex Exclusive + */ + getCellsFromViewRange(startIndex: number, endIndex: number): ICellViewModel[]; + /** * Set hidden areas on cell text models. */ @@ -640,6 +656,9 @@ export interface INotebookCellList { attachViewModel(viewModel: NotebookViewModel): void; clear(): void; getViewIndex(cell: ICellViewModel): number | undefined; + getViewIndex2(modelIndex: number): number | undefined; + getModelIndex(cell: CellViewModel): number | undefined; + getModelIndex2(viewIndex: number): number | undefined; getVisibleRangesPlusViewportAboveBelow(): ICellRange[]; focusElement(element: ICellViewModel): void; selectElement(element: ICellViewModel): void; @@ -883,3 +902,30 @@ export function updateEditorTopPadding(top: number) { export function getEditorTopPadding() { return EDITOR_TOP_PADDING; } + +export function expandCellRangesWithHiddenCells(editor: INotebookEditor, viewModel: NotebookViewModel, ranges: ICellRange[]) { + // assuming ranges are sorted and no overlap + const indexes = cellRangesToIndexes(ranges); + let modelRanges: ICellRange[] = []; + indexes.forEach(index => { + const viewCell = viewModel.viewCells[index]; + + if (!viewCell) { + return; + } + + const viewIndex = editor.getViewIndex(viewCell); + if (viewIndex < 0) { + return; + } + + const nextViewIndex = viewIndex + 1; + const range = editor.getCellRangeFromViewRange(viewIndex, nextViewIndex); + + if (range) { + modelRanges.push(range); + } + }); + + return reduceRanges(modelRanges); +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 711238ca5c0..2aa21740f5c 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -319,8 +319,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return this.viewModel?.getSelections() ?? []; } - getSelection() { - return this.viewModel?.getSelection(); + getFocus() { + return this.viewModel?.getFocus() ?? { start: 0, end: 0 }; } getSelectionViewModels(): ICellViewModel[] { @@ -424,6 +424,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor keyboardSupport: false, mouseSupport: true, multipleSelectionSupport: true, + selectionNavigation: true, enableKeyboardNavigation: true, additionalScrollHeight: 0, transformOptimization: false, //(isMacintosh && isNative) || getTitleBarStyle(this.configurationService, this.environmentService) === 'native', @@ -1039,6 +1040,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } else if (this._list.length > 0) { this.viewModel?.updateSelectionsState({ kind: SelectionStateType.Index, + focus: { start: 0, end: 1 }, selections: [{ start: 0, end: 1 }] }); } @@ -1283,6 +1285,49 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return this._list.revealElementRangeInCenterIfOutsideViewportAsync(cell, range); } + getViewIndex(cell: ICellViewModel): number { + if (!this._list) { + return -1; + } + return this._list.getViewIndex(cell) ?? -1; + } + + getCellRangeFromViewRange(startIndex: number, endIndex: number): ICellRange | undefined { + if (!this.viewModel) { + return undefined; + } + + const modelIndex = this._list.getModelIndex2(startIndex); + if (modelIndex === undefined) { + throw new Error(`startIndex ${startIndex} out of boundary`); + } + + if (endIndex >= this._list.length) { + // it's the end + const endModelIndex = this.viewModel.length; + return { start: modelIndex, end: endModelIndex }; + } else { + const endModelIndex = this._list.getModelIndex2(endIndex); + if (endModelIndex === undefined) { + throw new Error(`endIndex ${endIndex} out of boundary`); + } + return { start: modelIndex, end: endModelIndex }; + } + } + + getCellsFromViewRange(startIndex: number, endIndex: number): ICellViewModel[] { + if (!this.viewModel) { + return []; + } + + const range = this.getCellRangeFromViewRange(startIndex, endIndex); + if (!range) { + return []; + } + + return this.viewModel.viewCells.slice(range.start, range.end); + } + setCellEditorSelection(cell: ICellViewModel, range: Range): void { this._list.setCellSelection(cell, range); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index e3f0ce91934..9a10a645caf 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -293,7 +293,7 @@ export class NotebookCellList extends WorkbenchList implements ID // convert model selections to view selections const viewSelections = cellRangesToIndexes(model.getSelections()).map(index => model.getCellByIndex(index)).filter(cell => !!cell).map(cell => this._getViewIndexUpperBound(cell!)); this.setSelection(viewSelections, undefined, true); - const primary = cellRangesToIndexes([model.getSelection()]).map(index => model.getCellByIndex(index)).filter(cell => !!cell).map(cell => this._getViewIndexUpperBound(cell!)); + const primary = cellRangesToIndexes([model.getFocus()]).map(index => model.getCellByIndex(index)).filter(cell => !!cell).map(cell => this._getViewIndexUpperBound(cell!)); if (primary.length) { this.setFocus(primary, undefined, true); @@ -363,6 +363,8 @@ export class NotebookCellList extends WorkbenchList implements ID } if (!hasDifference) { + // they call 'setHiddenAreas' for a reason, even if the ranges are still the same, it's possible that the hiddenRangeSum is not update to date + this._updateHiddenRangePrefixSum(newRanges); return false; } } @@ -373,6 +375,16 @@ export class NotebookCellList extends WorkbenchList implements ID this._hiddenRangeIds = hiddenAreaIds; // set hidden ranges prefix sum + this._updateHiddenRangePrefixSum(newRanges); + + if (triggerViewUpdate) { + this.updateHiddenAreasInView(oldRanges, newRanges); + } + + return true; + } + + private _updateHiddenRangePrefixSum(newRanges: ICellRange[]) { let start = 0; let index = 0; const ret: number[] = []; @@ -397,12 +409,6 @@ export class NotebookCellList extends WorkbenchList implements ID } this.hiddenRangesPrefixSum = new PrefixSumComputer(values); - - if (triggerViewUpdate) { - this.updateHiddenAreasInView(oldRanges, newRanges); - } - - return true; } /** @@ -445,12 +451,30 @@ export class NotebookCellList extends WorkbenchList implements ID if (!selectionsLeft.length && this._viewModel!.viewCells.length) { // after splice, the selected cells are deleted - this._viewModel!.updateSelectionsState({ kind: SelectionStateType.Index, selections: [{ start: 0, end: 1 }] }); + this._viewModel!.updateSelectionsState({ kind: SelectionStateType.Index, focus: { start: 0, end: 1 }, selections: [{ start: 0, end: 1 }] }); } } + getModelIndex(cell: CellViewModel): number | undefined { + const viewIndex = this.indexOf(cell); + return this.getModelIndex2(viewIndex); + } + + getModelIndex2(viewIndex: number): number | undefined { + if (!this.hiddenRangesPrefixSum) { + return viewIndex; + } + + const modelIndex = this.hiddenRangesPrefixSum.getAccumulatedValue(viewIndex - 1); + return modelIndex; + } + getViewIndex(cell: ICellViewModel) { const modelIndex = this._viewModel!.getCellIndex(cell); + return this.getViewIndex2(modelIndex); + } + + getViewIndex2(modelIndex: number): number | undefined { if (!this.hiddenRangesPrefixSum) { return modelIndex; } @@ -598,30 +622,12 @@ export class NotebookCellList extends WorkbenchList implements ID focusNext(n: number | undefined, loop: boolean | undefined, browserEvent?: UIEvent, filter?: (element: CellViewModel) => boolean): void { this._focusNextPreviousDelegate.onFocusNext(() => { super.focusNext(n, loop, browserEvent, filter); - const focus = this.getFocus(); - if (focus.length) { - const focusedElementHandle = this.element(focus[0]).handle; - this._viewModel?.updateSelectionsState({ - kind: SelectionStateType.Handle, - primary: focusedElementHandle, - selections: [focusedElementHandle] - }, 'view'); - } }); } focusPrevious(n: number | undefined, loop: boolean | undefined, browserEvent?: UIEvent, filter?: (element: CellViewModel) => boolean): void { this._focusNextPreviousDelegate.onFocusPrevious(() => { super.focusPrevious(n, loop, browserEvent, filter); - const focus = this.getFocus(); - if (focus.length) { - const focusedElementHandle = this.element(focus[0]).handle; - this._viewModel?.updateSelectionsState({ - kind: SelectionStateType.Handle, - primary: focusedElementHandle, - selections: [focusedElementHandle] - }, 'view'); - } }); } @@ -701,7 +707,7 @@ export class NotebookCellList extends WorkbenchList implements ID const endElementHeight = this.view.elementHeight(endIndex); if (endElementTop >= wrapperBottom) { - return this._revealInternal(startIndex, false, CellRevealPosition.Top); + return this._revealInternal(endIndex, false, CellRevealPosition.Bottom); } if (endElementTop < wrapperBottom) { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts index ffc9001117d..11fa2956367 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts @@ -76,9 +76,8 @@ export class CodeCell extends Disposable { }); const updateForFocusMode = () => { - if (this.notebookEditor.getSelection().start !== this.notebookEditor.viewModel.getCellIndex(viewCell)) { + if (this.notebookEditor.getFocus().start !== this.notebookEditor.viewModel.getCellIndex(viewCell)) { templateData.container.classList.toggle('cell-editor-focus', viewCell.focusMode === CellFocusMode.Editor); - return; } if (viewCell.focusMode === CellFocusMode.Editor) { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/cellSelectionCollection.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/cellSelectionCollection.ts index 9d2d5e1d98a..587cd6ecce4 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/cellSelectionCollection.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/cellSelectionCollection.ts @@ -32,7 +32,7 @@ export class NotebookCellSelectionCollection extends Disposable { private _primary: ICellRange | null = null; - private _selections: ICellRange[] = [{ start: 0, end: 0 }]; + private _selections: ICellRange[] = []; get selections(): ICellRange[] { return this._selections; @@ -42,36 +42,17 @@ export class NotebookCellSelectionCollection extends Disposable { return this._selections[0]; } + get focus(): ICellRange { + return this._primary ?? { start: 0, end: 0 }; + } + setState(primary: ICellRange | null, selections: ICellRange[], forceEventEmit: boolean, source: 'view' | 'model') { - if (primary !== null) { - const primaryRange = primary; - // TODO@rebornix deal with overlap - const newSelections = [primaryRange, ...selections.filter(selection => !(selection.start === primaryRange.start && selection.end === primaryRange.end)).sort((a, b) => a.start - b.start)]; + const changed = primary !== this._primary || !rangesEqual(this._selections, selections); - const changed = primary !== this._primary || !rangesEqual(this._selections, newSelections); - this._primary = primary; - this._selections = newSelections; - - if (!this._selections.length) { - this._selections.push({ start: 0, end: 0 }); - } - - if (changed || forceEventEmit) { - this._onDidChangeSelection.fire(source); - } - } else { - const changed = primary !== this._primary || !rangesEqual(this._selections, selections); - - this._primary = primary; - this._selections = selections; - - if (!this._selections.length) { - this._selections.push({ start: 0, end: 0 }); - } - - if (changed || forceEventEmit) { - this._onDidChangeSelection.fire(source); - } + this._primary = primary; + this._selections = selections; + if (changed || forceEventEmit) { + this._onDidChangeSelection.fire(source); } } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts index 491314b43f4..9ddd1f01299 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts @@ -24,7 +24,7 @@ import { NotebookEventDispatcher, NotebookMetadataChangedEvent } from 'vs/workbe import { CellFoldingState, EditorFoldingStateDelegate } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { CellKind, NotebookCellMetadata, INotebookSearchOptions, ICellRange, NotebookCellsChangeType, ICell, NotebookCellTextModelSplice, CellEditType, IOutputDto, SelectionStateType, ISelectionState, cellIndexesToRanges, cellRangesToIndexes } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, NotebookCellMetadata, INotebookSearchOptions, ICellRange, NotebookCellsChangeType, ICell, NotebookCellTextModelSplice, CellEditType, IOutputDto, SelectionStateType, ISelectionState, cellIndexesToRanges, cellRangesToIndexes, reduceRanges } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { FoldingRegions } from 'vs/editor/contrib/folding/foldingRanges'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; @@ -353,8 +353,8 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD }); } - getSelection() { - return this._selectionCollection.selection; + getFocus() { + return this._selectionCollection.focus; } getSelections() { @@ -396,13 +396,13 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD const selections = cellIndexesToRanges(state.selections.map(sel => this.getCellIndexByHandle(sel))) .map(range => this.validateRange(range)) .filter(range => range !== null) as ICellRange[]; - this._selectionCollection.setState(primarySelection, selections, true, source); + this._selectionCollection.setState(primarySelection, reduceRanges(selections), true, source); } else { - const primarySelection = this.validateRange(state.selections[0]); + const primarySelection = this.validateRange(state.focus); const selections = state.selections .map(range => this.validateRange(range)) .filter(range => range !== null) as ICellRange[]; - this._selectionCollection.setState(primarySelection, selections, true, source); + this._selectionCollection.setState(primarySelection, reduceRanges(selections), true, source); } } } @@ -715,14 +715,14 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD } deleteCell(index: number, synchronous: boolean, pushUndoStop: boolean = true) { - const primarySelectionIndex = this.getSelection()?.start ?? null; + const focusSelectionIndex = this.getFocus()?.start ?? null; let endPrimarySelection: number | null = null; - if (index === primarySelectionIndex) { - if (primarySelectionIndex < this.length - 1) { - endPrimarySelection = this._viewCells[primarySelectionIndex + 1].handle; - } else if (primarySelectionIndex === this.length - 1 && this.length > 1) { - endPrimarySelection = this._viewCells[primarySelectionIndex - 1].handle; + if (index === focusSelectionIndex) { + if (focusSelectionIndex < this.length - 1) { + endPrimarySelection = this._viewCells[focusSelectionIndex + 1].handle; + } else if (focusSelectionIndex === this.length - 1 && this.length > 1) { + endPrimarySelection = this._viewCells[focusSelectionIndex - 1].handle; } } @@ -736,7 +736,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD cells: [] }], synchronous, - { kind: SelectionStateType.Index, selections: this.getSelections() }, + { kind: SelectionStateType.Index, focus: this.getFocus(), selections: this.getSelections() }, () => ({ kind: SelectionStateType.Handle, primary: endPrimarySelection, selections: endSelections }), undefined, pushUndoStop @@ -764,7 +764,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD length, newIdx } - ], synchronous, { kind: SelectionStateType.Index, selections: this.getSelections() }, () => ({ kind: SelectionStateType.Index, selections: [{ start: newIdx, end: newIdx + 1 }] }), undefined); + ], synchronous, { kind: SelectionStateType.Index, focus: this.getFocus(), selections: this.getSelections() }, () => ({ kind: SelectionStateType.Index, focus: { start: newIdx, end: newIdx + 1 }, selections: [{ start: newIdx, end: newIdx + 1 }] }), undefined); return true; } diff --git a/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts b/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts index 52d2a01a081..5bfc431dc34 100644 --- a/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts +++ b/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts @@ -14,6 +14,7 @@ import { ISelectionState, NotebookCellMetadata } from 'vs/workbench/contrib/note export interface ITextCellEditingDelegate { insertCell?(index: number, cell: NotebookCellTextModel, endSelections?: ISelectionState): void; deleteCell?(index: number, endSelections?: ISelectionState): void; + replaceCell?(index: number, count: number, cells: NotebookCellTextModel[], endSelections?: ISelectionState): void; moveCell?(fromIndex: number, length: number, toIndex: number, beforeSelections: ISelectionState | undefined, endSelections?: ISelectionState): void; updateCellMetadata?(index: number, newMetadata: NotebookCellMetadata): void; } @@ -63,34 +64,22 @@ export class SpliceCellsEdit implements IResourceUndoRedoElement { } undo(): void { - if (!this.editingDelegate.deleteCell || !this.editingDelegate.insertCell) { - throw new Error('Notebook Insert/Delete Cell not implemented for Undo/Redo'); + if (!this.editingDelegate.replaceCell) { + throw new Error('Notebook Replace Cell not implemented for Undo/Redo'); } this.diffs.forEach(diff => { - for (let i = 0; i < diff[2].length; i++) { - this.editingDelegate.deleteCell!(diff[0], this.beforeHandles); - } - - diff[1].reverse().forEach(cell => { - this.editingDelegate.insertCell!(diff[0], cell, this.beforeHandles); - }); + this.editingDelegate.replaceCell!(diff[0], diff[2].length, diff[1], this.beforeHandles); }); } redo(): void { - if (!this.editingDelegate.deleteCell || !this.editingDelegate.insertCell) { - throw new Error('Notebook Insert/Delete Cell not implemented for Undo/Redo'); + if (!this.editingDelegate.replaceCell) { + throw new Error('Notebook Replace Cell not implemented for Undo/Redo'); } this.diffs.reverse().forEach(diff => { - for (let i = 0; i < diff[1].length; i++) { - this.editingDelegate.deleteCell!(diff[0], this.endHandles); - } - - diff[2].reverse().forEach(cell => { - this.editingDelegate.insertCell!(diff[0], cell, this.endHandles); - }); + this.editingDelegate.replaceCell!(diff[0], diff[1].length, diff[2], this.endHandles); }); } } diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts index 08eb5a6a5c4..562043609ea 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts @@ -7,6 +7,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { ICell, NotebookCellOutputsSplice, CellKind, NotebookCellMetadata, NotebookDocumentMetadata, TransientOptions, IOutputDto, ICellOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; import { URI } from 'vs/base/common/uri'; +import * as UUID from 'vs/base/common/uuid'; import * as model from 'vs/editor/common/model'; import { Range } from 'vs/editor/common/core/range'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -179,3 +180,27 @@ export class NotebookCellTextModel extends Disposable implements ICell { super.dispose(); } } + +export function cloneMetadata(cell: NotebookCellTextModel) { + return { + editable: cell.metadata?.editable, + breakpointMargin: cell.metadata?.breakpointMargin, + hasExecutionOrder: cell.metadata?.hasExecutionOrder, + inputCollapsed: cell.metadata?.inputCollapsed, + outputCollapsed: cell.metadata?.outputCollapsed, + custom: cell.metadata?.custom + }; +} + +export function cloneNotebookCellTextModel(cell: NotebookCellTextModel) { + return { + source: cell.getValue(), + language: cell.language, + cellKind: cell.cellKind, + outputs: cell.outputs.map(output => ({ + outputs: output.outputs, + /* paste should generate new outputId */ outputId: UUID.generateUuid() + })), + metadata: cloneMetadata(cell) + }; +} diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index 0246e87346e..3c4d8eb1c8d 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -414,6 +414,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._operationManager.pushEditOperation(new SpliceCellsEdit(this.uri, undoDiff, { insertCell: (index, cell, endSelections) => { this._insertNewCell(index, [cell], true, endSelections); }, deleteCell: (index, endSelections) => { this._removeCell(index, 1, true, endSelections); }, + replaceCell: (index, count, cells, endSelections) => { this._replaceNewCells(index, count, cells, true, endSelections); }, }, undefined, undefined), undefined, undefined); } @@ -505,6 +506,27 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._eventEmitter.emit({ kind: NotebookCellsChangeType.ModelChange, changes: [[index, count, []]], transient: false }, synchronous, endSelections); } + private _replaceNewCells(index: number, count: number, cells: NotebookCellTextModel[], synchronous: boolean, endSelections: ISelectionState | undefined) { + for (let i = index; i < index + count; i++) { + const cell = this._cells[i]; + this._cellListeners.get(cell.handle)?.dispose(); + this._cellListeners.delete(cell.handle); + } + + for (let i = 0; i < cells.length; i++) { + this._mapping.set(cells[i].handle, cells[i]); + const dirtyStateListener = cells[i].onDidChangeContent(() => { + this._eventEmitter.emit({ kind: NotebookCellsChangeType.ChangeCellContent, transient: false }, true); + }); + + this._cellListeners.set(cells[i].handle, dirtyStateListener); + } + + this._cells.splice(index, count, ...cells); + this._eventEmitter.emit({ kind: NotebookCellsChangeType.ModelChange, changes: [[index, count, cells]], transient: false }, synchronous, endSelections); + + } + private _isCellMetadataChanged(a: NotebookCellMetadata, b: NotebookCellMetadata) { const keys = new Set([...Object.keys(a || {}), ...Object.keys(b || {})]); for (let key of keys) { diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 7c8e99762d1..20d063da235 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -305,10 +305,7 @@ export interface ISelectionHandleState { export interface ISelectionIndexState { kind: SelectionStateType.Index; - - /** - * [primarySelection, ...secondarySelections] - */ + focus: ICellRange; selections: ICellRange[]; } @@ -792,6 +789,7 @@ export interface INotebookDecorationRenderOptions { export function cellIndexesToRanges(indexes: number[]) { + indexes.sort((a, b) => a - b); const first = indexes.shift(); if (first === undefined) { @@ -819,3 +817,36 @@ export function cellRangesToIndexes(ranges: ICellRange[]) { return indexes; } + +/** + * todo@rebornix notebookBrowser.reduceCellRanges + * @returns + */ +export function reduceRanges(ranges: ICellRange[]) { + const sorted = ranges.sort((a, b) => a.start - b.start); + const first = sorted[0]; + + if (!first) { + return []; + } + + return sorted.reduce((prev: ICellRange[], curr) => { + const last = prev[prev.length - 1]; + if (last.end >= curr.start) { + last.end = Math.max(last.end, curr.end); + } else { + prev.push(curr); + } + return prev; + }, [first] as ICellRange[]); +} + +/** + * todo@rebornix test and sort + * @param range + * @param other + * @returns + */ +export function cellRangeContains(range: ICellRange, other: ICellRange): boolean { + return other.start >= range.start && other.end <= range.end; +} diff --git a/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts b/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts index bc9f5874744..5abb1ffffd9 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts @@ -8,6 +8,7 @@ import { NOTEBOOK_DISPLAY_ORDER, sortMimeTypes, CellKind, diff, CellUri, cellRan import { TestCell, setupInstantiationService } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; import { URI } from 'vs/base/common/uri'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { reduceCellRanges } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; suite('NotebookCommon', () => { const instantiationService = setupInstantiationService(); @@ -313,5 +314,19 @@ suite('CellRange', function () { assert.deepStrictEqual(cellIndexesToRanges([0, 1]), [{ start: 0, end: 2 }]); assert.deepStrictEqual(cellIndexesToRanges([0, 1, 2]), [{ start: 0, end: 3 }]); assert.deepStrictEqual(cellIndexesToRanges([0, 1, 3]), [{ start: 0, end: 2 }, { start: 3, end: 4 }]); + + assert.deepStrictEqual(cellIndexesToRanges([1, 0]), [{ start: 0, end: 2 }]); + assert.deepStrictEqual(cellIndexesToRanges([1, 2, 0]), [{ start: 0, end: 3 }]); + assert.deepStrictEqual(cellIndexesToRanges([3, 1, 0]), [{ start: 0, end: 2 }, { start: 3, end: 4 }]); + + assert.deepStrictEqual(cellIndexesToRanges([9, 10]), [{ start: 9, end: 11 }]); + assert.deepStrictEqual(cellIndexesToRanges([10, 9]), [{ start: 9, end: 11 }]); + }); + + test('Reduce ranges', function () { + assert.deepStrictEqual(reduceCellRanges([{ start: 0, end: 1 }, { start: 1, end: 2 }]), [{ start: 0, end: 2 }]); + assert.deepStrictEqual(reduceCellRanges([{ start: 0, end: 2 }, { start: 1, end: 3 }]), [{ start: 0, end: 3 }]); + assert.deepStrictEqual(reduceCellRanges([{ start: 1, end: 3 }, { start: 0, end: 2 }]), [{ start: 0, end: 3 }]); + assert.deepStrictEqual(reduceCellRanges([{ start: 0, end: 2 }, { start: 4, end: 5 }]), [{ start: 0, end: 2 }, { start: 4, end: 5 }]); }); }); diff --git a/src/vs/workbench/contrib/notebook/test/notebookSelection.test.ts b/src/vs/workbench/contrib/notebook/test/notebookSelection.test.ts index 4e77f4b4ad9..c7dd1c62c1d 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookSelection.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookSelection.test.ts @@ -4,42 +4,28 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { FoldingModel, updateFoldingStateAtIndex } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; import { NotebookCellSelectionCollection } from 'vs/workbench/contrib/notebook/browser/viewModel/cellSelectionCollection'; -import { CellKind, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { createNotebookCellList, setupInstantiationService, withTestNotebook } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; +import { CellEditType, CellKind, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { createNotebookCellList, setupInstantiationService, TestCell, withTestNotebook } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; suite('NotebookSelection', () => { - test('selection is never empty', function () { + test('focus is never empty', function () { const selectionCollection = new NotebookCellSelectionCollection(); - assert.deepStrictEqual(selectionCollection.selections, [{ start: 0, end: 0 }]); + assert.deepStrictEqual(selectionCollection.focus, { start: 0, end: 0 }); selectionCollection.setState(null, [], true, 'model'); - assert.deepStrictEqual(selectionCollection.selections, [{ start: 0, end: 0 }]); - }); - - test('selections[0] is primary selection', function () { - const selectionCollection = new NotebookCellSelectionCollection(); - selectionCollection.setState(null, [{ start: 0, end: 1 }, { start: 3, end: 5 }], true, 'model'); - assert.deepStrictEqual(selectionCollection.selection, { start: 0, end: 1 }); - assert.deepStrictEqual(selectionCollection.selections, [{ start: 0, end: 1 }, { start: 3, end: 5 }]); - - selectionCollection.setState({ start: 0, end: 1 }, [{ start: 3, end: 5 }], true, 'model'); - assert.deepStrictEqual(selectionCollection.selection, { start: 0, end: 1 }); - assert.deepStrictEqual(selectionCollection.selections, [{ start: 0, end: 1 }, { start: 3, end: 5 }]); - - selectionCollection.setState({ start: 0, end: 1 }, [], true, 'model'); - assert.deepStrictEqual(selectionCollection.selection, { start: 0, end: 1 }); - assert.deepStrictEqual(selectionCollection.selections, [{ start: 0, end: 1 }]); - + assert.deepStrictEqual(selectionCollection.focus, { start: 0, end: 0 }); }); }); suite('NotebookCellList focus/selection', () => { const instantiationService = setupInstantiationService(); + const textModelService = instantiationService.get(ITextModelService); - test('notebook cell list setFocus', function () { - withTestNotebook( + test('notebook cell list setFocus', async function () { + await withTestNotebook( instantiationService, [ ['var a = 1;', 'javascript', CellKind.Code, [], {}], @@ -51,16 +37,16 @@ suite('NotebookCellList focus/selection', () => { assert.strictEqual(cellList.length, 2); cellList.setFocus([0]); - assert.deepStrictEqual(viewModel.getSelection(), { start: 0, end: 1 }); + assert.deepStrictEqual(viewModel.getFocus(), { start: 0, end: 1 }); cellList.setFocus([1]); - assert.deepStrictEqual(viewModel.getSelection(), { start: 1, end: 2 }); + assert.deepStrictEqual(viewModel.getFocus(), { start: 1, end: 2 }); cellList.detachViewModel(); }); }); - test('notebook cell list setSelections', function () { - withTestNotebook( + test('notebook cell list setSelections', async function () { + await withTestNotebook( instantiationService, [ ['var a = 1;', 'javascript', CellKind.Code, [], {}], @@ -73,18 +59,76 @@ suite('NotebookCellList focus/selection', () => { assert.strictEqual(cellList.length, 2); cellList.setSelection([0]); // the only selection is also the focus - assert.deepStrictEqual(viewModel.getSelection(), { start: 0, end: 1 }); + assert.deepStrictEqual(viewModel.getSelections(), [{ start: 0, end: 1 }]); // set selection does not modify focus cellList.setSelection([1]); - assert.deepStrictEqual(viewModel.getSelection(), { start: 0, end: 1 }); - // `getSelections()` now returns all focus/selection ranges - assert.deepStrictEqual(viewModel.getSelections(), [{ start: 0, end: 1 }, { start: 1, end: 2 }]); + assert.deepStrictEqual(viewModel.getSelections(), [{ start: 1, end: 2 }]); }); }); - test('notebook cell list focus/selection with folding regions', function () { - withTestNotebook( + test('notebook cell list setFocus', async function () { + await withTestNotebook( + instantiationService, + [ + ['var a = 1;', 'javascript', CellKind.Code, [], {}], + ['var b = 2;', 'javascript', CellKind.Code, [], {}] + ], + (editor, viewModel) => { + const cellList = createNotebookCellList(instantiationService); + cellList.attachViewModel(viewModel); + + assert.strictEqual(cellList.length, 2); + cellList.setFocus([0]); + assert.deepStrictEqual(viewModel.getFocus(), { start: 0, end: 1 }); + + cellList.setFocus([1]); + assert.deepStrictEqual(viewModel.getFocus(), { start: 1, end: 2 }); + + cellList.setSelection([1]); + assert.deepStrictEqual(viewModel.getSelections(), [{ start: 1, end: 2 }]); + }); + }); + + + test('notebook cell list focus/selection from UI', async function () { + await withTestNotebook( + instantiationService, + [ + ['# header a', 'markdown', CellKind.Markdown, [], {}], + ['var b = 1;', 'javascript', CellKind.Code, [], {}], + ['# header b', 'markdown', CellKind.Markdown, [], {}], + ['var b = 2;', 'javascript', CellKind.Code, [], {}], + ['# header c', 'markdown', CellKind.Markdown, [], {}] + ], + (editor, viewModel) => { + const cellList = createNotebookCellList(instantiationService); + cellList.attachViewModel(viewModel); + assert.deepStrictEqual(viewModel.getFocus(), { start: 0, end: 1 }); + assert.deepStrictEqual(viewModel.getSelections(), [{ start: 0, end: 1 }]); + + // arrow down, move both focus and selections + cellList.setFocus([1], new KeyboardEvent('keydown'), undefined); + cellList.setSelection([1], new KeyboardEvent('keydown'), undefined); + assert.deepStrictEqual(viewModel.getFocus(), { start: 1, end: 2 }); + assert.deepStrictEqual(viewModel.getSelections(), [{ start: 1, end: 2 }]); + + // shift+arrow down, expands selection + cellList.setFocus([2], new KeyboardEvent('keydown'), undefined); + cellList.setSelection([1, 2]); + assert.deepStrictEqual(viewModel.getFocus(), { start: 2, end: 3 }); + assert.deepStrictEqual(viewModel.getSelections(), [{ start: 1, end: 3 }]); + + // arrow down, will move focus but not expand selection + cellList.setFocus([3], new KeyboardEvent('keydown'), undefined); + assert.deepStrictEqual(viewModel.getFocus(), { start: 3, end: 4 }); + assert.deepStrictEqual(viewModel.getSelections(), [{ start: 1, end: 3 }]); + }); + }); + + + test('notebook cell list focus/selection with folding regions', async function () { + await withTestNotebook( instantiationService, [ ['# header a', 'markdown', CellKind.Markdown, [], {}], @@ -100,6 +144,8 @@ suite('NotebookCellList focus/selection', () => { const cellList = createNotebookCellList(instantiationService); cellList.attachViewModel(viewModel); assert.strictEqual(cellList.length, 5); + assert.deepStrictEqual(viewModel.getFocus(), { start: 0, end: 1 }); + assert.deepStrictEqual(viewModel.getSelections(), [{ start: 0, end: 1 }]); cellList.setFocus([0]); updateFoldingStateAtIndex(foldingModel, 0, true); @@ -108,25 +154,110 @@ suite('NotebookCellList focus/selection', () => { cellList.setHiddenAreas(viewModel.getHiddenRanges(), true); assert.strictEqual(cellList.length, 3); - // currently, focus on a folded cell will only select the cell itself, excluding its "inner" cells - assert.deepStrictEqual(viewModel.getSelection(), { start: 0, end: 1 }); + // currently, focus on a folded cell will only focus the cell itself, excluding its "inner" cells + assert.deepStrictEqual(viewModel.getFocus(), { start: 0, end: 1 }); + assert.deepStrictEqual(viewModel.getSelections(), [{ start: 0, end: 1 }]); cellList.focusNext(1, false); // focus next should skip the folded items - assert.deepStrictEqual(viewModel.getSelection(), { start: 2, end: 3 }); - assert.deepStrictEqual(viewModel.getSelections(), [{ start: 2, end: 3 }]); + assert.deepStrictEqual(viewModel.getFocus(), { start: 2, end: 3 }); + assert.deepStrictEqual(viewModel.getSelections(), [{ start: 0, end: 1 }]); // unfold updateFoldingStateAtIndex(foldingModel, 2, false); viewModel.updateFoldingRanges(foldingModel.regions); cellList.setHiddenAreas(viewModel.getHiddenRanges(), true); assert.strictEqual(cellList.length, 4); - assert.deepStrictEqual(viewModel.getSelection(), { start: 2, end: 3 }); + assert.deepStrictEqual(viewModel.getFocus(), { start: 2, end: 3 }); }); }); - test('notebook validate range', () => { - withTestNotebook( + test('notebook cell list focus/selection with folding regions and applyEdits', async function () { + await withTestNotebook( + instantiationService, + [ + ['# header a', 'markdown', CellKind.Markdown, [], {}], + ['var b = 1;', 'javascript', CellKind.Code, [], {}], + ['# header b', 'markdown', CellKind.Markdown, [], {}], + ['var b = 2;', 'javascript', CellKind.Code, [], {}], + ['var c = 3', 'javascript', CellKind.Markdown, [], {}], + ['# header d', 'markdown', CellKind.Markdown, [], {}], + ['var e = 4;', 'javascript', CellKind.Code, [], {}], + ], + (editor, viewModel) => { + const foldingModel = new FoldingModel(); + foldingModel.attachViewModel(viewModel); + + const cellList = createNotebookCellList(instantiationService); + cellList.attachViewModel(viewModel); + cellList.setFocus([0]); + cellList.setSelection([0]); + + updateFoldingStateAtIndex(foldingModel, 0, true); + updateFoldingStateAtIndex(foldingModel, 2, true); + viewModel.updateFoldingRanges(foldingModel.regions); + cellList.setHiddenAreas(viewModel.getHiddenRanges(), true); + assert.strictEqual(cellList.getModelIndex2(0), 0); + assert.strictEqual(cellList.getModelIndex2(1), 2); + + viewModel.notebookDocument.applyEdits([{ + editType: CellEditType.Replace, index: 0, count: 2, cells: [] + }], true, undefined, () => undefined, undefined, false); + viewModel.updateFoldingRanges(foldingModel.regions); + cellList.setHiddenAreas(viewModel.getHiddenRanges(), true); + + assert.strictEqual(cellList.getModelIndex2(0), 0); + assert.strictEqual(cellList.getModelIndex2(1), 3); + + // mimic undo + viewModel.notebookDocument.applyEdits([{ + editType: CellEditType.Replace, index: 0, count: 0, cells: [ + new TestCell(viewModel.viewType, 7, '# header f', 'markdown', CellKind.Code, [], textModelService), + new TestCell(viewModel.viewType, 8, 'var g = 5;', 'javascript', CellKind.Code, [], textModelService) + ] + }], true, undefined, () => undefined, undefined, false); + viewModel.updateFoldingRanges(foldingModel.regions); + cellList.setHiddenAreas(viewModel.getHiddenRanges(), true); + assert.strictEqual(cellList.getModelIndex2(0), 0); + assert.strictEqual(cellList.getModelIndex2(1), 1); + assert.strictEqual(cellList.getModelIndex2(2), 2); + + + }); + }); + + test('notebook cell list getModelIndex', async function () { + await withTestNotebook( + instantiationService, + [ + ['# header a', 'markdown', CellKind.Markdown, [], {}], + ['var b = 1;', 'javascript', CellKind.Code, [], {}], + ['# header b', 'markdown', CellKind.Markdown, [], {}], + ['var b = 2;', 'javascript', CellKind.Code, [], {}], + ['# header c', 'markdown', CellKind.Markdown, [], {}] + ], + (editor, viewModel) => { + const foldingModel = new FoldingModel(); + foldingModel.attachViewModel(viewModel); + + const cellList = createNotebookCellList(instantiationService); + cellList.attachViewModel(viewModel); + + updateFoldingStateAtIndex(foldingModel, 0, true); + updateFoldingStateAtIndex(foldingModel, 2, true); + viewModel.updateFoldingRanges(foldingModel.regions); + cellList.setHiddenAreas(viewModel.getHiddenRanges(), true); + + assert.deepStrictEqual(cellList.getModelIndex2(-1), 0); + assert.deepStrictEqual(cellList.getModelIndex2(0), 0); + assert.deepStrictEqual(cellList.getModelIndex2(1), 2); + assert.deepStrictEqual(cellList.getModelIndex2(2), 4); + }); + }); + + + test('notebook validate range', async () => { + await withTestNotebook( instantiationService, [ ['# header a', 'markdown', CellKind.Markdown, [], {}], @@ -145,30 +276,31 @@ suite('NotebookCellList focus/selection', () => { }); }); - test('notebook updateSelectionState', function () { - withTestNotebook( + test('notebook updateSelectionState', async function () { + await withTestNotebook( instantiationService, [ ['# header a', 'markdown', CellKind.Markdown, [], {}], ['var b = 1;', 'javascript', CellKind.Code, [], {}] ], (editor, viewModel) => { - viewModel.updateSelectionsState({ kind: SelectionStateType.Index, selections: [{ start: 1, end: 2 }, { start: -1, end: 0 }] }); + viewModel.updateSelectionsState({ kind: SelectionStateType.Index, focus: { start: 1, end: 2 }, selections: [{ start: 1, end: 2 }, { start: -1, end: 0 }] }); assert.deepStrictEqual(viewModel.getSelections(), [{ start: 1, end: 2 }]); }); }); - test('notebook cell selection w/ cell deletion', function () { - withTestNotebook( + test('notebook cell selection w/ cell deletion', async function () { + await withTestNotebook( instantiationService, [ ['# header a', 'markdown', CellKind.Markdown, [], {}], ['var b = 1;', 'javascript', CellKind.Code, [], {}] ], (editor, viewModel) => { - viewModel.updateSelectionsState({ kind: SelectionStateType.Index, selections: [{ start: 1, end: 2 }] }); + viewModel.updateSelectionsState({ kind: SelectionStateType.Index, focus: { start: 1, end: 2 }, selections: [{ start: 1, end: 2 }] }); viewModel.deleteCell(1, true, false); - assert.deepStrictEqual(viewModel.getSelections(), [{ start: 0, end: 1 }]); + assert.deepStrictEqual(viewModel.getFocus(), { start: 0, end: 1 }); + assert.deepStrictEqual(viewModel.getSelections(), []); }); }); }); diff --git a/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts index 285c567a834..079191fba8c 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts @@ -279,7 +279,7 @@ suite('NotebookTextModel', () => { textModel.applyEdits([ { editType: CellEditType.Replace, index: 1, count: 1, cells: [] }, { editType: CellEditType.Replace, index: 1, count: 0, cells: [new TestCell(viewModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], textModelService)] }, - ], true, undefined, () => ({ kind: SelectionStateType.Index, selections: [{ start: 0, end: 1 }] }), undefined); + ], true, undefined, () => ({ kind: SelectionStateType.Index, focus: { start: 0, end: 1 }, selections: [{ start: 0, end: 1 }] }), undefined); assert.equal(textModel.cells.length, 4); assert.equal(textModel.cells[0].getValue(), 'var a = 1;'); @@ -318,7 +318,7 @@ suite('NotebookTextModel', () => { editType: CellEditType.Metadata, metadata: { editable: false }, } - ], true, undefined, () => ({ kind: SelectionStateType.Index, selections: [{ start: 0, end: 1 }] }), undefined); + ], true, undefined, () => ({ kind: SelectionStateType.Index, focus: { start: 0, end: 1 }, selections: [{ start: 0, end: 1 }] }), undefined); assert.notEqual(changeEvent, undefined); assert.equal(changeEvent!.rawEvents.length, 2); diff --git a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts index 277224659e1..936b24e7a3d 100644 --- a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts @@ -67,6 +67,18 @@ export class TestNotebookEditor implements INotebookEditor { creationOptions: INotebookEditorCreationOptions = { isEmbedded: false }; constructor(readonly viewModel: NotebookViewModel) { } + getCellRangeFromViewRange(startIndex: number, endIndex: number): ICellRange | undefined { + throw new Error('Method not implemented.'); + } + getViewIndex(cell: ICellViewModel): number { + throw new Error('Method not implemented.'); + } + getCellsFromViewRange(startIndex: number, endIndex: number): ICellViewModel[] { + throw new Error('Method not implemented.'); + } + getFocus(): ICellRange | undefined { + return undefined; + } getVisibleRangesPlusViewportAboveBelow(): ICellRange[] { throw new Error('Method not implemented.'); }