diff --git a/src/vs/workbench/contrib/notebook/browser/controller/cellOperations.ts b/src/vs/workbench/contrib/notebook/browser/controller/cellOperations.ts index 6bfccad2a61..70dca20c5c3 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/cellOperations.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/cellOperations.ts @@ -7,12 +7,13 @@ import { IBulkEditService, ResourceEdit, ResourceTextEdit } from 'vs/editor/brow import { IPosition, Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { EndOfLinePreference, IReadonlyTextBuffer } from 'vs/editor/common/model'; +import { IModeService } from 'vs/editor/common/services/modeService'; import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; import { INotebookActionContext, INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { CellEditState, CellFocusMode, expandCellRangesWithHiddenCells, IActiveNotebookEditor, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; +import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { cloneNotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { CellEditType, CellKind, ICellEditOperation, ICellReplaceEdit, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellEditType, CellKind, ICellEditOperation, ICellReplaceEdit, IOutputDto, ISelectionState, NotebookCellMetadata, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { cellRangeContains, cellRangesToIndexes, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; export async function changeCellToKind(kind: CellKind, context: INotebookActionContext, language?: string, mime?: string): Promise { @@ -571,3 +572,79 @@ export function computeCellLinesContents(cell: ICellViewModel, splitPoints: IPos return newLineModels; } + +export function insertCell( + modeService: IModeService, + editor: IActiveNotebookEditor, + cell: ICellViewModel | undefined, + type: CellKind, + direction: 'above' | 'below' = 'above', + initialText: string = '', + ui: boolean = false +) { + const viewModel = editor._getViewModel(); + const activeKernel = editor.activeKernel; + if (viewModel.options.isReadOnly) { + return null; + } + + const index = cell ? viewModel.getCellIndex(cell) : 0; + const nextIndex = ui ? viewModel.getNextVisibleCellIndex(index) : index + 1; + let language; + if (type === CellKind.Code) { + const supportedLanguages = activeKernel?.supportedLanguages ?? modeService.getRegisteredModes(); + const defaultLanguage = supportedLanguages[0] || 'plaintext'; + if (cell?.cellKind === CellKind.Code) { + language = cell.language; + } else if (cell?.cellKind === CellKind.Markup) { + const nearestCodeCellIndex = viewModel.nearestCodeCellIndex(index); + if (nearestCodeCellIndex > -1) { + language = viewModel.cellAt(nearestCodeCellIndex)!.language; + } else { + language = defaultLanguage; + } + } else { + if (cell === undefined && direction === 'above') { + // insert cell at the very top + language = viewModel.viewCells.find(cell => cell.cellKind === CellKind.Code)?.language || defaultLanguage; + } else { + language = defaultLanguage; + } + } + + if (!supportedLanguages.includes(language)) { + // the language no longer exists + language = defaultLanguage; + } + } else { + language = 'markdown'; + } + + const insertIndex = cell ? + (direction === 'above' ? index : nextIndex) : + index; + return insertCellAtIndex(viewModel, insertIndex, initialText, language, type, undefined, [], true); +} + +export function insertCellAtIndex(viewModel: NotebookViewModel, index: number, source: string, language: string, type: CellKind, metadata: NotebookCellMetadata | undefined, outputs: IOutputDto[], synchronous: boolean, pushUndoStop: boolean = true): CellViewModel { + const endSelections: ISelectionState = { kind: SelectionStateType.Index, focus: { start: index, end: index + 1 }, selections: [{ start: index, end: index + 1 }] }; + viewModel.notebookDocument.applyEdits([ + { + editType: CellEditType.Replace, + index, + count: 0, + cells: [ + { + cellKind: type, + language: language, + mime: undefined, + outputs: outputs, + metadata: metadata, + source: source + } + ] + } + ], synchronous, { kind: SelectionStateType.Index, focus: viewModel.getFocus(), selections: viewModel.getSelections() }, () => endSelections, undefined, pushUndoStop); + return viewModel.cellAt(index)!; +} + diff --git a/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts index cd95bc358e7..bae088c61ce 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts @@ -7,11 +7,13 @@ import { maxIndex, minIndex } from 'vs/base/common/arrays'; import { Iterable } from 'vs/base/common/iterator'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { URI, UriComponents } from 'vs/base/common/uri'; +import { IModeService } from 'vs/editor/common/services/modeService'; import { localize } from 'vs/nls'; import { MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { EditorsOrder } from 'vs/workbench/common/editor'; +import { insertCell } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; import { cellExecutionArgs, CellToolbarOrder, CELL_TITLE_CELL_GROUP_ID, executeNotebookCondition, getContextFromActiveEditor, getContextFromUri, INotebookActionContext, INotebookCellActionContext, INotebookCellToolbarActionContext, INotebookCommandContext, NotebookAction, NotebookCellAction, NotebookMultiCellAction, NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT, parseMultiCellExecutionArgs } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { CellEditState, CellFocusMode, EXECUTE_CELL_COMMAND_ID, NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_TYPE, NOTEBOOK_HAS_RUNNING_CELL, NOTEBOOK_INTERRUPTIBLE_KERNEL, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_MISSING_KERNEL_EXTENSION } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; @@ -407,6 +409,7 @@ registerAction2(class ExecuteCellSelectBelow extends NotebookCellAction { if (typeof idx !== 'number') { return; } + const modeService = accessor.get(IModeService); if (context.cell.cellKind === CellKind.Markup) { const nextCell = context.notebookEditor.cellAt(idx + 1); @@ -414,7 +417,8 @@ registerAction2(class ExecuteCellSelectBelow extends NotebookCellAction { if (nextCell) { context.notebookEditor.focusNotebookCell(nextCell, 'container'); } else { - const newCell = context.notebookEditor.insertNotebookCell(context.cell, CellKind.Markup, 'below'); + const newCell = insertCell(modeService, context.notebookEditor, context.cell, CellKind.Markup, 'below'); + if (newCell) { context.notebookEditor.focusNotebookCell(newCell, 'editor'); } @@ -426,7 +430,8 @@ registerAction2(class ExecuteCellSelectBelow extends NotebookCellAction { if (nextCell) { context.notebookEditor.focusNotebookCell(nextCell, 'container'); } else { - const newCell = context.notebookEditor.insertNotebookCell(context.cell, CellKind.Code, 'below'); + const newCell = insertCell(modeService, context.notebookEditor, context.cell, CellKind.Code, 'below'); + if (newCell) { context.notebookEditor.focusNotebookCell(newCell, 'editor'); } @@ -452,10 +457,11 @@ registerAction2(class ExecuteCellInsertBelow extends NotebookCellAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { + const modeService = accessor.get(IModeService); const newFocusMode = context.cell.focusMode === CellFocusMode.Editor ? 'editor' : 'container'; - const executionP = runCell(accessor, context); - const newCell = context.notebookEditor.insertNotebookCell(context.cell, CellKind.Code, 'below'); + const newCell = insertCell(modeService, context.notebookEditor, context.cell, CellKind.Code, 'below'); + if (newCell) { context.notebookEditor.focusNotebookCell(newCell, newFocusMode); } diff --git a/src/vs/workbench/contrib/notebook/browser/controller/insertCellActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/insertCellActions.ts index 2d190218c7a..0f242119c79 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/insertCellActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/insertCellActions.ts @@ -5,12 +5,14 @@ import { Codicon } from 'vs/base/common/codicons'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { IModeService } from 'vs/editor/common/services/modeService'; import { localize } from 'vs/nls'; import { IAction2Options, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { InputFocusedContext } from 'vs/platform/contextkey/common/contextkeys'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { insertCell } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; import { INotebookActionContext, NotebookAction } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_EDITOR_EDITABLE } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; @@ -41,12 +43,13 @@ abstract class InsertCellCommand extends NotebookAction { context.notebookEditor.focus(); } + const modeService = accessor.get(IModeService); if (context.cell) { - newCell = context.notebookEditor.insertNotebookCell(context.cell, this.kind, this.direction, undefined, true); + newCell = insertCell(modeService, context.notebookEditor, context.cell, this.kind, this.direction, undefined, true); } else { const focusRange = context.notebookEditor.getFocus(); const next = focusRange.end - 1; - newCell = context.notebookEditor.insertNotebookCell(context.notebookEditor.cellAt(next), this.kind, this.direction, undefined, true); + newCell = insertCell(modeService, context.notebookEditor, context.notebookEditor.cellAt(next), this.kind, this.direction, undefined, true); } if (newCell) { @@ -181,7 +184,9 @@ registerAction2(class InsertCodeCellAtTopAction extends NotebookAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { - const newCell = context.notebookEditor.insertNotebookCell(undefined, CellKind.Code, 'above', undefined, true); + const modeService = accessor.get(IModeService); + const newCell = insertCell(modeService, context.notebookEditor, undefined, CellKind.Code, 'above', undefined, true); + if (newCell) { context.notebookEditor.focusNotebookCell(newCell, 'editor'); } @@ -206,7 +211,9 @@ registerAction2(class InsertMarkdownCellAtTopAction extends NotebookAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { - const newCell = context.notebookEditor.insertNotebookCell(undefined, CellKind.Markup, 'above', undefined, true); + const modeService = accessor.get(IModeService); + const newCell = insertCell(modeService, context.notebookEditor, undefined, CellKind.Markup, 'above', undefined, true); + if (newCell) { context.notebookEditor.focusNotebookCell(newCell, 'editor'); } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index a52498afd78..09f172f8522 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -464,11 +464,6 @@ export interface INotebookEditor { */ getOutputRenderer(): OutputRenderer; - /** - * Insert a new cell around `cell` - */ - insertNotebookCell(cell: ICellViewModel | undefined, type: CellKind, direction?: 'above' | 'below', initialText?: string, ui?: boolean): CellViewModel | null; - /** * Focus the container of a cell (the monaco editor inside is not focused). */ diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index cbfb99259fe..fbba109883a 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -2017,63 +2017,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD return new Promise(resolve => { r = resolve; }); } - private _nearestCodeCellIndex(index: number /* exclusive */) { - if (!this.viewModel) { - return -1; - } - - return this.viewModel.nearestCodeCellIndex(index); - } - - insertNotebookCell(cell: ICellViewModel | undefined, type: CellKind, direction: 'above' | 'below' = 'above', initialText: string = '', ui: boolean = false): CellViewModel | null { - if (!this.viewModel) { - return null; - } - - if (this.viewModel.options.isReadOnly) { - return null; - } - - const index = cell ? this.viewModel.getCellIndex(cell) : 0; - const nextIndex = ui ? this.viewModel.getNextVisibleCellIndex(index) : index + 1; - let language; - if (type === CellKind.Code) { - const supportedLanguages = this.activeKernel?.supportedLanguages ?? this.modeService.getRegisteredModes(); - const defaultLanguage = supportedLanguages[0] || 'plaintext'; - if (cell?.cellKind === CellKind.Code) { - language = cell.language; - } else if (cell?.cellKind === CellKind.Markup) { - const nearestCodeCellIndex = this._nearestCodeCellIndex(index); - if (nearestCodeCellIndex > -1) { - language = this.viewModel.cellAt(nearestCodeCellIndex)!.language; - } else { - language = defaultLanguage; - } - } else { - if (cell === undefined && direction === 'above') { - // insert cell at the very top - language = this.viewModel.viewCells.find(cell => cell.cellKind === CellKind.Code)?.language || defaultLanguage; - } else { - language = defaultLanguage; - } - } - - if (!supportedLanguages.includes(language)) { - // the language no longer exists - language = defaultLanguage; - } - } else { - language = 'markdown'; - } - - const insertIndex = cell ? - (direction === 'above' ? index : nextIndex) : - index; - const focused = this._list.getFocusedElements(); - const selections = this._list.getSelectedElements(); - return this.viewModel.createCell(insertIndex, initialText, language, type, undefined, [], true, undefined, focused[0]?.handle ?? null, selections); - } - getActiveCell() { const elements = this._list.getFocusedElements(); diff --git a/src/vs/workbench/contrib/notebook/test/notebookEditorKernelManager.test.ts b/src/vs/workbench/contrib/notebook/test/notebookEditorKernelManager.test.ts index e0eb2b1a961..6fd17c74c03 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookEditorKernelManager.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookEditorKernelManager.test.ts @@ -21,6 +21,7 @@ import { mock } from 'vs/base/test/common/mock'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Mimes } from 'vs/base/common/mime'; +import { insertCellAtIndex } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; suite('NotebookEditorKernelManager', () => { @@ -62,7 +63,7 @@ suite('NotebookEditorKernelManager', () => { async (viewModel) => { const kernelManager = instantiationService.createInstance(NotebookEditorKernelManager); - const cell = viewModel.createCell(1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true); + const cell = insertCellAtIndex(viewModel, 1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true); await assertThrowsAsync(async () => await kernelManager.executeNotebookCell(cell)); }); }); @@ -74,7 +75,7 @@ suite('NotebookEditorKernelManager', () => { kernelService.registerKernel(new TestNotebookKernel({ languages: ['testlang'] })); const kernelManager = instantiationService.createInstance(NotebookEditorKernelManager); - const cell = viewModel.createCell(1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true); + const cell = insertCellAtIndex(viewModel, 1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true); await assertThrowsAsync(async () => await kernelManager.executeNotebookCell(cell)); }); @@ -90,7 +91,7 @@ suite('NotebookEditorKernelManager', () => { const executeSpy = sinon.spy(); kernel.executeNotebookCellsRequest = executeSpy; - const cell = viewModel.createCell(0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true); + const cell = insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true); await kernelManager.executeNotebookCells(viewModel.notebookDocument, [cell]); assert.strictEqual(executeSpy.calledOnce, true); }); @@ -121,7 +122,7 @@ suite('NotebookEditorKernelManager', () => { let event: ISelectedNotebooksChangeEvent | undefined; kernelService.onDidChangeSelectedNotebooks(e => event = e); - const cell = viewModel.createCell(0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true); + const cell = insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); await kernelManager.executeNotebookCells(viewModel.notebookDocument, [cell]); assert.strictEqual(didExecute, true); diff --git a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts index 8a47126767b..f7cb8585cec 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts @@ -15,6 +15,7 @@ import { TestConfigurationService } from 'vs/platform/configuration/test/common/ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; +import { insertCellAtIndex } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; import { reduceCellRanges } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; @@ -51,7 +52,7 @@ suite('NotebookViewModel', () => { ['var b = 2;', 'javascript', CellKind.Code, [], {}] ], (editor, viewModel) => { - const cell = viewModel.createCell(1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true, null, []); + const cell = insertCellAtIndex(viewModel, 1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); assert.strictEqual(viewModel.length, 3); assert.strictEqual(viewModel.notebookDocument.cells.length, 3); assert.strictEqual(viewModel.getCellIndex(cell), 1); @@ -125,13 +126,13 @@ suite('NotebookViewModel', () => { const lastViewCell = viewModel.cellAt(viewModel.length - 1)!; const insertIndex = viewModel.getCellIndex(firstViewCell) + 1; - const cell = viewModel.createCell(insertIndex, 'var c = 3;', 'javascript', CellKind.Code, {}, [], true); + const cell = insertCellAtIndex(viewModel, insertIndex, 'var c = 3;', 'javascript', CellKind.Code, {}, [], true); const addedCellIndex = viewModel.getCellIndex(cell); viewModel.deleteCell(addedCellIndex, true); const secondInsertIndex = viewModel.getCellIndex(lastViewCell) + 1; - const cell2 = viewModel.createCell(secondInsertIndex, 'var d = 4;', 'javascript', CellKind.Code, {}, [], true); + const cell2 = insertCellAtIndex(viewModel, secondInsertIndex, 'var d = 4;', 'javascript', CellKind.Code, {}, [], true); assert.strictEqual(viewModel.length, 3); assert.strictEqual(viewModel.notebookDocument.cells.length, 3); @@ -184,7 +185,7 @@ suite('NotebookViewModel Decorations', () => { end: 2, }); - viewModel.createCell(0, 'var d = 6;', 'javascript', CellKind.Code, {}, [], true); + insertCellAtIndex(viewModel, 0, 'var d = 6;', 'javascript', CellKind.Code, {}, [], true); assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), { start: 2, @@ -198,7 +199,7 @@ suite('NotebookViewModel Decorations', () => { end: 2 }); - viewModel.createCell(3, 'var d = 7;', 'javascript', CellKind.Code, {}, [], true); + insertCellAtIndex(viewModel, 3, 'var d = 7;', 'javascript', CellKind.Code, {}, [], true); assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), { start: 1, @@ -241,14 +242,14 @@ suite('NotebookViewModel Decorations', () => { end: 3 }); - viewModel.createCell(5, 'var d = 9;', 'javascript', CellKind.Code, {}, [], true); + insertCellAtIndex(viewModel, 5, 'var d = 9;', 'javascript', CellKind.Code, {}, [], true); assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), { start: 1, end: 3 }); - viewModel.createCell(4, 'var d = 10;', 'javascript', CellKind.Code, {}, [], true); + insertCellAtIndex(viewModel, 4, 'var d = 10;', 'javascript', CellKind.Code, {}, [], true); assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), { start: 1,