From f48e190b1869ccc5b8aae97fef8646b63e368241 Mon Sep 17 00:00:00 2001 From: rebornix Date: Fri, 13 Mar 2020 09:51:09 -0700 Subject: [PATCH] cursor movement first cut --- src/vs/platform/actions/common/actions.ts | 4 +- .../browser/contrib/notebookActions.ts | 114 +++++++++++++++++- .../notebook/browser/notebookBrowser.ts | 12 ++ .../notebook/browser/notebookEditor.ts | 17 ++- .../notebook/browser/view/notebookCellList.ts | 49 +++++++- .../browser/view/renderers/codeCell.ts | 8 +- .../browser/view/renderers/markdownCell.ts | 8 +- .../viewModel/notebookCellViewModel.ts | 61 +++++++++- .../contrib/notebook/common/notebookCommon.ts | 3 + 9 files changed, 258 insertions(+), 18 deletions(-) diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index ab37df3e9fa..ade8ecf75f3 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -425,14 +425,14 @@ export function registerAction2(ctor: { new(): Action2 }): IDisposable { KeybindingsRegistry.registerKeybindingRule({ ...item, id: command.id, - when: ContextKeyExpr.and(command.precondition, item.when) + when: command.precondition ? ContextKeyExpr.and(command.precondition, item.when) : item.when }); } } else if (keybinding) { KeybindingsRegistry.registerKeybindingRule({ ...keybinding, id: command.id, - when: ContextKeyExpr.and(command.precondition, keybinding.when) + when: command.precondition ? ContextKeyExpr.and(command.precondition, keybinding.when) : keybinding.when }); } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts index 71e05eda054..c170793f073 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts @@ -12,8 +12,8 @@ import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebook import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { NOTEBOOK_EDITOR_FOCUSED, NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, CellState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellKind, NOTEBOOK_EDITOR_CURSOR_BOUNDARY } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel'; registerAction2(class extends Action2 { @@ -153,6 +153,10 @@ registerAction2(class extends Action2 { let activeCell = editor.getActiveCell(); if (activeCell) { + if (activeCell.cellKind === CellKind.Markdown) { + activeCell.state = CellState.Read; + } + editor.focusNotebookCell(activeCell, false); } } @@ -361,3 +365,109 @@ function changeActiveCellToKind(kind: CellKind, accessor: ServicesAccessor): voi editor.focusNotebookCell(newCell, false); editor.deleteNotebookCell(activeCell); } + +function getActiveCell(accessor: ServicesAccessor): [NotebookEditor, CellViewModel] | undefined { + const editorService = accessor.get(IEditorService); + const notebookService = accessor.get(INotebookService); + + const resource = editorService.activeEditor?.resource; + if (!resource) { + return; + } + + const editor = getActiveNotebookEditor(editorService); + if (!editor) { + return; + } + + const notebookProviders = notebookService.getContributedNotebookProviders(resource); + if (!notebookProviders.length) { + return; + } + + const activeCell = editor.getActiveCell(); + if (!activeCell) { + return; + } + + return [editor, activeCell]; +} + + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.notebook.cursorDown', + title: 'Notebook Cursor Move Down', + keybinding: { + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.has(InputFocusedContextKey), NOTEBOOK_EDITOR_CURSOR_BOUNDARY.notEqualsTo('top'), NOTEBOOK_EDITOR_CURSOR_BOUNDARY.notEqualsTo('none')), + primary: KeyCode.DownArrow, + weight: KeybindingWeight.WorkbenchContrib + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const activeCellRet = getActiveCell(accessor); + + if (!activeCellRet) { + return; + } + + const [editor, activeCell] = activeCellRet; + + const idx = editor.viewModel?.getViewCellIndex(activeCell); + if (typeof idx !== 'number') { + return; + } + + const newCell = editor.viewModel?.viewCells[idx + 1]; + + if (!newCell) { + return; + } + + editor.focusNotebookCell(newCell, true); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.notebook.cursorUp', + title: 'Notebook Cursor Move Up', + keybinding: { + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.has(InputFocusedContextKey), NOTEBOOK_EDITOR_CURSOR_BOUNDARY.notEqualsTo('bottom'), NOTEBOOK_EDITOR_CURSOR_BOUNDARY.notEqualsTo('none')), + primary: KeyCode.UpArrow, + weight: KeybindingWeight.WorkbenchContrib + }, + }); + } + + async run(accessor: ServicesAccessor): Promise { + const activeCellRet = getActiveCell(accessor); + + if (!activeCellRet) { + return; + } + + const [editor, activeCell] = activeCellRet; + const idx = editor.viewModel?.getViewCellIndex(activeCell); + if (typeof idx !== 'number') { + return; + } + + if (idx < 1) { + // we don't do loop + return; + } + + const newCell = editor.viewModel?.viewCells[idx - 1]; + + if (!newCell) { + return; + } + + editor.focusNotebookCell(newCell, true); + } +}); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 1b03bb7b0ce..dd3de3f1789 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -220,3 +220,15 @@ export enum CellState { */ Editing } + +export enum CellFocusMode { + Container, + Editor +} + +export enum CursorAtBoundary { + None, + Top, + Bottom, + Both +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 15d67268960..8462eae7225 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -21,7 +21,7 @@ import { contrastBorder, editorBackground, focusBorder, foreground, textBlockQuo import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { EditorOptions, IEditorMemento, ICompositeCodeEditor, IEditorCloseEvent } from 'vs/workbench/common/editor'; -import { INotebookEditor, NotebookLayoutInfo, CellState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookEditor, NotebookLayoutInfo, CellState, CellFocusMode } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEditorInput, NotebookEditorModel } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService'; import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; @@ -186,6 +186,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { this.body, this.instantiationService.createInstance(NotebookCellListDelegate), renders, + this.contextKeyService, { setRowLineHeight: false, setRowHeight: false, @@ -212,7 +213,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { listInactiveFocusBackground: editorBackground, listInactiveFocusOutline: editorBackground, } - } + }, ); this.control = new NotebookCodeEditors(this.list, this.renderedEditors); @@ -595,18 +596,22 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { const index = this.notebookViewModel!.getViewCellIndex(cell); if (focusEditor) { + this.list?.setFocus([index]); + this.list?.setSelection([index]); + this.list?.focusView(); + cell.state = CellState.PreviewContent; + cell.focusMode = CellFocusMode.Editor; } else { let itemDOM = this.list?.domElementAtIndex(index); if (document.activeElement && itemDOM && itemDOM.contains(document.activeElement)) { (document.activeElement as HTMLElement).blur(); } - cell.state = CellState.Read; + this.list?.setFocus([index]); + this.list?.setSelection([index]); + this.list?.focusView(); } - - this.list?.setFocus([index]); - this.list?.focusView(); } //#endregion diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index bd0193c2a79..9b0f56d91c5 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -15,11 +15,12 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { isMacintosh } from 'vs/base/common/platform'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel'; -import { EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { EDITOR_TOP_PADDING, NOTEBOOK_EDITOR_CURSOR_BOUNDARY } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Range } from 'vs/editor/common/core/range'; -import { CellRevealType, CellRevealPosition } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellRevealType, CellRevealPosition, CursorAtBoundary } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; + export class NotebookCellList extends WorkbenchList implements IDisposable { get onWillScroll(): Event { return this.view.onWillScroll; } @@ -34,8 +35,8 @@ export class NotebookCellList extends WorkbenchList implements ID container: HTMLElement, delegate: IListVirtualDelegate, renderers: IListRenderer[], + contextKeyService: IContextKeyService, options: IWorkbenchListOptions, - @IContextKeyService contextKeyService: IContextKeyService, @IListService listService: IListService, @IThemeService themeService: IThemeService, @IConfigurationService configurationService: IConfigurationService, @@ -53,6 +54,48 @@ export class NotebookCellList extends WorkbenchList implements ID }); this._previousSelectedElements = e.elements; })); + + const notebookEditorCursorAtBoundaryContext = NOTEBOOK_EDITOR_CURSOR_BOUNDARY.bindTo(contextKeyService); + notebookEditorCursorAtBoundaryContext.set('none'); + + let cursorSelectionLisener: IDisposable | null = null; + + const recomputeContext = (element: CellViewModel) => { + switch (element.cursorAtBoundary()) { + case CursorAtBoundary.Both: + notebookEditorCursorAtBoundaryContext.set('both'); + break; + case CursorAtBoundary.Top: + notebookEditorCursorAtBoundaryContext.set('top'); + break; + case CursorAtBoundary.Bottom: + notebookEditorCursorAtBoundaryContext.set('bottom'); + break; + default: + notebookEditorCursorAtBoundaryContext.set('none'); + break; + } + return; + }; + + // Cursor Boundary context + this._localDisposableStore.add(this.onDidChangeFocus((e) => { + cursorSelectionLisener?.dispose(); + if (e.elements.length) { + // we only validate the first focused element + const focusedElement = e.elements[0]; + + cursorSelectionLisener = focusedElement.onDidChangeCursorSelection(() => { + recomputeContext(focusedElement); + }); + recomputeContext(focusedElement); + return; + } + + // reset context + notebookEditorCursorAtBoundaryContext.set('none'); + })); + } domElementAtIndex(index: number): HTMLElement | null { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts index 090586a0927..8818be4af67 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts @@ -9,7 +9,7 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel'; import { getResizesObserver } from 'vs/workbench/contrib/notebook/browser/view/renderers/sizeObserver'; import { CELL_MARGIN, IOutput, EDITOR_TOP_PADDING, EDITOR_BOTTOM_PADDING, ITransformedDisplayOutputDto, IRenderOutput, CellOutputKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { CellRenderTemplate, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellRenderTemplate, INotebookEditor, CellFocusMode } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { raceCancellation } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; @@ -86,6 +86,12 @@ export class CodeCell extends Disposable { } }); + this._register(viewCell.onDidChangeFocusMode(() => { + if (viewCell.focusMode === CellFocusMode.Editor) { + templateData.editor?.focus(); + } + })); + let cellWidthResizeObserver = getResizesObserver(templateData.cellContainer, { width: width, height: totalHeight diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts index 993eee2a545..4408a1b4fb0 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts @@ -10,7 +10,7 @@ import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { getResizesObserver } from 'vs/workbench/contrib/notebook/browser/view/renderers/sizeObserver'; import { CELL_MARGIN, EDITOR_TOP_PADDING, EDITOR_BOTTOM_PADDING } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { INotebookEditor, CellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookEditor, CellRenderTemplate, CellFocusMode } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { raceCancellation } from 'vs/base/common/async'; @@ -194,6 +194,12 @@ export class StatefullMarkdownCell extends Disposable { viewUpdate(); })); + this._register(viewCell.onDidChangeFocusMode(() => { + if (viewCell.focusMode === CellFocusMode.Editor) { + this.editor?.focus(); + } + })); + viewUpdate(); } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel.ts index 6f7e90d9a7d..64b8be8acab 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import * as UUID from 'vs/base/common/uuid'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Range } from 'vs/editor/common/core/range'; @@ -16,7 +16,7 @@ import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer' import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { MarkdownRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/mdRenderer'; import { CellKind, EDITOR_BOTTOM_PADDING, EDITOR_TOP_PADDING, ICell, IOutput, NotebookCellOutputsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { CellFindMatch, CellState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellFindMatch, CellState, CursorAtBoundary, CellFocusMode } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; export class CellViewModel extends Disposable { @@ -26,6 +26,8 @@ export class CellViewModel extends Disposable { readonly onDidDispose = this._onDidDispose.event; protected readonly _onDidChangeEditingState = new Emitter(); readonly onDidChangeEditingState = this._onDidChangeEditingState.event; + protected readonly _onDidChangeFocusMode = new Emitter(); + readonly onDidChangeFocusMode = this._onDidChangeFocusMode.event; protected readonly _onDidChangeOutputs = new Emitter(); readonly onDidChangeOutputs = this._onDidChangeOutputs.event; private _outputCollection: number[] = []; @@ -74,6 +76,17 @@ export class CellViewModel extends Disposable { this._onDidChangeEditingState.fire(); } + private _focusMode: CellFocusMode = CellFocusMode.Container; + + get focusMode() { + return this._focusMode; + } + + set focusMode(newMode: CellFocusMode) { + this._focusMode = newMode; + this._onDidChangeFocusMode.fire(); + } + private _selfSizeMonitoring: boolean = false; set selfSizeMonitoring(newVal: boolean) { @@ -106,6 +119,10 @@ export class CellViewModel extends Disposable { private _editorViewStates: editorCommon.ICodeEditorViewState | null; private _lastDecorationId: number = 0; private _resolvedDecorations = new Map(); + private readonly _onDidChangeCursorSelection: Emitter = this._register(new Emitter()); + public readonly onDidChangeCursorSelection: Event = this._onDidChangeCursorSelection.event; + + private _cursorChangeListener: IDisposable | null = null; readonly id: string = UUID.generateUuid(); @@ -300,6 +317,8 @@ export class CellViewModel extends Disposable { } }); + this._cursorChangeListener = this._textEditor.onDidChangeCursorSelection(() => this._onDidChangeCursorSelection.fire()); + this._onDidChangeCursorSelection.fire(); this._onDidChangeEditorAttachState.fire(true); } @@ -315,6 +334,7 @@ export class CellViewModel extends Disposable { } }); this._textEditor = undefined; + this._cursorChangeListener?.dispose(); this._onDidChangeEditorAttachState.fire(false); } @@ -373,11 +393,46 @@ export class CellViewModel extends Disposable { } onDeselect() { - if (this.state === CellState.PreviewContent) { + if (this.cellKind === CellKind.Code) { + this.state = CellState.Read; + } else if (this.state === CellState.PreviewContent) { this.state = CellState.Read; } } + cursorAtBoundary(): CursorAtBoundary { + if (!this._textEditor) { + return CursorAtBoundary.None; + } + + // only validate primary cursor + const selection = this._textEditor.getSelection(); + + // only validate empty cursor + if (!selection || !selection.isEmpty()) { + return CursorAtBoundary.None; + } + + // we don't allow attaching text editor without a model + const lineCnt = this._textEditor.getModel()!.getLineCount(); + + if (selection.startLineNumber === lineCnt) { + // bottom + + if (selection.startLineNumber === 1) { + return CursorAtBoundary.Both; + } else { + return CursorAtBoundary.Bottom; + } + } + + if (selection.startLineNumber === 1) { + return CursorAtBoundary.Top; + } + + return CursorAtBoundary.None; + } + getMarkdownRenderer() { if (!this._mdRenderer) { this._mdRenderer = this._instaService.createInstance(MarkdownRenderer); diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index a3cd4890ff3..59d67dd6000 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -12,6 +12,7 @@ import { URI } from 'vs/base/common/uri'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { PieceTreeTextBufferFactory } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; export enum CellKind { Markdown = 1, @@ -329,3 +330,5 @@ export function diff(before: T[], after: T[], contains: (a: T) => boolean): I export interface ICellEditorViewState { selections: editorCommon.ICursorState[]; } + +export const NOTEBOOK_EDITOR_CURSOR_BOUNDARY = new RawContextKey<'none' | 'top' | 'bottom' | 'both'>('notebookEditorCursorAtBoundary', 'none'); -- GitLab