diff --git a/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts b/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts index 6469a249ffb70..3d4e4ae30e838 100644 --- a/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts +++ b/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts @@ -26,6 +26,7 @@ import { NotebookEditorWidgetService } from '../service/notebook-editor-widget-s 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'; +import { NotebookEditorWidget } from '../notebook-editor-widget'; export namespace NotebookCommands { export const ADD_NEW_CELL_COMMAND = Command.toDefaultLocalizedCommand({ @@ -87,6 +88,11 @@ export namespace NotebookCommands { id: 'notebook.find', category: 'Notebook', }); + + export const CENTER_ACTIVE_CELL = Command.toDefaultLocalizedCommand({ + id: 'notebook.centerActiveCell', + category: 'Notebook', + }); } export enum CellChangeDirection { @@ -253,6 +259,13 @@ export class NotebookActionsContribution implements CommandContribution, MenuCon } }); + commands.registerCommand(NotebookCommands.CENTER_ACTIVE_CELL, { + execute: (editor?: NotebookEditorWidget) => { + const model = editor ? editor.model : this.notebookEditorWidgetService.focusedEditor?.model; + model?.selectedCell?.requestCenterEditor(); + } + }); + } protected editableCommandHandler(execute: (notebookModel: NotebookModel) => void): CommandHandler { @@ -345,6 +358,11 @@ export class NotebookActionsContribution implements CommandContribution, MenuCon keybinding: 'ctrlcmd+f', when: `${NOTEBOOK_EDITOR_FOCUSED}` }, + { + command: NotebookCommands.CENTER_ACTIVE_CELL.id, + keybinding: 'ctrlcmd+l', + when: `${NOTEBOOK_EDITOR_FOCUSED}` + } ); } diff --git a/packages/notebook/src/browser/contributions/notebook-status-bar-contribution.ts b/packages/notebook/src/browser/contributions/notebook-status-bar-contribution.ts new file mode 100644 index 0000000000000..05ded847b5bb3 --- /dev/null +++ b/packages/notebook/src/browser/contributions/notebook-status-bar-contribution.ts @@ -0,0 +1,77 @@ +// ***************************************************************************** +// Copyright (C) 2024 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { inject, injectable } from '@theia/core/shared/inversify'; +import { FrontendApplicationContribution, StatusBar, StatusBarAlignment } from '@theia/core/lib/browser'; +import { Disposable } from '@theia/core/lib/common'; +import { NotebookEditorWidgetService } from '../service/notebook-editor-widget-service'; +import { NotebookEditorWidget } from '../notebook-editor-widget'; +import { nls } from '@theia/core'; +import { NotebookCommands } from './notebook-actions-contribution'; + +export const NOTEBOOK_CELL_SELECTION_STATUS_BAR_ID = 'notebook-cell-selection-position'; + +@injectable() +export class NotebookStatusBarContribution implements FrontendApplicationContribution { + + @inject(StatusBar) protected readonly statusBar: StatusBar; + @inject(NotebookEditorWidgetService) protected readonly editorWidgetService: NotebookEditorWidgetService; + + protected currentCellSelectionListener: Disposable | undefined; + protected lastFocusedEditor: NotebookEditorWidget | undefined; + + onStart(): void { + this.editorWidgetService.onDidChangeFocusedEditor(editor => { + this.currentCellSelectionListener?.dispose(); + this.currentCellSelectionListener = editor?.model?.onDidChangeSelectedCell(() => + this.updateStatusbar(editor) + ); + editor?.onDidDispose(() => { + this.lastFocusedEditor = undefined; + this.updateStatusbar(); + }); + this.updateStatusbar(editor); + this.lastFocusedEditor = editor; + }); + if (this.editorWidgetService.focusedEditor) { + this.updateStatusbar(); + } + } + + protected async updateStatusbar(editor?: NotebookEditorWidget): Promise { + if ((!editor && !this.lastFocusedEditor?.isVisible) || editor?.model?.cells.length === 0) { + this.statusBar.removeElement(NOTEBOOK_CELL_SELECTION_STATUS_BAR_ID); + return; + } + + await editor?.ready; + if (!editor?.model) { + return; + } + + const selectedCellIndex = editor.model.selectedCell ? editor.model.cells.indexOf(editor.model.selectedCell) + 1 : ''; + + this.statusBar.setElement(NOTEBOOK_CELL_SELECTION_STATUS_BAR_ID, { + text: nls.localizeByDefault('Cell {0} of {1}', selectedCellIndex, editor.model.cells.length), + alignment: StatusBarAlignment.RIGHT, + priority: 100, + command: NotebookCommands.CENTER_ACTIVE_CELL.id, + arguments: [editor] + }); + + } + +} diff --git a/packages/notebook/src/browser/notebook-editor-widget.tsx b/packages/notebook/src/browser/notebook-editor-widget.tsx index a88ed0a77b692..216568353aca0 100644 --- a/packages/notebook/src/browser/notebook-editor-widget.tsx +++ b/packages/notebook/src/browser/notebook-editor-widget.tsx @@ -204,6 +204,7 @@ export class NotebookEditorWidget extends ReactWidget implements Navigatable, Sa } // Ensure that the model is loaded before adding the editor this.notebookEditorService.addNotebookEditor(this); + this._model.selectedCell = this._model.cells[0]; this.update(); this.notebookContextManager.init(this); return this._model; diff --git a/packages/notebook/src/browser/notebook-frontend-module.ts b/packages/notebook/src/browser/notebook-frontend-module.ts index b39d914333a6e..16da00f9c6676 100644 --- a/packages/notebook/src/browser/notebook-frontend-module.ts +++ b/packages/notebook/src/browser/notebook-frontend-module.ts @@ -47,6 +47,7 @@ import { NotebookClipboardService } from './service/notebook-clipboard-service'; import { bindNotebookPreferences } from './contributions/notebook-preferences'; import { NotebookOptionsService } from './service/notebook-options'; import { NotebookUndoRedoHandler } from './contributions/notebook-undo-redo-handler'; +import { NotebookStatusBarContribution } from './contributions/notebook-status-bar-contribution'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(NotebookColorContribution).toSelf().inSingletonScope(); @@ -112,4 +113,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(NotebookUndoRedoHandler).toSelf().inSingletonScope(); bind(UndoRedoHandler).toService(NotebookUndoRedoHandler); + + bind(NotebookStatusBarContribution).toSelf().inSingletonScope(); + bind(FrontendApplicationContribution).toService(NotebookStatusBarContribution); }); 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 c6ed48582fc31..f9ce7ca5d11b6 100644 --- a/packages/notebook/src/browser/view-model/notebook-cell-model.ts +++ b/packages/notebook/src/browser/view-model/notebook-cell-model.ts @@ -126,6 +126,9 @@ export class NotebookCellModel implements NotebookCell, Disposable { protected readonly onDidSelectFindMatchEmitter = new Emitter(); readonly onDidSelectFindMatch: Event = this.onDidSelectFindMatchEmitter.event; + protected onDidRequestCenterEditorEmitter = new Emitter(); + readonly onDidRequestCenterEditor = this.onDidRequestCenterEditorEmitter.event; + @inject(NotebookCellModelProps) protected readonly props: NotebookCellModelProps; @@ -299,6 +302,10 @@ export class NotebookCellModel implements NotebookCell, Disposable { this.onWillBlurCellEditorEmitter.fire(); } + requestCenterEditor(): void { + this.onDidRequestCenterEditorEmitter.fire(); + } + spliceNotebookCellOutputs(splice: NotebookCellOutputsSplice): void { if (splice.deleteCount > 0 && splice.newOutputs.length > 0) { const commonLen = Math.min(splice.deleteCount, splice.newOutputs.length); diff --git a/packages/notebook/src/browser/view/notebook-cell-editor.tsx b/packages/notebook/src/browser/view/notebook-cell-editor.tsx index db72b0765c267..a0832cc2a2b82 100644 --- a/packages/notebook/src/browser/view/notebook-cell-editor.tsx +++ b/packages/notebook/src/browser/view/notebook-cell-editor.tsx @@ -124,20 +124,7 @@ export class CellEditor extends React.Component { animationFrame().then(() => this.setMatches()); })); - this.toDispose.push(this.props.cell.onDidSelectFindMatch(match => { - const editorDomNode = this.editor?.getControl().getDomNode(); - if (editorDomNode) { - editorDomNode.scrollIntoView({ - behavior: 'instant', - block: 'center' - }); - } else { - this.container?.scrollIntoView({ - behavior: 'instant', - block: 'center' - }); - } - })); + this.toDispose.push(this.props.cell.onDidSelectFindMatch(match => this.centerEditorInView())); this.toDispose.push(this.props.notebookModel.onDidChangeSelectedCell(e => { if (e.cell !== this.props.cell && this.editor?.getControl().hasTextFocus()) { @@ -155,6 +142,10 @@ export class CellEditor extends React.Component { }); this.toDispose.push(disposable); } + + this.toDispose.push(this.props.cell.onDidRequestCenterEditor(() => { + this.centerEditorInView(); + })); } override componentWillUnmount(): void { @@ -166,6 +157,21 @@ export class CellEditor extends React.Component { this.toDispose = new DisposableCollection(); } + protected centerEditorInView(): void { + const editorDomNode = this.editor?.getControl().getDomNode(); + if (editorDomNode) { + editorDomNode.scrollIntoView({ + behavior: 'instant', + block: 'center' + }); + } else { + this.container?.scrollIntoView({ + behavior: 'instant', + block: 'center' + }); + } + } + protected async initEditor(): Promise { const { cell, notebookModel, monacoServices } = this.props; if (this.container) {