diff --git a/packages/core/src/browser/keybinding.ts b/packages/core/src/browser/keybinding.ts index a550ce746be3d..a88eba3057be2 100644 --- a/packages/core/src/browser/keybinding.ts +++ b/packages/core/src/browser/keybinding.ts @@ -497,6 +497,9 @@ export class KeybindingRegistry { isEnabledInScope(binding: common.Keybinding, target: HTMLElement | undefined): boolean { const context = binding.context && this.contexts[binding.context]; + if (binding.command && !this.commandRegistry.isEnabled(binding.command, binding.args)) { + return false; + } if (context && !context.isEnabled(binding)) { return false; } diff --git a/packages/notebook/package.json b/packages/notebook/package.json index afb6eaaa116bd..d50da607b4c9a 100644 --- a/packages/notebook/package.json +++ b/packages/notebook/package.json @@ -7,6 +7,7 @@ "@theia/editor": "1.47.0", "@theia/filesystem": "1.47.0", "@theia/monaco": "1.47.0", + "@theia/monaco-editor-core": "1.83.101", "react-perfect-scrollbar": "^1.5.8", "tslib": "^2.6.2" }, diff --git a/packages/notebook/src/browser/contributions/cell-operations.ts b/packages/notebook/src/browser/contributions/cell-operations.ts new file mode 100644 index 0000000000000..c77c0665f4d00 --- /dev/null +++ b/packages/notebook/src/browser/contributions/cell-operations.ts @@ -0,0 +1,38 @@ +// ***************************************************************************** +// Copyright (C) 2023 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 { CellEditType, CellKind } from '../../common'; +import { NotebookCellModel } from '../view-model/notebook-cell-model'; +import { NotebookModel } from '../view-model/notebook-model'; + +/** + * a collection of different reusable notbook cell operations + */ + +export function changeCellType(notebookModel: NotebookModel, cell: NotebookCellModel, type: CellKind): void { + if (cell.cellKind === type) { + return; + } + notebookModel.applyEdits([{ + editType: CellEditType.Replace, + index: notebookModel.cells.indexOf(cell), + count: 1, + cells: [{ + ...cell.getData(), + cellKind: type + }] + }], true); +} diff --git a/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts b/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts index 4795d8da7b582..50f3ebf73a9ef 100644 --- a/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts +++ b/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts @@ -16,13 +16,15 @@ import { Command, CommandContribution, CommandHandler, CommandRegistry, CompoundMenuNodeRole, MenuContribution, MenuModelRegistry, nls } from '@theia/core'; import { inject, injectable } from '@theia/core/shared/inversify'; -import { ApplicationShell, codicon, CommonCommands } from '@theia/core/lib/browser'; +import { ApplicationShell, codicon, CommonCommands, KeybindingContribution, KeybindingRegistry } from '@theia/core/lib/browser'; import { NotebookModel } from '../view-model/notebook-model'; import { NotebookService } from '../service/notebook-service'; import { CellEditType, CellKind, NotebookCommand } from '../../common'; import { NotebookKernelQuickPickService } from '../service/notebook-kernel-quick-pick-service'; 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 } from './notebook-context-keys'; export namespace NotebookCommands { export const ADD_NEW_CELL_COMMAND = Command.toDefaultLocalizedCommand({ @@ -59,10 +61,20 @@ export namespace NotebookCommands { category: 'Notebook', iconClass: codicon('clear-all') }); + + export const CHANGE_SELECTED_CELL = Command.toDefaultLocalizedCommand({ + id: 'notebook.change-selected-cell', + category: 'Notebook', + }); +} + +export enum CellChangeDirection { + Up = 'up', + Down = 'down' } @injectable() -export class NotebookActionsContribution implements CommandContribution, MenuContribution { +export class NotebookActionsContribution implements CommandContribution, MenuContribution, KeybindingContribution { @inject(NotebookService) protected notebookService: NotebookService; @@ -76,10 +88,22 @@ export class NotebookActionsContribution implements CommandContribution, MenuCon @inject(ApplicationShell) protected shell: ApplicationShell; + @inject(NotebookEditorWidgetService) + protected notebookEditorWidgetService: NotebookEditorWidgetService; + registerCommands(commands: CommandRegistry): void { commands.registerCommand(NotebookCommands.ADD_NEW_CELL_COMMAND, { - execute: (notebookModel: NotebookModel, cellKind: CellKind, index?: number) => { - const insertIndex = index ?? (notebookModel.selectedCell ? notebookModel.cells.indexOf(notebookModel.selectedCell) : 0); + execute: (notebookModel: NotebookModel, cellKind: CellKind = CellKind.Markup, index?: number | 'above' | 'below') => { + notebookModel = notebookModel ?? this.notebookEditorWidgetService.focusedEditor?.model; + + let insertIndex: number = 0; + if (index && index >= 0) { + insertIndex = index as number; + } else if (notebookModel.selectedCell && typeof index === 'string') { + // if index is -1 insert below otherwise at the index of the selected cell which is above the selected. + insertIndex = notebookModel.cells.indexOf(notebookModel.selectedCell) + (index === 'below' ? 1 : 0); + } + let firstCodeCell; if (cellKind === CellKind.Code) { firstCodeCell = notebookModel.cells.find(cell => cell.cellKind === CellKind.Code); @@ -101,11 +125,11 @@ export class NotebookActionsContribution implements CommandContribution, MenuCon }); commands.registerCommand(NotebookCommands.ADD_NEW_MARKDOWN_CELL_COMMAND, this.editableCommandHandler( - notebookModel => commands.executeCommand(NotebookCommands.ADD_NEW_CELL_COMMAND.id, notebookModel, CellKind.Markup) + notebookModel => commands.executeCommand(NotebookCommands.ADD_NEW_CELL_COMMAND.id, notebookModel, CellKind.Markup, 'below') )); commands.registerCommand(NotebookCommands.ADD_NEW_CODE_CELL_COMMAND, this.editableCommandHandler( - notebookModel => commands.executeCommand(NotebookCommands.ADD_NEW_CELL_COMMAND.id, notebookModel, CellKind.Code) + notebookModel => commands.executeCommand(NotebookCommands.ADD_NEW_CELL_COMMAND.id, notebookModel, CellKind.Code, 'below') )); commands.registerCommand(NotebookCommands.SELECT_KERNEL_COMMAND, this.editableCommandHandler( @@ -120,6 +144,24 @@ export class NotebookActionsContribution implements CommandContribution, MenuCon notebookModel => notebookModel.cells.forEach(cell => cell.spliceNotebookCellOutputs({ start: 0, deleteCount: cell.outputs.length, newOutputs: [] })) )); + commands.registerCommand(NotebookCommands.CHANGE_SELECTED_CELL, + { + execute: (change: number | CellChangeDirection) => { + const model = this.notebookEditorWidgetService.focusedEditor?.model; + if (model && typeof change === 'number') { + model.setSelectedCell(model.cells[change]); + } else if (model && model.selectedCell) { + const currentIndex = model.cells.indexOf(model.selectedCell); + if (change === CellChangeDirection.Up && currentIndex > 0) { + model.setSelectedCell(model.cells[currentIndex - 1]); + } else if (change === CellChangeDirection.Down && currentIndex < model.cells.length - 1) { + model.setSelectedCell(model.cells[currentIndex + 1]); + } + } + } + } + ); + commands.registerHandler(CommonCommands.UNDO.id, { isEnabled: () => { const widget = this.shell.activeWidget; @@ -134,6 +176,7 @@ export class NotebookActionsContribution implements CommandContribution, MenuCon }, execute: () => (this.shell.activeWidget as NotebookEditorWidget).redo() }); + } protected editableCommandHandler(execute: (notebookModel: NotebookModel) => void): CommandHandler { @@ -179,6 +222,23 @@ export class NotebookActionsContribution implements CommandContribution, MenuCon // other items } + registerKeybindings(keybindings: KeybindingRegistry): void { + keybindings.registerKeybindings( + { + command: NotebookCommands.CHANGE_SELECTED_CELL.id, + keybinding: 'up', + args: CellChangeDirection.Up, + when: `!editorTextFocus && ${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}` + }, + ); + } + } export namespace NotebookMenus { diff --git a/packages/notebook/src/browser/contributions/notebook-cell-actions-contribution.ts b/packages/notebook/src/browser/contributions/notebook-cell-actions-contribution.ts index b3e1eade2be7d..73f7c049c966e 100644 --- a/packages/notebook/src/browser/contributions/notebook-cell-actions-contribution.ts +++ b/packages/notebook/src/browser/contributions/notebook-cell-actions-contribution.ts @@ -15,15 +15,22 @@ // ***************************************************************************** import { Command, CommandContribution, CommandHandler, CommandRegistry, CompoundMenuNodeRole, MenuContribution, MenuModelRegistry, nls } from '@theia/core'; -import { codicon } from '@theia/core/lib/browser'; +import { codicon, Key, KeybindingContribution, KeybindingRegistry, KeyCode, KeyModifier } from '@theia/core/lib/browser'; import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { NotebookModel } from '../view-model/notebook-model'; import { NotebookCellModel } from '../view-model/notebook-cell-model'; -import { NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_TYPE, NotebookContextKeys, NOTEBOOK_CELL_EXECUTING } from './notebook-context-keys'; +import { + NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_TYPE, + NotebookContextKeys, NOTEBOOK_CELL_EXECUTING, NOTEBOOK_EDITOR_FOCUSED, + NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_EDITABLE +} from './notebook-context-keys'; import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; import { NotebookExecutionService } from '../service/notebook-execution-service'; import { NotebookCellOutputModel } from '../view-model/notebook-cell-output-model'; -import { CellEditType } from '../../common'; +import { CellEditType, CellKind } from '../../common'; +import { NotebookEditorWidgetService } from '../service/notebook-editor-widget-service'; +import { NotebookCommands } from './notebook-actions-contribution'; +import { changeCellType } from './cell-operations'; export namespace NotebookCellCommands { /** Parameters: notebookModel: NotebookModel | undefined, cell: NotebookCellModel */ @@ -52,6 +59,10 @@ export namespace NotebookCellCommands { iconClass: codicon('play'), }); /** Parameters: notebookModel: NotebookModel, cell: NotebookCellModel */ + export const EXECUTE_SINGLE_CELL_AND_FOCUS_NEXT_COMMAND = Command.toDefaultLocalizedCommand({ + id: 'notebook.cell.execute-cell-and-focus-next', + }); + /** Parameters: notebookModel: NotebookModel, cell: NotebookCellModel */ export const STOP_CELL_EXECUTION_COMMAND = Command.toDefaultLocalizedCommand({ id: 'notebook.cell.stop-cell-execution', iconClass: codicon('stop'), @@ -66,10 +77,39 @@ export namespace NotebookCellCommands { id: 'notebook.cell.change-presentation', label: 'Change Presentation', }); + + export const INSERT_NEW_CELL_ABOVE_COMMAND = Command.toDefaultLocalizedCommand({ + id: 'notebook.cell.insertCodeCellAboveAndFocusContainer', + label: 'Insert Code Cell Above and Focus Container' + }); + + export const INSERT_NEW_CELL_BELOW_COMMAND = Command.toDefaultLocalizedCommand({ + id: 'notebook.cell.insertCodeCellBelowAndFocusContainer', + label: 'Insert Code Cell Below and Focus Container' + }); + + export const INSERT_MARKDOWN_CELL_ABOVE_COMMAND = Command.toLocalizedCommand({ + id: 'notebook.cell.insertMarkdownCellAbove', + label: 'Insert Markdown Cell Above' + }); + export const INSERT_MARKDOWN_CELL_BELOW_COMMAND = Command.toLocalizedCommand({ + id: 'notebook.cell.insertMarkdownCellBelow', + label: 'Insert Markdown Cell Below' + }); + + export const TO_CODE_CELL_COMMAND = Command.toLocalizedCommand({ + id: 'notebook.cell.to-code-cell', + label: 'Change Cell to Code' + }); + + export const TO_MARKDOWN_CELL_COMMAND = Command.toLocalizedCommand({ + id: 'notebook.cell.to-markdown-cell', + label: 'Change Cell to Mardown' + }); } @injectable() -export class NotebookCellActionContribution implements MenuContribution, CommandContribution { +export class NotebookCellActionContribution implements MenuContribution, CommandContribution, KeybindingContribution { @inject(ContextKeyService) protected contextKeyService: ContextKeyService; @@ -77,6 +117,9 @@ export class NotebookCellActionContribution implements MenuContribution, Command @inject(NotebookExecutionService) protected notebookExecutionService: NotebookExecutionService; + @inject(NotebookEditorWidgetService) + protected notebookEditorWidgetService: NotebookEditorWidgetService; + @postConstruct() protected init(): void { NotebookContextKeys.initNotebookContextKeys(this.contextKeyService); @@ -173,28 +216,65 @@ export class NotebookCellActionContribution implements MenuContribution, Command registerCommands(commands: CommandRegistry): void { commands.registerCommand(NotebookCellCommands.EDIT_COMMAND, this.editableCellCommandHandler((_, cell) => cell.requestEdit())); - commands.registerCommand(NotebookCellCommands.STOP_EDIT_COMMAND, { execute: (_, cell: NotebookCellModel) => cell.requestStopEdit() }); + commands.registerCommand(NotebookCellCommands.STOP_EDIT_COMMAND, { execute: (_, cell: NotebookCellModel) => (cell ?? this.getSelectedCell()).requestStopEdit() }); commands.registerCommand(NotebookCellCommands.DELETE_COMMAND, - this.editableCellCommandHandler((notebookModel, cell) => notebookModel.applyEdits([{ - editType: CellEditType.Replace, - index: notebookModel.cells.indexOf(cell), - count: 1, - cells: [] - }], true))); + this.editableCellCommandHandler((notebookModel, cell) => { + notebookModel.applyEdits([{ + editType: CellEditType.Replace, + index: notebookModel.cells.indexOf(cell), + count: 1, + cells: [] + }] + , true); + })); commands.registerCommand(NotebookCellCommands.SPLIT_CELL_COMMAND); commands.registerCommand(NotebookCellCommands.EXECUTE_SINGLE_CELL_COMMAND, this.editableCellCommandHandler( - (notebookModel, cell) => this.notebookExecutionService.executeNotebookCells(notebookModel, [cell]) - )); + (notebookModel, cell) => { + this.notebookExecutionService.executeNotebookCells(notebookModel, [cell]); + }) + ); + + commands.registerCommand(NotebookCellCommands.EXECUTE_SINGLE_CELL_AND_FOCUS_NEXT_COMMAND, this.editableCellCommandHandler( + (notebookModel, cell) => { + commands.executeCommand(NotebookCellCommands.EXECUTE_SINGLE_CELL_COMMAND.id, notebookModel, cell); + const index = notebookModel.cells.indexOf(cell); + if (index < notebookModel.cells.length - 1) { + notebookModel.setSelectedCell(notebookModel.cells[index + 1]); + } else { + commands.executeCommand(NotebookCellCommands.INSERT_NEW_CELL_BELOW_COMMAND.id, notebookModel, CellKind.Code, 'below'); + } + }) + ); + commands.registerCommand(NotebookCellCommands.STOP_CELL_EXECUTION_COMMAND, { - execute: (notebookModel: NotebookModel, cell: NotebookCellModel) => this.notebookExecutionService.cancelNotebookCells(notebookModel, [cell]) + execute: (notebookModel: NotebookModel, cell: NotebookCellModel) => { + notebookModel = notebookModel ?? this.notebookEditorWidgetService.focusedEditor?.model; + cell = cell ?? this.getSelectedCell(); + this.notebookExecutionService.cancelNotebookCells(notebookModel, [cell]); + } }); commands.registerCommand(NotebookCellCommands.CLEAR_OUTPUTS_COMMAND, this.editableCellCommandHandler( - (_, cell) => cell.spliceNotebookCellOutputs({ start: 0, deleteCount: cell.outputs.length, newOutputs: [] }) + (_, cell) => cell.spliceNotebookCellOutputs({ start: 0, deleteCount: (cell ?? this.getSelectedCell()).outputs.length, newOutputs: [] }) )); commands.registerCommand(NotebookCellCommands.CHANGE_OUTPUT_PRESENTATION_COMMAND, this.editableCellCommandHandler( (_, __, output) => output?.requestOutputPresentationUpdate() )); + + const insertCommand = (type: CellKind, index: number | 'above' | 'below'): CommandHandler => this.editableCellCommandHandler(() => + commands.executeCommand(NotebookCommands.ADD_NEW_CELL_COMMAND.id, undefined, type, index) + ); + commands.registerCommand(NotebookCellCommands.INSERT_NEW_CELL_ABOVE_COMMAND, insertCommand(CellKind.Code, 'above')); + commands.registerCommand(NotebookCellCommands.INSERT_NEW_CELL_BELOW_COMMAND, insertCommand(CellKind.Code, 'below')); + commands.registerCommand(NotebookCellCommands.INSERT_MARKDOWN_CELL_ABOVE_COMMAND, insertCommand(CellKind.Markup, 'above')); + commands.registerCommand(NotebookCellCommands.INSERT_MARKDOWN_CELL_BELOW_COMMAND, insertCommand(CellKind.Markup, 'below')); + + commands.registerCommand(NotebookCellCommands.TO_CODE_CELL_COMMAND, this.editableCellCommandHandler((notebookModel, cell) => { + changeCellType(notebookModel, cell, CellKind.Code); + })); + commands.registerCommand(NotebookCellCommands.TO_MARKDOWN_CELL_COMMAND, this.editableCellCommandHandler((notebookModel, cell) => { + changeCellType(notebookModel, cell, CellKind.Markup); + })); } protected editableCellCommandHandler(execute: (notebookModel: NotebookModel, cell: NotebookCellModel, output?: NotebookCellOutputModel) => void): CommandHandler { @@ -202,10 +282,61 @@ export class NotebookCellActionContribution implements MenuContribution, Command isEnabled: (notebookModel: NotebookModel) => !Boolean(notebookModel?.readOnly), isVisible: (notebookModel: NotebookModel) => !Boolean(notebookModel?.readOnly), execute: (notebookModel: NotebookModel, cell: NotebookCellModel, output?: NotebookCellOutputModel) => { + notebookModel = notebookModel ?? this.notebookEditorWidgetService.focusedEditor?.model; + cell = cell ?? this.getSelectedCell(); execute(notebookModel, cell, output); } }; } + + protected getSelectedCell(): NotebookCellModel | undefined { + return this.notebookEditorWidgetService.focusedEditor?.model?.selectedCell; + } + + registerKeybindings(keybindings: KeybindingRegistry): void { + keybindings.registerKeybindings( + { + command: NotebookCellCommands.EDIT_COMMAND.id, + keybinding: 'Enter', + when: `${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED} && ${NOTEBOOK_CELL_EDITABLE}`, + }, + { + command: NotebookCellCommands.STOP_EDIT_COMMAND.id, + keybinding: KeyCode.createKeyCode({ first: Key.ENTER, modifiers: [KeyModifier.Alt] }).toString(), + when: `editorTextFocus && ${NOTEBOOK_EDITOR_FOCUSED}`, + }, + { + command: NotebookCellCommands.EXECUTE_SINGLE_CELL_COMMAND.id, + keybinding: KeyCode.createKeyCode({ first: Key.ENTER, modifiers: [KeyModifier.CtrlCmd] }).toString(), + when: `${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED} && ${NOTEBOOK_CELL_TYPE} == 'code'`, + }, + { + command: NotebookCellCommands.EXECUTE_SINGLE_CELL_AND_FOCUS_NEXT_COMMAND.id, + keybinding: KeyCode.createKeyCode({ first: Key.ENTER, modifiers: [KeyModifier.Shift] }).toString(), + when: `${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED} && ${NOTEBOOK_CELL_TYPE} == 'code'`, + }, + { + command: NotebookCellCommands.CLEAR_OUTPUTS_COMMAND.id, + keybinding: KeyCode.createKeyCode({ first: Key.KEY_O, modifiers: [KeyModifier.Alt] }).toString(), + when: `${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED} && ${NOTEBOOK_CELL_TYPE} == 'code'`, + }, + { + command: NotebookCellCommands.CHANGE_OUTPUT_PRESENTATION_COMMAND.id, + keybinding: KeyCode.createKeyCode({ first: Key.KEY_P, modifiers: [KeyModifier.Alt] }).toString(), + when: `${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED} && ${NOTEBOOK_CELL_TYPE} == 'code'`, + }, + { + command: NotebookCellCommands.TO_CODE_CELL_COMMAND.id, + keybinding: 'Y', + when: `${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED} && ${NOTEBOOK_CELL_TYPE} == 'markdown'`, + }, + { + command: NotebookCellCommands.TO_MARKDOWN_CELL_COMMAND.id, + keybinding: 'M', + when: `${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED} && ${NOTEBOOK_CELL_TYPE} == 'code'`, + } + ); + } } export namespace NotebookCellActionContribution { @@ -218,3 +349,4 @@ export namespace NotebookCellActionContribution { export const ADDITIONAL_OUTPUT_SIDEBAR_MENU = [...OUTPUT_SIDEBAR_MENU, 'more']; } + diff --git a/packages/notebook/src/browser/notebook-editor-widget.tsx b/packages/notebook/src/browser/notebook-editor-widget.tsx index f78cd147fb8fa..ca894b04aae98 100644 --- a/packages/notebook/src/browser/notebook-editor-widget.tsx +++ b/packages/notebook/src/browser/notebook-editor-widget.tsx @@ -43,6 +43,8 @@ export function createNotebookEditorWidgetContainer(parent: interfaces.Container child.bind(NotebookContextManager).toSelf().inSingletonScope(); child.bind(NotebookMainToolbarRenderer).toSelf().inSingletonScope(); + child.bind(NotebookCodeCellRenderer).toSelf().inSingletonScope(); + child.bind(NotebookMarkdownCellRenderer).toSelf().inSingletonScope(); child.bind(NotebookEditorWidget).toSelf(); diff --git a/packages/notebook/src/browser/notebook-frontend-module.ts b/packages/notebook/src/browser/notebook-frontend-module.ts index 0a9704163672d..cb6814b219bd6 100644 --- a/packages/notebook/src/browser/notebook-frontend-module.ts +++ b/packages/notebook/src/browser/notebook-frontend-module.ts @@ -16,7 +16,7 @@ import '../../src/browser/style/index.css'; import { ContainerModule } from '@theia/core/shared/inversify'; -import { OpenHandler, WidgetFactory } from '@theia/core/lib/browser'; +import { KeybindingContribution, OpenHandler, WidgetFactory } from '@theia/core/lib/browser'; import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution'; import { NotebookOpenHandler } from './notebook-open-handler'; import { CommandContribution, MenuContribution, ResourceResolver, } from '@theia/core'; @@ -31,8 +31,6 @@ import { NotebookCellToolbarFactory } from './view/notebook-cell-toolbar-factory import { createNotebookModelContainer, NotebookModel, NotebookModelFactory, NotebookModelProps } from './view-model/notebook-model'; import { createNotebookCellModelContainer, NotebookCellModel, NotebookCellModelFactory, NotebookCellModelProps } from './view-model/notebook-cell-model'; import { createNotebookEditorWidgetContainer, NotebookEditorWidgetContainerFactory, NotebookEditorProps, NotebookEditorWidget } from './notebook-editor-widget'; -import { NotebookCodeCellRenderer } from './view/notebook-code-cell-view'; -import { NotebookMarkdownCellRenderer } from './view/notebook-markdown-cell-view'; import { NotebookActionsContribution } from './contributions/notebook-actions-contribution'; import { NotebookExecutionService } from './service/notebook-execution-service'; import { NotebookExecutionStateService } from './service/notebook-execution-state-service'; @@ -42,7 +40,6 @@ import { NotebookKernelHistoryService } from './service/notebook-kernel-history- import { NotebookEditorWidgetService } from './service/notebook-editor-widget-service'; import { NotebookRendererMessagingService } from './service/notebook-renderer-messaging-service'; import { NotebookColorContribution } from './contributions/notebook-color-contribution'; -import { NotebookCellContextManager } from './service/notebook-cell-context-manager'; export default new ContainerModule(bind => { bind(NotebookColorContribution).toSelf().inSingletonScope(); @@ -73,13 +70,12 @@ export default new ContainerModule(bind => { bind(NotebookCellActionContribution).toSelf().inSingletonScope(); bind(MenuContribution).toService(NotebookCellActionContribution); bind(CommandContribution).toService(NotebookCellActionContribution); + bind(KeybindingContribution).toService(NotebookCellActionContribution); bind(NotebookActionsContribution).toSelf().inSingletonScope(); bind(CommandContribution).toService(NotebookActionsContribution); bind(MenuContribution).toService(NotebookActionsContribution); - - bind(NotebookCodeCellRenderer).toSelf().inSingletonScope(); - bind(NotebookMarkdownCellRenderer).toSelf().inSingletonScope(); + bind(KeybindingContribution).toService(NotebookActionsContribution); bind(NotebookEditorWidgetContainerFactory).toFactory(ctx => (props: NotebookEditorProps) => createNotebookEditorWidgetContainer(ctx.container, props).get(NotebookEditorWidget) @@ -88,6 +84,6 @@ export default new ContainerModule(bind => { createNotebookModelContainer(ctx.container, props).get(NotebookModel) ); bind(NotebookCellModelFactory).toFactory(ctx => (props: NotebookCellModelProps) => - createNotebookCellModelContainer(ctx.container, props, NotebookCellContextManager).get(NotebookCellModel) + createNotebookCellModelContainer(ctx.container, props).get(NotebookCellModel) ); }); diff --git a/packages/notebook/src/browser/service/notebook-cell-context-manager.ts b/packages/notebook/src/browser/service/notebook-cell-context-manager.ts deleted file mode 100644 index d49a2bc861cd1..0000000000000 --- a/packages/notebook/src/browser/service/notebook-cell-context-manager.ts +++ /dev/null @@ -1,72 +0,0 @@ -// ***************************************************************************** -// Copyright (C) 2023 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 { ContextKeyChangeEvent, ContextKeyService, ScopedValueStore } from '@theia/core/lib/browser/context-key-service'; -import { NotebookCellModel } from '../view-model/notebook-cell-model'; -import { NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_TYPE } from '../contributions/notebook-context-keys'; -import { Disposable, DisposableCollection, Emitter } from '@theia/core'; -import { CellKind } from '../../common'; -import { NotebookExecutionStateService } from '../service/notebook-execution-state-service'; - -@injectable() -export class NotebookCellContextManager implements NotebookCellContextManager, Disposable { - @inject(ContextKeyService) protected contextKeyService: ContextKeyService; - - @inject(NotebookExecutionStateService) - protected readonly executionStateService: NotebookExecutionStateService; - - protected readonly toDispose = new DisposableCollection(); - - protected currentStore: ScopedValueStore; - protected currentContext: HTMLLIElement; - - protected readonly onDidChangeContextEmitter = new Emitter(); - readonly onDidChangeContext = this.onDidChangeContextEmitter.event; - - updateCellContext(cell: NotebookCellModel, newHtmlContext: HTMLLIElement): void { - if (newHtmlContext !== this.currentContext) { - this.toDispose.dispose(); - - this.currentContext = newHtmlContext; - this.currentStore = this.contextKeyService.createScoped(newHtmlContext); - - this.currentStore.setContext(NOTEBOOK_CELL_TYPE, cell.cellKind === CellKind.Code ? 'code' : 'markdown'); - - this.toDispose.push(this.contextKeyService.onDidChange(e => { - this.onDidChangeContextEmitter.fire(e); - })); - - this.toDispose.push(cell.onDidRequestCellEditChange(cellEdit => { - this.currentStore?.setContext(NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, cellEdit); - this.onDidChangeContextEmitter.fire({ affects: keys => keys.has(NOTEBOOK_CELL_MARKDOWN_EDIT_MODE) }); - })); - this.toDispose.push(this.executionStateService.onDidChangeExecution(e => { - if (e.affectsCell(cell.uri)) { - this.currentStore?.setContext(NOTEBOOK_CELL_EXECUTING, !!e.changed); - this.currentStore?.setContext(NOTEBOOK_CELL_EXECUTION_STATE, e.changed?.state ?? 'idle'); - this.onDidChangeContextEmitter.fire({ affects: keys => keys.has(NOTEBOOK_CELL_EXECUTING) || keys.has(NOTEBOOK_CELL_EXECUTION_STATE) }); - } - })); - this.onDidChangeContextEmitter.fire({ affects: keys => true }); - } - } - - dispose(): void { - this.toDispose.dispose(); - this.onDidChangeContextEmitter.dispose(); - } -} diff --git a/packages/notebook/src/browser/service/notebook-context-manager.ts b/packages/notebook/src/browser/service/notebook-context-manager.ts index d5b33c3bcac9a..53fa382868f67 100644 --- a/packages/notebook/src/browser/service/notebook-context-manager.ts +++ b/packages/notebook/src/browser/service/notebook-context-manager.ts @@ -15,11 +15,20 @@ // ***************************************************************************** import { inject, injectable } from '@theia/core/shared/inversify'; -import { ContextKeyChangeEvent, ContextKeyService } from '@theia/core/lib/browser/context-key-service'; +import { ContextKeyChangeEvent, ContextKeyService, ScopedValueStore } from '@theia/core/lib/browser/context-key-service'; import { DisposableCollection, Emitter } from '@theia/core'; import { NotebookKernelService } from './notebook-kernel-service'; -import { NOTEBOOK_KERNEL, NOTEBOOK_KERNEL_SELECTED, NOTEBOOK_VIEW_TYPE } from '../contributions/notebook-context-keys'; +import { + NOTEBOOK_CELL_EDITABLE, + NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE, + NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, + NOTEBOOK_CELL_TYPE, NOTEBOOK_KERNEL, NOTEBOOK_KERNEL_SELECTED, + NOTEBOOK_VIEW_TYPE +} from '../contributions/notebook-context-keys'; import { NotebookEditorWidget } from '../notebook-editor-widget'; +import { NotebookCellModel } from '../view-model/notebook-cell-model'; +import { CellKind } from '../../common'; +import { NotebookExecutionStateService } from './notebook-execution-state-service'; @injectable() export class NotebookContextManager { @@ -28,31 +37,82 @@ export class NotebookContextManager { @inject(NotebookKernelService) protected readonly notebookKernelService: NotebookKernelService; + @inject(NotebookExecutionStateService) + protected readonly executionStateService: NotebookExecutionStateService; + protected readonly toDispose = new DisposableCollection(); protected readonly onDidChangeContextEmitter = new Emitter(); readonly onDidChangeContext = this.onDidChangeContextEmitter.event; + protected _context?: HTMLElement; + + scopedStore: ScopedValueStore; + + get context(): HTMLElement | undefined { + return this._context; + } + init(widget: NotebookEditorWidget): void { - const scopedStore = this.contextKeyService.createScoped(widget.node); + this._context = widget.node; + this.scopedStore = this.contextKeyService.createScoped(widget.node); this.toDispose.dispose(); - scopedStore.setContext(NOTEBOOK_VIEW_TYPE, widget?.notebookType); + this.scopedStore.setContext(NOTEBOOK_VIEW_TYPE, widget?.notebookType); + // Kernel related keys const kernel = widget?.model ? this.notebookKernelService.getSelectedNotebookKernel(widget.model) : undefined; - scopedStore.setContext(NOTEBOOK_KERNEL_SELECTED, !!kernel); - scopedStore.setContext(NOTEBOOK_KERNEL, kernel?.id); + this.scopedStore.setContext(NOTEBOOK_KERNEL_SELECTED, !!kernel); + this.scopedStore.setContext(NOTEBOOK_KERNEL, kernel?.id); this.toDispose.push(this.notebookKernelService.onDidChangeSelectedKernel(e => { if (e.notebook.toString() === widget?.getResourceUri()?.toString()) { - scopedStore.setContext(NOTEBOOK_KERNEL_SELECTED, !!e.newKernel); - scopedStore.setContext(NOTEBOOK_KERNEL, e.newKernel); + this.scopedStore.setContext(NOTEBOOK_KERNEL_SELECTED, !!e.newKernel); + this.scopedStore.setContext(NOTEBOOK_KERNEL, e.newKernel); this.onDidChangeContextEmitter.fire(this.createContextKeyChangedEvent([NOTEBOOK_KERNEL_SELECTED, NOTEBOOK_KERNEL])); } })); + + // Cell Selection realted keys + this.scopedStore.setContext(NOTEBOOK_CELL_FOCUSED, !!widget.model?.selectedCell); + widget.model?.onDidChangeSelectedCell(e => { + this.scopedStore.setContext(NOTEBOOK_CELL_FOCUSED, !!e); + this.onDidChangeContextEmitter.fire(this.createContextKeyChangedEvent([NOTEBOOK_CELL_FOCUSED])); + }); + + widget.model?.onDidChangeSelectedCell(e => this.selectedCellChanged(e)); + this.onDidChangeContextEmitter.fire(this.createContextKeyChangedEvent([NOTEBOOK_VIEW_TYPE, NOTEBOOK_KERNEL_SELECTED, NOTEBOOK_KERNEL])); } + protected cellDisposables = new DisposableCollection(); + + selectedCellChanged(cell: NotebookCellModel | undefined): void { + this.cellDisposables.dispose(); + + this.scopedStore.setContext(NOTEBOOK_CELL_TYPE, cell ? cell.cellKind === CellKind.Code ? 'code' : 'markdown' : undefined); + + if (cell) { + this.scopedStore.setContext(NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, cell.editing); + this.scopedStore.setContext(NOTEBOOK_CELL_EDITABLE, cell.cellKind === CellKind.Markup && !cell.editing); + this.cellDisposables.push(cell.onDidRequestCellEditChange(cellEdit => { + this.scopedStore.setContext(NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, cellEdit); + this.scopedStore.setContext(NOTEBOOK_CELL_EDITABLE, cell.cellKind === CellKind.Markup && !cellEdit); + this.onDidChangeContextEmitter.fire(this.createContextKeyChangedEvent([NOTEBOOK_CELL_MARKDOWN_EDIT_MODE])); + })); + this.cellDisposables.push(this.executionStateService.onDidChangeExecution(e => { + if (cell && e.affectsCell(cell.uri)) { + this.scopedStore.setContext(NOTEBOOK_CELL_EXECUTING, !!e.changed); + this.scopedStore.setContext(NOTEBOOK_CELL_EXECUTION_STATE, e.changed?.state ?? 'idle'); + this.onDidChangeContextEmitter.fire(this.createContextKeyChangedEvent([NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE])); + } + })); + } + + this.onDidChangeContextEmitter.fire(this.createContextKeyChangedEvent([NOTEBOOK_CELL_TYPE])); + + } + createContextKeyChangedEvent(affectedKeys: string[]): ContextKeyChangeEvent { return { affects: keys => affectedKeys.some(key => keys.has(key)) }; } diff --git a/packages/notebook/src/browser/service/notebook-editor-widget-service.ts b/packages/notebook/src/browser/service/notebook-editor-widget-service.ts index e0dd0419977bf..8038b6cdf90c2 100644 --- a/packages/notebook/src/browser/service/notebook-editor-widget-service.ts +++ b/packages/notebook/src/browser/service/notebook-editor-widget-service.ts @@ -23,6 +23,8 @@ import { Emitter } from '@theia/core'; import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { ApplicationShell } from '@theia/core/lib/browser'; import { NotebookEditorWidget } from '../notebook-editor-widget'; +import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; +import { NOTEBOOK_EDITOR_FOCUSED } from '../contributions/notebook-context-keys'; @injectable() export class NotebookEditorWidgetService { @@ -30,6 +32,9 @@ export class NotebookEditorWidgetService { @inject(ApplicationShell) protected applicationShell: ApplicationShell; + @inject(ContextKeyService) + protected contextKeyService: ContextKeyService; + private readonly notebookEditors = new Map(); private readonly onNotebookEditorAddEmitter = new Emitter(); @@ -48,11 +53,13 @@ export class NotebookEditorWidgetService { if (event.newValue instanceof NotebookEditorWidget) { if (event.newValue !== this.focusedEditor) { this.focusedEditor = event.newValue; + this.contextKeyService.setContext(NOTEBOOK_EDITOR_FOCUSED, true); this.onDidChangeFocusedEditorEmitter.fire(this.focusedEditor); } } else if (event.newValue) { // Only unfocus editor if a new widget has been focused this.focusedEditor = undefined; + this.contextKeyService.setContext(NOTEBOOK_EDITOR_FOCUSED, true); this.onDidChangeFocusedEditorEmitter.fire(undefined); } }); diff --git a/packages/notebook/src/browser/service/notebook-kernel-service.ts b/packages/notebook/src/browser/service/notebook-kernel-service.ts index 9f5e0b81f3a38..f6271b2cbcb4d 100644 --- a/packages/notebook/src/browser/service/notebook-kernel-service.ts +++ b/packages/notebook/src/browser/service/notebook-kernel-service.ts @@ -24,7 +24,6 @@ import { StorageService } from '@theia/core/lib/browser'; import { NotebookKernelSourceAction } from '../../common'; import { NotebookModel } from '../view-model/notebook-model'; import { NotebookService } from './notebook-service'; -import { NotebookEditorWidgetService } from './notebook-editor-widget-service'; export interface SelectedNotebookKernelChangeEvent { notebook: URI; @@ -158,9 +157,6 @@ export class NotebookKernelService { @inject(StorageService) protected storageService: StorageService; - @inject(NotebookEditorWidgetService) - protected notebookEditorService: NotebookEditorWidgetService; - protected readonly kernels = new Map(); protected notebookBindings: Record = {}; 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 8a02da4c5c7d7..015295117a3ff 100644 --- a/packages/notebook/src/browser/view-model/notebook-cell-model.ts +++ b/packages/notebook/src/browser/view-model/notebook-cell-model.ts @@ -20,7 +20,6 @@ import { Disposable, DisposableCollection, Emitter, Event, URI } from '@theia/core'; import { inject, injectable, interfaces, postConstruct } from '@theia/core/shared/inversify'; -import { ContextKeyChangeEvent } from '@theia/core/lib/browser/context-key-service'; import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model'; import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service'; import { @@ -33,25 +32,15 @@ import { NotebookCellOutputModel } from './notebook-cell-output-model'; export const NotebookCellModelFactory = Symbol('NotebookModelFactory'); export type NotebookCellModelFactory = (props: NotebookCellModelProps) => NotebookCellModel; -export function createNotebookCellModelContainer(parent: interfaces.Container, props: NotebookCellModelProps, - notebookCellContextManager: new (...args: never[]) => unknown): interfaces.Container { +export function createNotebookCellModelContainer(parent: interfaces.Container, props: NotebookCellModelProps): interfaces.Container { const child = parent.createChild(); child.bind(NotebookCellModelProps).toConstantValue(props); - // We need the constructor as property here to avoid circular dependencies for the context manager - child.bind(NotebookCellContextManager).to(notebookCellContextManager).inSingletonScope(); child.bind(NotebookCellModel).toSelf(); return child; } -const NotebookCellContextManager = Symbol('NotebookCellContextManager'); -interface NotebookCellContextManager { - updateCellContext(cell: NotebookCellModel, context: HTMLElement): void; - dispose(): void; - onDidChangeContext: Event; -} - export interface CellInternalMetadataChangedEvent { readonly lastRunSuccessChanged?: boolean; } @@ -111,9 +100,6 @@ export class NotebookCellModel implements NotebookCell, Disposable { protected readonly onDidRequestCellEditChangeEmitter = new Emitter(); readonly onDidRequestCellEditChange = this.onDidRequestCellEditChangeEmitter.event; - @inject(NotebookCellContextManager) - readonly notebookCellContextManager: NotebookCellContextManager; - @inject(NotebookCellModelProps) protected readonly props: NotebookCellModelProps; @inject(MonacoTextModelService) @@ -152,14 +138,8 @@ export class NotebookCellModel implements NotebookCell, Disposable { protected textModel?: MonacoEditorModel; - protected htmlContext: HTMLLIElement; - - get context(): HTMLLIElement { - return this.htmlContext; - } - get text(): string { - return this.textModel ? this.textModel.getText() : this.source; + return this.textModel && !this.textModel.isDisposed() ? this.textModel.getText() : this.source; } get source(): string { @@ -198,6 +178,11 @@ export class NotebookCellModel implements NotebookCell, Disposable { return this.props.cellKind; } + protected _editing: boolean = false; + get editing(): boolean { + return this._editing; + } + @postConstruct() protected init(): void { this._outputs = this.props.outputs.map(op => new NotebookCellOutputModel(op)); @@ -205,13 +190,6 @@ export class NotebookCellModel implements NotebookCell, Disposable { this._internalMetadata = this.props.internalMetadata ?? {}; } - refChanged(node: HTMLLIElement): void { - if (node) { - this.htmlContext = node; - this.notebookCellContextManager.updateCellContext(this, node); - } - } - dispose(): void { this.onDidChangeOutputsEmitter.dispose(); this.onDidChangeOutputItemsEmitter.dispose(); @@ -219,18 +197,19 @@ export class NotebookCellModel implements NotebookCell, Disposable { this.onDidChangeMetadataEmitter.dispose(); this.onDidChangeInternalMetadataEmitter.dispose(); this.onDidChangeLanguageEmitter.dispose(); - this.notebookCellContextManager.dispose(); this.textModel?.dispose(); this.toDispose.dispose(); } requestEdit(): void { if (!this.textModel || !this.textModel.readOnly) { + this._editing = true; this.onDidRequestCellEditChangeEmitter.fire(true); } } requestStopEdit(): void { + this._editing = false; this.onDidRequestCellEditChangeEmitter.fire(false); } diff --git a/packages/notebook/src/browser/view-model/notebook-model.ts b/packages/notebook/src/browser/view-model/notebook-model.ts index a1236f63d18c7..7e88e23358eab 100644 --- a/packages/notebook/src/browser/view-model/notebook-model.ts +++ b/packages/notebook/src/browser/view-model/notebook-model.ts @@ -25,7 +25,6 @@ import { NotebookContentChangedEvent, NotebookModelWillAddRemoveEvent, CellEditO import { NotebookSerializer } from '../service/notebook-service'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { NotebookCellModel, NotebookCellModelFactory } from './notebook-cell-model'; -import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service'; import { inject, injectable, interfaces, postConstruct } from '@theia/core/shared/inversify'; import { UndoRedoService } from '@theia/editor/lib/browser/undo-redo-service'; import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; @@ -64,6 +63,9 @@ export class NotebookModel implements Saveable, Disposable { protected readonly onDidChangeContentEmitter = new Emitter(); readonly onDidChangeContent = this.onDidChangeContentEmitter.event; + protected readonly onDidChangeSelectedCellEmitter = new Emitter(); + readonly onDidChangeSelectedCell = this.onDidChangeSelectedCellEmitter.event; + get onDidChangeReadOnly(): Event { return this.props.resource.onDidChangeReadOnly ?? Event.None; } @@ -77,9 +79,6 @@ export class NotebookModel implements Saveable, Disposable { @inject(NotebookModelProps) protected props: NotebookModelProps; - @inject(MonacoTextModelService) - protected modelService: MonacoTextModelService; - @inject(NotebookCellModelFactory) protected cellModelFactory: NotebookCellModelFactory; readonly autoSave: 'off' | 'afterDelay' | 'onFocusChange' | 'onWindowChange'; @@ -232,6 +231,7 @@ export class NotebookModel implements Saveable, Disposable { setSelectedCell(cell: NotebookCellModel): void { this.selectedCell = cell; + this.onDidChangeSelectedCellEmitter.fire(cell); } private addCellOutputListeners(cells: NotebookCellModel[]): void { @@ -298,9 +298,13 @@ export class NotebookModel implements Saveable, Disposable { case CellEditType.Move: this.moveCellToIndex(cellIndex, edit.length, edit.newIdx, computeUndoRedo); break; - + } + // if selected cell is affected update it because it can potentially have been replaced + if (cell === this.selectedCell) { + this.setSelectedCell(this.cells[cellIndex]); } } + } protected async replaceCells(start: number, deleteCount: number, newCells: CellData[], computeUndoRedo: boolean): Promise { diff --git a/packages/notebook/src/browser/view/notebook-cell-editor.tsx b/packages/notebook/src/browser/view/notebook-cell-editor.tsx index 723e2ef3b5b10..31cdc04c35f2b 100644 --- a/packages/notebook/src/browser/view/notebook-cell-editor.tsx +++ b/packages/notebook/src/browser/view/notebook-cell-editor.tsx @@ -18,17 +18,20 @@ import * as React from '@theia/core/shared/react'; import { NotebookModel } from '../view-model/notebook-model'; import { NotebookCellModel } from '../view-model/notebook-cell-model'; import { SimpleMonacoEditor } from '@theia/monaco/lib/browser/simple-monaco-editor'; -import { MonacoEditorServices } from '@theia/monaco/lib/browser/monaco-editor'; +import { MonacoEditor, MonacoEditorServices } from '@theia/monaco/lib/browser/monaco-editor'; import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider'; import { DisposableCollection } from '@theia/core'; +import { IContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/common/contextkey'; +import { NotebookContextManager } from '../service/notebook-context-manager'; interface CellEditorProps { notebookModel: NotebookModel, cell: NotebookCellModel, - monacoServices: MonacoEditorServices + monacoServices: MonacoEditorServices, + notebookContextManager: NotebookContextManager; } -const DEFAULT_EDITOR_OPTIONS = { +const DEFAULT_EDITOR_OPTIONS: MonacoEditor.IOptions = { ...MonacoEditorProvider.inlineOptions, minHeight: -1, maxHeight: -1, @@ -68,7 +71,8 @@ export class CellEditor extends React.Component { editorModel, editorNode, monacoServices, - DEFAULT_EDITOR_OPTIONS); + DEFAULT_EDITOR_OPTIONS, + [[IContextKeyService, this.props.notebookContextManager.scopedStore]]); this.toDispose.push(this.editor); this.editor.setLanguage(cell.language); this.toDispose.push(this.editor.getControl().onDidContentSizeChange(() => { @@ -91,9 +95,9 @@ export class CellEditor extends React.Component { override render(): React.ReactNode { return
this.setContainer(container)}> + ref={container => this.setContainer(container)}>
; - } + } } diff --git a/packages/notebook/src/browser/view/notebook-cell-list-view.tsx b/packages/notebook/src/browser/view/notebook-cell-list-view.tsx index 6b17495120953..6ad3465386d8a 100644 --- a/packages/notebook/src/browser/view/notebook-cell-list-view.tsx +++ b/packages/notebook/src/browser/view/notebook-cell-list-view.tsx @@ -50,9 +50,13 @@ export class NotebookCellListView extends React.Component 0) { this.setState({ ...this.state, selectedCell: this.props.notebookModel.cells.find(model => model.handle === e.newCellIds![e.newCellIds!.length - 1]) }); } else { - this.setState({ ...this.state, selectedCell: this.props.notebookModel.cells.find(cell => cell === this.state.selectedCell)}); + this.setState({ ...this.state, selectedCell: this.props.notebookModel.cells.find(cell => cell === this.state.selectedCell) }); } })); + + this.toDispose.push(props.notebookModel.onDidChangeSelectedCell(cell => { + this.setState({ ...this.state, selectedCell: cell }); + })); } override componentWillUnmount(): void { @@ -71,15 +75,15 @@ export class NotebookCellListView extends React.Component this.onDragOver(e, cell, 'top')} /> {this.shouldRenderDragOverIndicator(cell, 'top') && }
  • { - this.setState({ selectedCell: cell }); + onClick={e => { + this.setState({ ...this.state, selectedCell: cell }); this.props.notebookModel.setSelectedCell(cell); }} onDragStart={e => this.onDragStart(e, index)} onDragOver={e => this.onDragOver(e, cell)} onDrop={e => this.onDrop(e, index)} draggable={true} - ref={(node: HTMLLIElement) => cell.refChanged(node)}> + ref={ref => cell === this.state.selectedCell && ref?.scrollIntoView({ block: 'nearest' })}>
    {this.renderCellContent(cell, index)} diff --git a/packages/notebook/src/browser/view/notebook-cell-toolbar-factory.tsx b/packages/notebook/src/browser/view/notebook-cell-toolbar-factory.tsx index ae11556c25be4..f6798745bcd65 100644 --- a/packages/notebook/src/browser/view/notebook-cell-toolbar-factory.tsx +++ b/packages/notebook/src/browser/view/notebook-cell-toolbar-factory.tsx @@ -23,6 +23,7 @@ import { ContextMenuRenderer } from '@theia/core/lib/browser'; import { NotebookModel } from '../view-model/notebook-model'; import { NotebookCellModel } from '../view-model/notebook-cell-model'; import { NotebookCellOutputModel } from '../view-model/notebook-cell-output-model'; +import { NotebookContextManager } from '../service/notebook-context-manager'; export interface NotebookCellToolbarItem { id: string; @@ -48,21 +49,24 @@ export class NotebookCellToolbarFactory { @inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry; + @inject(NotebookContextManager) + protected readonly notebookContextManager: NotebookContextManager; + renderCellToolbar(menuPath: string[], notebookModel: NotebookModel, cell: NotebookCellModel): React.ReactNode { return this.getMenuItems(menuPath, notebookModel, cell)} - onContextKeysChanged={cell.notebookCellContextManager.onDidChangeContext} />; + onContextKeysChanged={this.notebookContextManager.onDidChangeContext} />; } renderSidebar(menuPath: string[], notebookModel: NotebookModel, cell: NotebookCellModel, output?: NotebookCellOutputModel): React.ReactNode { return this.getMenuItems(menuPath, notebookModel, cell, output)} - onContextKeysChanged={cell.notebookCellContextManager.onDidChangeContext} />; + onContextKeysChanged={this.notebookContextManager.onDidChangeContext} />; } private getMenuItems(menuItemPath: string[], notebookModel: NotebookModel, cell: NotebookCellModel, output?: NotebookCellOutputModel): NotebookCellToolbarItem[] { const inlineItems: NotebookCellToolbarItem[] = []; for (const menuNode of this.menuRegistry.getMenu(menuItemPath).children) { - if (!menuNode.when || this.contextKeyService.match(menuNode.when, cell.context ?? undefined)) { + if (!menuNode.when || this.contextKeyService.match(menuNode.when, this.notebookContextManager.context)) { if (menuNode.role === CompoundMenuNodeRole.Flat) { inlineItems.push(...menuNode.children?.map(child => this.createToolbarItem(child, notebookModel, cell, output)) ?? []); } else { diff --git a/packages/notebook/src/browser/view/notebook-code-cell-view.tsx b/packages/notebook/src/browser/view/notebook-code-cell-view.tsx index 6be50f8812666..6d32dbd4e413d 100644 --- a/packages/notebook/src/browser/view/notebook-code-cell-view.tsx +++ b/packages/notebook/src/browser/view/notebook-code-cell-view.tsx @@ -29,6 +29,7 @@ import { CellExecution, NotebookExecutionStateService } from '../service/noteboo import { codicon } from '@theia/core/lib/browser'; import { NotebookCellExecutionState } from '../../common'; import { DisposableCollection } from '@theia/core'; +import { NotebookContextManager } from '../service/notebook-context-manager'; @injectable() export class NotebookCodeCellRenderer implements CellRenderer { @@ -47,6 +48,9 @@ export class NotebookCodeCellRenderer implements CellRenderer { @inject(NotebookExecutionStateService) protected readonly executionStateService: NotebookExecutionStateService; + @inject(NotebookContextManager) + protected readonly notebookContextManager: NotebookContextManager; + render(notebookModel: NotebookModel, cell: NotebookCellModel, handle: number): React.ReactNode { return
    @@ -56,7 +60,7 @@ export class NotebookCodeCellRenderer implements CellRenderer {

    {`[${cell.exec ?? ' '}]`}

    */}
    - +
    diff --git a/packages/notebook/src/browser/view/notebook-markdown-cell-view.tsx b/packages/notebook/src/browser/view/notebook-markdown-cell-view.tsx index af84d9fe361fc..5d2d0cff1e605 100644 --- a/packages/notebook/src/browser/view/notebook-markdown-cell-view.tsx +++ b/packages/notebook/src/browser/view/notebook-markdown-cell-view.tsx @@ -24,6 +24,7 @@ import { CellEditor } from './notebook-cell-editor'; import { inject, injectable } from '@theia/core/shared/inversify'; import { MonacoEditorServices } from '@theia/monaco/lib/browser/monaco-editor'; import { nls } from '@theia/core'; +import { NotebookContextManager } from '../service/notebook-context-manager'; @injectable() export class NotebookMarkdownCellRenderer implements CellRenderer { @@ -33,9 +34,12 @@ export class NotebookMarkdownCellRenderer implements CellRenderer { @inject(MonacoEditorServices) protected readonly monacoServices: MonacoEditorServices; + @inject(NotebookContextManager) + protected readonly notebookContextManager: NotebookContextManager; + render(notebookModel: NotebookModel, cell: NotebookCellModel): React.ReactNode { return ; + cell={cell} notebookModel={notebookModel} notebookContextManager={this.notebookContextManager} />; } } @@ -46,9 +50,10 @@ interface MarkdownCellProps { cell: NotebookCellModel, notebookModel: NotebookModel + notebookContextManager: NotebookContextManager; } -function MarkdownCell({ markdownRenderer, monacoServices, cell, notebookModel }: MarkdownCellProps): React.JSX.Element { +function MarkdownCell({ markdownRenderer, monacoServices, cell, notebookModel, notebookContextManager }: MarkdownCellProps): React.JSX.Element { const [editMode, setEditMode] = React.useState(false); React.useEffect(() => { @@ -62,7 +67,7 @@ function MarkdownCell({ markdownRenderer, monacoServices, cell, notebookModel }: } return editMode ? - : + :
    cell.requestEdit()} // This sets the non React HTML node from the markdown renderers output as a child node to this react component