Merge pull request #118482 from microsoft/rebornix/nb-list-focus
Rebornix/nb list focus
This commit is contained in:
commit
1a5d7f4f8e
|
@ -286,28 +286,9 @@ suite('Notebook API tests', function () {
|
|||
]
|
||||
});
|
||||
|
||||
const secondCell = vscode.window.activeNotebookEditor!.document.cells[1];
|
||||
|
||||
const moveCellEvent = asPromise<vscode.NotebookCellsChangeEvent>(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.NotebookCellOutputsChangeEvent>(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();
|
||||
|
||||
|
|
|
@ -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<void> {
|
||||
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<void> {
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -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>(IClipboardService);
|
||||
const notebookService = accessor.get<INotebookService>(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>(IClipboardService);
|
||||
const notebookService = accessor.get<INotebookService>(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<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
|
||||
|
|
|
@ -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<T = INotebookCellActionContext> extends NotebookAction {
|
||||
export abstract class NotebookCellAction<T = INotebookCellActionContext> 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<void> {
|
||||
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<void> {
|
||||
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({
|
||||
|
|
|
@ -390,8 +390,8 @@ export class NotebookCellOutline implements IOutline<OutlineEntry> {
|
|||
includeCodeCells = this._configurationService.getValue<boolean>('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<OutlineEntry> {
|
|||
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<OutlineEntry> {
|
|||
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, []);
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
||||
|
|
|
@ -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<void>;
|
||||
readonly onDidChangeSelection: Event<void>;
|
||||
getSelection(): ICellRange | undefined;
|
||||
getSelections(): ICellRange[];
|
||||
visibleRanges: ICellRange[];
|
||||
textModel?: NotebookTextModel;
|
||||
|
@ -571,6 +570,23 @@ export interface INotebookEditor extends ICommonNotebookEditor {
|
|||
*/
|
||||
revealRangeInCenterIfOutsideViewportAsync(cell: ICellViewModel, range: Range): Promise<void>;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -293,7 +293,7 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> 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<CellViewModel> 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<CellViewModel> 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<CellViewModel> implements ID
|
|||
}
|
||||
|
||||
this.hiddenRangesPrefixSum = new PrefixSumComputer(values);
|
||||
|
||||
if (triggerViewUpdate) {
|
||||
this.updateHiddenAreasInView(oldRanges, newRanges);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -445,12 +451,30 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> 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<CellViewModel> 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<CellViewModel> 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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 }]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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(), []);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.');
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue