From 1121b3f21e42529b29c3b2555de507b21f36d9b4 Mon Sep 17 00:00:00 2001 From: Jonah Iden Date: Mon, 29 Apr 2024 10:16:04 +0200 Subject: [PATCH] select next node when on first or last line of editor Signed-off-by: Jonah Iden --- .../notebook-actions-contribution.ts | 26 ++++++++++++++++--- .../contributions/notebook-context-keys.ts | 5 ++++ .../browser/view-model/notebook-cell-model.ts | 8 +++--- .../src/browser/view/notebook-cell-editor.tsx | 25 +++++++++++++++++- 4 files changed, 56 insertions(+), 8 deletions(-) diff --git a/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts b/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts index 00ae102716ac7..d3d3fb25e8c88 100644 --- a/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts +++ b/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts @@ -24,8 +24,9 @@ import { NotebookKernelQuickPickService } from '../service/notebook-kernel-quick import { NotebookExecutionService } from '../service/notebook-execution-service'; import { NotebookEditorWidget } from '../notebook-editor-widget'; import { NotebookEditorWidgetService } from '../service/notebook-editor-widget-service'; -import { NOTEBOOK_CELL_FOCUSED, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_HAS_OUTPUTS } from './notebook-context-keys'; +import { NOTEBOOK_CELL_CURSOR_FIRST_LINE, NOTEBOOK_CELL_CURSOR_LAST_LINE, NOTEBOOK_CELL_FOCUSED, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_HAS_OUTPUTS } from './notebook-context-keys'; import { NotebookClipboardService } from '../service/notebook-clipboard-service'; +import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; export namespace NotebookCommands { export const ADD_NEW_CELL_COMMAND = Command.toDefaultLocalizedCommand({ @@ -110,6 +111,9 @@ export class NotebookActionsContribution implements CommandContribution, MenuCon @inject(NotebookClipboardService) protected notebookClipboardService: NotebookClipboardService; + @inject(ContextKeyService) + protected contextKeyService: ContextKeyService; + registerCommands(commands: CommandRegistry): void { commands.registerCommand(NotebookCommands.ADD_NEW_CELL_COMMAND, { execute: (notebookModel: NotebookModel, cellKind: CellKind = CellKind.Markup, index?: number | 'above' | 'below') => { @@ -169,15 +173,29 @@ export class NotebookActionsContribution implements CommandContribution, MenuCon commands.registerCommand(NotebookCommands.CHANGE_SELECTED_CELL, { execute: (change: number | CellChangeDirection) => { - const model = this.notebookEditorWidgetService.focusedEditor?.model; + const focusedEditor = this.notebookEditorWidgetService.focusedEditor; + const model = focusedEditor?.model; if (model && typeof change === 'number') { model.setSelectedCell(model.cells[change]); } else if (model && model.selectedCell) { const currentIndex = model.cells.indexOf(model.selectedCell); + const shouldFocusEditor = this.contextKeyService.match('editorTextFocus'); + if (change === CellChangeDirection.Up && currentIndex > 0) { model.setSelectedCell(model.cells[currentIndex - 1]); + if (model.selectedCell?.cellKind === CellKind.Code && shouldFocusEditor) { + model.selectedCell.requestFocusEditor('lastLine'); + } } else if (change === CellChangeDirection.Down && currentIndex < model.cells.length - 1) { model.setSelectedCell(model.cells[currentIndex + 1]); + if (model.selectedCell?.cellKind === CellKind.Code && shouldFocusEditor) { + model.selectedCell.requestFocusEditor(); + } + } + + if (model.selectedCell.cellKind === CellKind.Markup) { + // since were losing focus from the cell editor, we need to focus the notebook editor again + focusedEditor?.node.focus(); } } } @@ -294,13 +312,13 @@ export class NotebookActionsContribution implements CommandContribution, MenuCon command: NotebookCommands.CHANGE_SELECTED_CELL.id, keybinding: 'up', args: CellChangeDirection.Up, - when: `!editorTextFocus && ${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED}` + when: `(!editorTextFocus || ${NOTEBOOK_CELL_CURSOR_FIRST_LINE}) && ${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED}` }, { command: NotebookCommands.CHANGE_SELECTED_CELL.id, keybinding: 'down', args: CellChangeDirection.Down, - when: `!editorTextFocus && ${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED}` + when: `(!editorTextFocus || ${NOTEBOOK_CELL_CURSOR_LAST_LINE}) && ${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED}` }, { command: NotebookCommands.CUT_SELECTED_CELL.id, diff --git a/packages/notebook/src/browser/contributions/notebook-context-keys.ts b/packages/notebook/src/browser/contributions/notebook-context-keys.ts index 5bc987d154897..787d63748b10c 100644 --- a/packages/notebook/src/browser/contributions/notebook-context-keys.ts +++ b/packages/notebook/src/browser/contributions/notebook-context-keys.ts @@ -58,6 +58,9 @@ export const NOTEBOOK_INTERRUPTIBLE_KERNEL = 'notebookInterruptibleKernel'; export const NOTEBOOK_MISSING_KERNEL_EXTENSION = 'notebookMissingKernelExtension'; export const NOTEBOOK_HAS_OUTPUTS = 'notebookHasOutputs'; +export const NOTEBOOK_CELL_CURSOR_FIRST_LINE = 'cellEditorCursorPositionFirstLine'; +export const NOTEBOOK_CELL_CURSOR_LAST_LINE = 'cellEditorCursorPositionLastLine'; + export namespace NotebookContextKeys { export function initNotebookContextKeys(service: ContextKeyService): void { service.createKey(HAS_OPENED_NOTEBOOK, false); @@ -93,6 +96,8 @@ export namespace NotebookContextKeys { service.createKey(NOTEBOOK_CELL_INPUT_COLLAPSED, false); service.createKey(NOTEBOOK_CELL_OUTPUT_COLLAPSED, false); service.createKey(NOTEBOOK_CELL_RESOURCE, ''); + service.createKey(NOTEBOOK_CELL_CURSOR_FIRST_LINE, false); + service.createKey(NOTEBOOK_CELL_CURSOR_LAST_LINE, false); // Kernels service.createKey(NOTEBOOK_KERNEL, undefined); diff --git a/packages/notebook/src/browser/view-model/notebook-cell-model.ts b/packages/notebook/src/browser/view-model/notebook-cell-model.ts index ea637ae256fbb..303d075e0cd2a 100644 --- a/packages/notebook/src/browser/view-model/notebook-cell-model.ts +++ b/packages/notebook/src/browser/view-model/notebook-cell-model.ts @@ -36,6 +36,8 @@ import { LanguageService } from '@theia/core/lib/browser/language-service'; export const NotebookCellModelFactory = Symbol('NotebookModelFactory'); export type NotebookCellModelFactory = (props: NotebookCellModelProps) => NotebookCellModel; +export type CellEditorFocusRequest = number | 'lastLine' | undefined; + export function createNotebookCellModelContainer(parent: interfaces.Container, props: NotebookCellModelProps): interfaces.Container { const child = parent.createChild(); @@ -104,7 +106,7 @@ export class NotebookCellModel implements NotebookCell, Disposable { protected readonly onDidRequestCellEditChangeEmitter = new Emitter(); readonly onDidRequestCellEditChange = this.onDidRequestCellEditChangeEmitter.event; - protected readonly onWillFocusCellEditorEmitter = new Emitter(); + protected readonly onWillFocusCellEditorEmitter = new Emitter(); readonly onWillFocusCellEditor = this.onWillFocusCellEditorEmitter.event; protected readonly onWillBlurCellEditorEmitter = new Emitter(); @@ -278,9 +280,9 @@ export class NotebookCellModel implements NotebookCell, Disposable { this.onDidRequestCellEditChangeEmitter.fire(false); } - requestFocusEditor(): void { + requestFocusEditor(focusRequest?: CellEditorFocusRequest): void { this.requestEdit(); - this.onWillFocusCellEditorEmitter.fire(); + this.onWillFocusCellEditorEmitter.fire(focusRequest); } requestBlurEditor(): void { diff --git a/packages/notebook/src/browser/view/notebook-cell-editor.tsx b/packages/notebook/src/browser/view/notebook-cell-editor.tsx index f0e8de320eed2..0ffeef23f88ab 100644 --- a/packages/notebook/src/browser/view/notebook-cell-editor.tsx +++ b/packages/notebook/src/browser/view/notebook-cell-editor.tsx @@ -25,6 +25,7 @@ import { NotebookContextManager } from '../service/notebook-context-manager'; import { DisposableCollection, OS } from '@theia/core'; import { NotebookViewportService } from './notebook-viewport-service'; import { BareFontInfo } from '@theia/monaco-editor-core/esm/vs/editor/common/config/fontInfo'; +import { NOTEBOOK_CELL_CURSOR_FIRST_LINE, NOTEBOOK_CELL_CURSOR_LAST_LINE } from '../contributions/notebook-context-keys'; interface CellEditorProps { notebookModel: NotebookModel, @@ -54,8 +55,19 @@ export class CellEditor extends React.Component { override componentDidMount(): void { this.disposeEditor(); - this.toDispose.push(this.props.cell.onWillFocusCellEditor(() => { + this.toDispose.push(this.props.cell.onWillFocusCellEditor(focusRequest => { this.editor?.getControl().focus(); + const lineCount = this.editor?.getControl().getModel()?.getLineCount(); + if (focusRequest && lineCount) { + this.editor?.getControl().setPosition(focusRequest === 'lastLine' ? + { lineNumber: lineCount, column: 1 } : + { lineNumber: focusRequest, column: 1 }, + 'keyboard'); + } + const currentLine = this.editor?.getControl().getPosition()?.lineNumber; + this.props.notebookContextManager.scopedStore.setContext(NOTEBOOK_CELL_CURSOR_FIRST_LINE, currentLine === 1); + this.props.notebookContextManager.scopedStore.setContext(NOTEBOOK_CELL_CURSOR_LAST_LINE, currentLine === lineCount); + })); this.toDispose.push(this.props.cell.onDidChangeEditorOptions(options => { @@ -119,10 +131,21 @@ export class CellEditor extends React.Component { })); this.toDispose.push(this.editor.getControl().onDidFocusEditorText(() => { this.props.notebookContextManager.onDidEditorTextFocus(true); + this.props.notebookModel.setSelectedCell(cell); })); this.toDispose.push(this.editor.getControl().onDidBlurEditorText(() => { this.props.notebookContextManager.onDidEditorTextFocus(false); })); + this.toDispose.push(this.editor.getControl().onDidChangeCursorPosition(e => { + if (e.secondaryPositions.length === 0) { + this.props.notebookContextManager.scopedStore.setContext(NOTEBOOK_CELL_CURSOR_FIRST_LINE, e.position.lineNumber === 1); + this.props.notebookContextManager.scopedStore.setContext(NOTEBOOK_CELL_CURSOR_LAST_LINE, + e.position.lineNumber === this.editor!.getControl().getModel()!.getLineCount()); + } else { + this.props.notebookContextManager.scopedStore.setContext(NOTEBOOK_CELL_CURSOR_FIRST_LINE, false); + this.props.notebookContextManager.scopedStore.setContext(NOTEBOOK_CELL_CURSOR_LAST_LINE, false); + } + })); if (cell.editing && notebookModel.selectedCell === cell) { this.editor.getControl().focus(); }