deploy: current vibn theia state
Some checks failed
Playwright Tests / Playwright Tests (ubuntu-22.04, Node.js 22.x) (push) Has been cancelled
3PP License Check / 3PP License Check (11, 22.x, ubuntu-22.04) (push) Has been cancelled
Publish packages to NPM / Perform Publishing (push) Has been cancelled

Made-with: Cursor
This commit is contained in:
2026-02-27 12:01:08 -08:00
commit 8bb5110148
3782 changed files with 640947 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: [
'../../configs/build.eslintrc.json'
],
parserOptions: {
tsconfigRootDir: __dirname,
project: 'tsconfig.json'
}
};

View File

@@ -0,0 +1,31 @@
<div align='center'>
<br />
<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />
<h2>ECLIPSE THEIA - NOTEBOOK EXTENSION</h2>
<hr />
</div>
## Description
The `@theia/notebook` extension contributes functionality for integrated notebooks
## Additional Information
- [API documentation for `@theia/notebook`](https://eclipse-theia.github.io/theia/docs/next/modules/_theia_notebook.html)
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
- [Theia - Website](https://theia-ide.org/)
## License
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
## Trademark
"Theia" is a trademark of the Eclipse Foundation
<https://www.eclipse.org/theia>

View File

@@ -0,0 +1,58 @@
{
"name": "@theia/notebook",
"version": "1.68.0",
"description": "Theia - Notebook Extension",
"dependencies": {
"@theia/core": "1.68.0",
"@theia/editor": "1.68.0",
"@theia/filesystem": "1.68.0",
"@theia/monaco": "1.68.0",
"@theia/monaco-editor-core": "1.96.302",
"@theia/outline-view": "1.68.0",
"advanced-mark.js": "^2.6.0",
"react-perfect-scrollbar": "^1.5.8",
"tslib": "^2.6.2"
},
"publishConfig": {
"access": "public"
},
"theiaExtensions": [
{
"frontend": "lib/browser/notebook-frontend-module",
"backend": "lib/node/notebook-backend-module"
}
],
"keywords": [
"theia-extension"
],
"license": "EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0",
"repository": {
"type": "git",
"url": "https://github.com/eclipse-theia/theia.git"
},
"bugs": {
"url": "https://github.com/eclipse-theia/theia/issues"
},
"homepage": "https://github.com/eclipse-theia/theia",
"files": [
"lib",
"src"
],
"scripts": {
"build": "theiaext build",
"clean": "theiaext clean",
"compile": "theiaext compile",
"docs": "theiaext docs",
"lint": "theiaext lint",
"watch": "theiaext watch"
},
"devDependencies": {
"@theia/ext-scripts": "1.68.0",
"@types/markdown-it": "^12.2.3",
"@types/vscode-notebook-renderer": "^1.72.0"
},
"nyc": {
"extends": "../../configs/nyc.json"
},
"gitHead": "21358137e41342742707f660b8e222f940a27652"
}

View File

@@ -0,0 +1,44 @@
// *****************************************************************************
// 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, language?: string): void {
if (cell.cellKind === type) {
return;
}
if (type === CellKind.Markup) {
language = 'markdown';
} else {
language ??= cell.language;
}
notebookModel.applyEdits([{
editType: CellEditType.Replace,
index: notebookModel.cells.indexOf(cell),
count: 1,
cells: [{
...cell.getData(),
cellKind: type,
language
}]
}], true);
}

View File

@@ -0,0 +1,383 @@
// *****************************************************************************
// 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 { Command, CommandContribution, CommandHandler, CommandRegistry, MenuContribution, MenuModelRegistry, nls, URI } from '@theia/core';
import { inject, injectable } from '@theia/core/shared/inversify';
import { ApplicationShell, codicon, 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 { NotebookEditorWidgetService } from '../service/notebook-editor-widget-service';
import {
NOTEBOOK_CELL_CURSOR_FIRST_LINE, NOTEBOOK_CELL_CURSOR_LAST_LINE,
NOTEBOOK_CELL_FOCUSED, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_HAS_OUTPUTS, NOTEBOOK_OUTPUT_FOCUSED
} 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({
id: 'notebook.add-new-cell',
iconClass: codicon('add')
});
export const ADD_NEW_MARKDOWN_CELL_COMMAND = Command.toDefaultLocalizedCommand({
id: 'notebook.add-new-markdown-cell',
iconClass: codicon('add'),
tooltip: nls.localizeByDefault('Add Markdown Cell')
} as NotebookCommand);
export const ADD_NEW_CODE_CELL_COMMAND = Command.toDefaultLocalizedCommand({
id: 'notebook.add-new-code-cell',
iconClass: codicon('add'),
tooltip: nls.localizeByDefault('Add Code Cell')
} as NotebookCommand);
export const SELECT_KERNEL_COMMAND = Command.toDefaultLocalizedCommand({
id: 'notebook.selectKernel',
category: 'Notebook',
iconClass: codicon('server-environment')
});
export const EXECUTE_NOTEBOOK_COMMAND = Command.toDefaultLocalizedCommand({
id: 'notebook.execute',
category: 'Notebook',
iconClass: codicon('run-all')
});
export const CLEAR_ALL_OUTPUTS_COMMAND = Command.toDefaultLocalizedCommand({
id: 'notebook.clear-all-outputs',
category: 'Notebook',
iconClass: codicon('clear-all')
});
export const CHANGE_SELECTED_CELL = Command.toDefaultLocalizedCommand({
id: 'notebook.change-selected-cell',
category: 'Notebook',
});
export const CUT_SELECTED_CELL = Command.toDefaultLocalizedCommand({
id: 'notebook.cell.cut',
category: 'Notebook',
});
export const COPY_SELECTED_CELL = Command.toDefaultLocalizedCommand({
id: 'notebook.cell.copy',
category: 'Notebook',
});
export const PASTE_CELL = Command.toDefaultLocalizedCommand({
id: 'notebook.cell.paste',
category: 'Notebook',
});
export const NOTEBOOK_FIND = Command.toDefaultLocalizedCommand({
id: 'notebook.find',
category: 'Notebook',
});
export const CENTER_ACTIVE_CELL = Command.toDefaultLocalizedCommand({
id: 'notebook.centerActiveCell',
category: 'Notebook',
});
}
export enum CellChangeDirection {
Up = 'up',
Down = 'down'
}
@injectable()
export class NotebookActionsContribution implements CommandContribution, MenuContribution, KeybindingContribution {
@inject(NotebookService)
protected notebookService: NotebookService;
@inject(NotebookKernelQuickPickService)
protected notebookKernelQuickPickService: NotebookKernelQuickPickService;
@inject(NotebookExecutionService)
protected notebookExecutionService: NotebookExecutionService;
@inject(ApplicationShell)
protected shell: ApplicationShell;
@inject(NotebookEditorWidgetService)
protected notebookEditorWidgetService: NotebookEditorWidgetService;
@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', focusContainer?: boolean) => {
notebookModel = notebookModel ?? this.notebookEditorWidgetService.focusedEditor?.model;
const viewModel = this.notebookEditorWidgetService.focusedEditor?.viewModel;
let insertIndex: number = 0;
if (typeof index === 'number' && index >= 0) {
insertIndex = index;
} else if (viewModel?.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(viewModel.selectedCell) + (index === 'below' ? 1 : 0);
}
let cellLanguage: string = 'markdown';
if (cellKind === CellKind.Code) {
cellLanguage = this.notebookService.getCodeCellLanguage(notebookModel);
}
notebookModel.applyEdits([{
editType: CellEditType.Replace,
index: insertIndex,
count: 0,
cells: [{
cellKind,
language: cellLanguage,
source: '',
outputs: [],
metadata: {},
}]
}], true);
if (focusContainer) {
viewModel?.cellViewModels.get(viewModel.selectedCell?.handle ?? -1)?.requestBlurEditor();
}
}
});
commands.registerCommand(NotebookCommands.ADD_NEW_MARKDOWN_CELL_COMMAND, this.editableCommandHandler(
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, 'below')
));
commands.registerCommand(NotebookCommands.SELECT_KERNEL_COMMAND, this.editableCommandHandler(
notebookModel => this.notebookKernelQuickPickService.showQuickPick(notebookModel)
));
commands.registerCommand(NotebookCommands.EXECUTE_NOTEBOOK_COMMAND, this.editableCommandHandler(
notebookModel => this.notebookExecutionService.executeNotebookCells(notebookModel, notebookModel.cells)
));
commands.registerCommand(NotebookCommands.CLEAR_ALL_OUTPUTS_COMMAND, this.editableCommandHandler(
notebookModel => notebookModel.applyEdits(notebookModel.cells.map(cell => ({
editType: CellEditType.Output,
handle: cell.handle, deleteCount: cell.outputs.length, outputs: []
})), false)
));
commands.registerCommand(NotebookCommands.CHANGE_SELECTED_CELL,
{
execute: (change: number | CellChangeDirection) => {
const focusedEditor = this.notebookEditorWidgetService.focusedEditor;
const model = focusedEditor?.model;
const viewModel = focusedEditor?.viewModel;
if (model && typeof change === 'number') {
viewModel?.setSelectedCell(model.cells[change]);
} else if (model && viewModel?.selectedCell) {
const currentIndex = model.cells.indexOf(viewModel?.selectedCell);
const shouldFocusEditor = this.contextKeyService.match('editorTextFocus');
if (change === CellChangeDirection.Up && currentIndex > 0) {
viewModel?.setSelectedCell(model.cells[currentIndex - 1]);
if ((viewModel?.selectedCell?.cellKind === CellKind.Code
|| (viewModel?.selectedCell?.cellKind === CellKind.Markup && viewModel?.selectedCellViewModel?.editing)) && shouldFocusEditor) {
viewModel?.cellViewModels.get(viewModel.selectedCell.handle)?.requestFocusEditor('lastLine');
}
} else if (change === CellChangeDirection.Down && currentIndex < model.cells.length - 1) {
viewModel?.setSelectedCell(model.cells[currentIndex + 1]);
if ((viewModel?.selectedCell?.cellKind === CellKind.Code
|| (viewModel?.selectedCell?.cellKind === CellKind.Markup && viewModel?.selectedCellViewModel?.editing)) && shouldFocusEditor) {
viewModel?.cellViewModels.get(viewModel.selectedCell.handle)?.requestFocusEditor();
}
}
if (viewModel?.selectedCell.cellKind === CellKind.Markup) {
// since were losing focus from the cell editor, we need to focus the notebook editor again
focusedEditor?.node.focus();
}
}
}
}
);
commands.registerCommand({ id: 'list.focusUp' }, {
execute: () => commands.executeCommand(NotebookCommands.CHANGE_SELECTED_CELL.id, CellChangeDirection.Up)
});
commands.registerCommand({ id: 'list.focusDown' }, {
execute: () => commands.executeCommand(NotebookCommands.CHANGE_SELECTED_CELL.id, CellChangeDirection.Down)
});
commands.registerCommand(NotebookCommands.CUT_SELECTED_CELL, this.editableCommandHandler(
() => {
const model = this.notebookEditorWidgetService.focusedEditor?.model;
const selectedCell = this.notebookEditorWidgetService.focusedEditor?.viewModel?.selectedCell;
if (selectedCell && model) {
model.applyEdits([{ editType: CellEditType.Replace, index: model.cells.indexOf(selectedCell), count: 1, cells: [] }], true);
this.notebookClipboardService.copyCell(selectedCell);
}
}));
commands.registerCommand(NotebookCommands.COPY_SELECTED_CELL, {
execute: () => {
const viewModel = this.notebookEditorWidgetService.focusedEditor?.viewModel;
const selectedCell = viewModel?.selectedCell;
if (selectedCell) {
this.notebookClipboardService.copyCell(selectedCell);
}
}
});
commands.registerCommand(NotebookCommands.PASTE_CELL, {
isEnabled: () => !Boolean(this.notebookEditorWidgetService.focusedEditor?.model?.readOnly),
isVisible: () => !Boolean(this.notebookEditorWidgetService.focusedEditor?.model?.readOnly),
execute: (position?: 'above') => {
const copiedCell = this.notebookClipboardService.getCell();
if (copiedCell) {
const model = this.notebookEditorWidgetService.focusedEditor?.model;
const viewModel = this.notebookEditorWidgetService.focusedEditor?.viewModel;
const insertIndex = viewModel?.selectedCell && model ? model.cells.indexOf(viewModel?.selectedCell) + (position === 'above' ? 0 : 1) : 0;
model?.applyEdits([{ editType: CellEditType.Replace, index: insertIndex, count: 0, cells: [copiedCell] }], true);
}
}
});
commands.registerCommand(NotebookCommands.NOTEBOOK_FIND, {
execute: () => {
this.notebookEditorWidgetService.focusedEditor?.showFindWidget();
}
});
commands.registerCommand(NotebookCommands.CENTER_ACTIVE_CELL, {
execute: (editor?: NotebookEditorWidget) => {
const viewModel = editor ? editor.viewModel : this.notebookEditorWidgetService.focusedEditor?.viewModel;
viewModel?.selectedCell?.requestCenterEditor();
}
});
}
protected editableCommandHandler(execute: (notebookModel: NotebookModel) => void): CommandHandler {
return {
isEnabled: (item: URI | NotebookModel) => this.withModel(item, model => !Boolean(model?.readOnly), false),
isVisible: (item: URI | NotebookModel) => this.withModel(item, model => !Boolean(model?.readOnly), false),
execute: (uri: URI | NotebookModel) => {
this.withModel(uri, execute, undefined);
}
};
}
protected withModel<T>(item: URI | NotebookModel, execute: (notebookModel: NotebookModel) => T, defaultValue: T): T {
if (item instanceof URI) {
const model = this.notebookService.getNotebookEditorModel(item);
if (!model) {
return defaultValue;
}
item = model;
}
return execute(item);
}
registerMenus(menus: MenuModelRegistry): void {
// independent submenu for plugins to add commands
menus.registerSubmenu(NotebookMenus.NOTEBOOK_MAIN_TOOLBAR, 'Notebook Main Toolbar');
// Add Notebook Cell items
menus.registerMenuAction(NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_CELL_ADD_GROUP, {
commandId: NotebookCommands.ADD_NEW_CODE_CELL_COMMAND.id,
label: nls.localizeByDefault('Code'),
icon: codicon('add'),
});
menus.registerMenuAction(NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_CELL_ADD_GROUP, {
commandId: NotebookCommands.ADD_NEW_MARKDOWN_CELL_COMMAND.id,
label: nls.localizeByDefault('Markdown'),
icon: codicon('add'),
});
// Execution related items
menus.registerMenuAction(NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_EXECUTION_GROUP, {
commandId: NotebookCommands.EXECUTE_NOTEBOOK_COMMAND.id,
label: nls.localizeByDefault('Run All'),
icon: codicon('run-all'),
order: '10'
});
menus.registerMenuAction(NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_EXECUTION_GROUP, {
commandId: NotebookCommands.CLEAR_ALL_OUTPUTS_COMMAND.id,
label: nls.localizeByDefault('Clear All Outputs'),
icon: codicon('clear-all'),
order: '30',
when: NOTEBOOK_HAS_OUTPUTS
});
menus.registerSubmenu(NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_HIDDEN_ITEMS_CONTEXT_MENU, '');
}
registerKeybindings(keybindings: KeybindingRegistry): void {
keybindings.registerKeybindings(
{
command: NotebookCommands.CHANGE_SELECTED_CELL.id,
keybinding: 'up',
args: CellChangeDirection.Up,
when: `(!editorTextFocus || ${NOTEBOOK_CELL_CURSOR_FIRST_LINE}) && !suggestWidgetVisible && ${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED}`
},
{
command: NotebookCommands.CHANGE_SELECTED_CELL.id,
keybinding: 'down',
args: CellChangeDirection.Down,
when: `(!editorTextFocus || ${NOTEBOOK_CELL_CURSOR_LAST_LINE}) && !suggestWidgetVisible && ${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED}`
},
{
command: NotebookCommands.CUT_SELECTED_CELL.id,
keybinding: 'ctrlcmd+x',
when: `${NOTEBOOK_EDITOR_FOCUSED} && !inputFocus && !${NOTEBOOK_OUTPUT_FOCUSED}`
},
{
command: NotebookCommands.COPY_SELECTED_CELL.id,
keybinding: 'ctrlcmd+c',
when: `${NOTEBOOK_EDITOR_FOCUSED} && !inputFocus && !${NOTEBOOK_OUTPUT_FOCUSED}`
},
{
command: NotebookCommands.PASTE_CELL.id,
keybinding: 'ctrlcmd+v',
when: `${NOTEBOOK_EDITOR_FOCUSED} && !inputFocus && !${NOTEBOOK_OUTPUT_FOCUSED}`
},
{
command: NotebookCommands.NOTEBOOK_FIND.id,
keybinding: 'ctrlcmd+f',
when: `${NOTEBOOK_EDITOR_FOCUSED}`
},
{
command: NotebookCommands.CENTER_ACTIVE_CELL.id,
keybinding: 'ctrlcmd+l',
when: `${NOTEBOOK_EDITOR_FOCUSED}`
}
);
}
}
export namespace NotebookMenus {
export const NOTEBOOK_MAIN_TOOLBAR = ['notebook', 'toolbar'];
export const NOTEBOOK_MAIN_TOOLBAR_CELL_ADD_GROUP = [...NOTEBOOK_MAIN_TOOLBAR, 'cell-add-group'];
export const NOTEBOOK_MAIN_TOOLBAR_EXECUTION_GROUP = [...NOTEBOOK_MAIN_TOOLBAR, 'cell-execution-group'];
export const NOTEBOOK_MAIN_TOOLBAR_HIDDEN_ITEMS_CONTEXT_MENU = ['notebook-main-toolbar-hidden-items-context-menu'];
}

View File

@@ -0,0 +1,586 @@
// *****************************************************************************
// 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 { Command, CommandContribution, CommandHandler, CommandRegistry, MenuContribution, MenuModelRegistry, nls } from '@theia/core';
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, NOTEBOOK_EDITOR_FOCUSED,
NOTEBOOK_CELL_FOCUSED,
NOTEBOOK_CELL_LIST_FOCUSED
} 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 { CellData, CellEditType, CellKind } from '../../common';
import { NotebookEditorWidgetService } from '../service/notebook-editor-widget-service';
import { NotebookCommands } from './notebook-actions-contribution';
import { changeCellType } from './cell-operations';
import { EditorLanguageQuickPickService } from '@theia/editor/lib/browser/editor-language-quick-pick-service';
import { NotebookService } from '../service/notebook-service';
import { Selection } from '@theia/monaco-editor-core/esm/vs/editor/common/core/selection';
import { Range } from '@theia/core/shared/vscode-languageserver-protocol';
import { NOTEBOOK_EDITOR_ID_PREFIX } from '../notebook-editor-widget';
export namespace NotebookCellCommands {
/** Parameters: notebookModel: NotebookModel | undefined, cell: NotebookCellModel */
export const EDIT_COMMAND = Command.toDefaultLocalizedCommand({
id: 'notebook.cell.edit',
category: 'Notebook',
iconClass: codicon('edit')
});
/** Parameters: notebookModel: NotebookModel | undefined, cell: NotebookCellModel */
export const STOP_EDIT_COMMAND = Command.toDefaultLocalizedCommand({
id: 'notebook.cell.stop-edit',
category: 'Notebook',
iconClass: codicon('check')
});
/** Parameters: notebookModel: NotebookModel, cell: NotebookCellModel */
export const DELETE_COMMAND = Command.toDefaultLocalizedCommand({
id: 'notebook.cell.delete',
iconClass: codicon('trash')
});
/** Parameters: notebookModel: NotebookModel, cell: NotebookCellModel */
export const SPLIT_CELL_COMMAND = Command.toDefaultLocalizedCommand({
id: 'notebook.cell.split',
iconClass: codicon('split-vertical'),
});
/** Parameters: notebookModel: NotebookModel, cell: NotebookCellModel */
export const EXECUTE_SINGLE_CELL_COMMAND = Command.toDefaultLocalizedCommand({
id: 'notebook.cell.execute-cell',
category: 'Notebook',
label: 'Execute Cell',
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',
label: 'Execute Notebook Cell and Select Below',
category: 'Notebook',
});
/** Parameters: notebookModel: NotebookModel, cell: NotebookCellModel */
export const EXECUTE_SINGLE_CELL_AND_INSERT_BELOW_COMMAND = Command.toDefaultLocalizedCommand({
id: 'notebook.cell.execute-cell-and-insert-below',
label: 'Execute Notebook Cell and Insert Below',
category: 'Notebook',
});
export const EXECUTE_ABOVE_CELLS_COMMAND = Command.toDefaultLocalizedCommand({
id: 'notebookActions.executeAbove',
label: 'Execute Above Cells',
iconClass: codicon('run-above')
});
export const EXECUTE_CELL_AND_BELOW_COMMAND = Command.toDefaultLocalizedCommand({
id: 'notebookActions.executeBelow',
label: 'Execute Cell and Below',
iconClass: codicon('run-below')
});
/** Parameters: notebookModel: NotebookModel, cell: NotebookCellModel */
export const STOP_CELL_EXECUTION_COMMAND = Command.toDefaultLocalizedCommand({
id: 'notebook.cell.stop-cell-execution',
iconClass: codicon('stop'),
});
/** Parameters: notebookModel: NotebookModel | undefined, cell: NotebookCellModel */
export const CLEAR_OUTPUTS_COMMAND = Command.toDefaultLocalizedCommand({
id: 'notebook.cell.clear-outputs',
category: 'Notebook',
label: 'Clear Cell Outputs',
});
/** Parameters: notebookModel: NotebookModel | undefined, cell: NotebookCellModel | undefined, output: NotebookCellOutputModel */
export const CHANGE_OUTPUT_PRESENTATION_COMMAND = Command.toDefaultLocalizedCommand({
id: 'notebook.cell.change-presentation',
category: 'Notebook',
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.changeToCode',
category: 'Notebook',
label: 'Change Cell to Code'
});
export const TO_MARKDOWN_CELL_COMMAND = Command.toLocalizedCommand({
id: 'notebook.cell.changeToMarkdown',
category: 'Notebook',
label: 'Change Cell to Markdown'
});
export const TOGGLE_CELL_OUTPUT = Command.toDefaultLocalizedCommand({
id: 'notebook.cell.toggleOutputs',
category: 'Notebook',
label: 'Collapse Cell Output',
});
export const CHANGE_CELL_LANGUAGE = Command.toDefaultLocalizedCommand({
id: 'notebook.cell.changeLanguage',
category: 'Notebook',
label: 'Change Cell Language',
});
export const TOGGLE_LINE_NUMBERS = Command.toDefaultLocalizedCommand({
id: 'notebook.cell.toggleLineNumbers',
category: 'Notebook',
label: 'Show Cell Line Numbers',
});
}
@injectable()
export class NotebookCellActionContribution implements MenuContribution, CommandContribution, KeybindingContribution {
@inject(ContextKeyService)
protected contextKeyService: ContextKeyService;
@inject(NotebookService)
protected notebookService: NotebookService;
@inject(NotebookExecutionService)
protected notebookExecutionService: NotebookExecutionService;
@inject(NotebookEditorWidgetService)
protected notebookEditorWidgetService: NotebookEditorWidgetService;
@inject(EditorLanguageQuickPickService)
protected languageQuickPickService: EditorLanguageQuickPickService;
@postConstruct()
protected init(): void {
NotebookContextKeys.initNotebookContextKeys(this.contextKeyService);
}
registerMenus(menus: MenuModelRegistry): void {
menus.registerMenuAction(NotebookCellActionContribution.ACTION_MENU, {
commandId: NotebookCellCommands.EDIT_COMMAND.id,
icon: NotebookCellCommands.EDIT_COMMAND.iconClass,
when: `${NOTEBOOK_CELL_TYPE} == 'markdown' && !${NOTEBOOK_CELL_MARKDOWN_EDIT_MODE}`,
label: nls.localizeByDefault('Edit Cell'),
order: '10'
});
menus.registerMenuAction(NotebookCellActionContribution.ACTION_MENU, {
commandId: NotebookCellCommands.STOP_EDIT_COMMAND.id,
icon: NotebookCellCommands.STOP_EDIT_COMMAND.iconClass,
when: `${NOTEBOOK_CELL_TYPE} == 'markdown' && ${NOTEBOOK_CELL_MARKDOWN_EDIT_MODE}`,
label: nls.localizeByDefault('Stop Editing Cell'),
order: '10'
});
menus.registerMenuAction(NotebookCellActionContribution.ACTION_MENU, {
commandId: NotebookCellCommands.EXECUTE_ABOVE_CELLS_COMMAND.id,
icon: NotebookCellCommands.EXECUTE_ABOVE_CELLS_COMMAND.iconClass,
when: `${NOTEBOOK_CELL_TYPE} == 'code'`,
label: nls.localizeByDefault('Execute Above Cells'),
order: '10'
});
menus.registerMenuAction(NotebookCellActionContribution.ACTION_MENU, {
commandId: NotebookCellCommands.EXECUTE_CELL_AND_BELOW_COMMAND.id,
icon: NotebookCellCommands.EXECUTE_CELL_AND_BELOW_COMMAND.iconClass,
when: `${NOTEBOOK_CELL_TYPE} == 'code'`,
label: nls.localizeByDefault('Execute Cell and Below'),
order: '20'
});
menus.registerMenuAction(NotebookCellActionContribution.ACTION_MENU, {
commandId: NotebookCellCommands.SPLIT_CELL_COMMAND.id,
icon: NotebookCellCommands.SPLIT_CELL_COMMAND.iconClass,
label: nls.localizeByDefault('Split Cell'),
order: '20'
});
menus.registerMenuAction(NotebookCellActionContribution.ACTION_MENU, {
commandId: NotebookCellCommands.DELETE_COMMAND.id,
icon: NotebookCellCommands.DELETE_COMMAND.iconClass,
label: nls.localizeByDefault('Delete Cell'),
order: '999'
});
menus.registerSubmenu(
NotebookCellActionContribution.ADDITIONAL_ACTION_MENU,
nls.localizeByDefault('More'),
{
sortString: '30',
icon: codicon('ellipsis')
}
);
menus.registerSubmenu(NotebookCellActionContribution.CONTRIBUTED_CELL_ACTION_MENU, '');
// since contributions are adding to an independent submenu we have to manually add it to the more submenu
menus.linkCompoundMenuNode({
newParentPath: NotebookCellActionContribution.ADDITIONAL_ACTION_MENU,
submenuPath: NotebookCellActionContribution.CONTRIBUTED_CELL_ACTION_MENU
});
// code cell sidebar menu
menus.registerMenuAction(NotebookCellActionContribution.CODE_CELL_SIDEBAR_MENU, {
commandId: NotebookCellCommands.EXECUTE_SINGLE_CELL_COMMAND.id,
icon: NotebookCellCommands.EXECUTE_SINGLE_CELL_COMMAND.iconClass,
label: nls.localizeByDefault('Execute Cell'),
when: `!${NOTEBOOK_CELL_EXECUTING}`
});
menus.registerMenuAction(NotebookCellActionContribution.CODE_CELL_SIDEBAR_MENU, {
commandId: NotebookCellCommands.STOP_CELL_EXECUTION_COMMAND.id,
icon: NotebookCellCommands.STOP_CELL_EXECUTION_COMMAND.iconClass,
label: nls.localizeByDefault('Stop Cell Execution'),
when: NOTEBOOK_CELL_EXECUTING
});
// Notebook Cell extra execution options
menus.registerSubmenu(NotebookCellActionContribution.CONTRIBUTED_CELL_EXECUTION_MENU,
nls.localizeByDefault('More...'),
{ icon: codicon('chevron-down') });
// menus.getMenu(NotebookCellActionContribution.CODE_CELL_SIDEBAR_MENU).addNode(menus.getMenuNode(NotebookCellActionContribution.CONTRIBUTED_CELL_EXECUTION_MENU));
// code cell output sidebar menu
menus.registerSubmenu(
NotebookCellActionContribution.ADDITIONAL_OUTPUT_SIDEBAR_MENU,
nls.localizeByDefault('More'),
{ icon: codicon('ellipsis') }
);
menus.registerMenuAction(NotebookCellActionContribution.ADDITIONAL_OUTPUT_SIDEBAR_MENU, {
commandId: NotebookCellCommands.CLEAR_OUTPUTS_COMMAND.id,
label: nls.localizeByDefault('Clear Cell Outputs'),
});
menus.registerMenuAction(NotebookCellActionContribution.ADDITIONAL_OUTPUT_SIDEBAR_MENU, {
commandId: NotebookCellCommands.CHANGE_OUTPUT_PRESENTATION_COMMAND.id,
label: nls.localizeByDefault('Change Presentation'),
});
}
registerCommands(commands: CommandRegistry): void {
commands.registerCommand(NotebookCellCommands.EDIT_COMMAND, this.editableCellCommandHandler((_, cell) => {
const cellViewModel = this.notebookEditorWidgetService.focusedEditor?.viewModel.cellViewModels.get(cell.handle);
cellViewModel?.requestFocusEditor();
}));
commands.registerCommand(NotebookCellCommands.STOP_EDIT_COMMAND, {
execute: (_, cell: NotebookCellModel) => {
const cellViewModel = this.notebookEditorWidgetService.focusedEditor?.viewModel.cellViewModels.get(cell.handle);
cellViewModel?.requestBlurEditor();
}
});
commands.registerCommand(NotebookCellCommands.DELETE_COMMAND,
this.editableCellCommandHandler((notebookModel, cell) => {
notebookModel.applyEdits([{
editType: CellEditType.Replace,
index: notebookModel.cells.indexOf(cell),
count: 1,
cells: []
}]
, true);
}));
commands.registerCommand(NotebookCellCommands.SPLIT_CELL_COMMAND, this.editableCellCommandHandler(
async (notebookModel, cell) => {
// selection (0,0,0,0) should also be used in !cell.editing mode, but `cell.editing`
// is not properly implemented for Code cells.
const cellSelection: Range = cell.selection ?? { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } };
const textModel = await cell.resolveTextModel();
// Create new cell with the text after the cursor
const splitOffset = textModel.offsetAt({
line: cellSelection.start.line,
character: cellSelection.start.character
});
const newCell: CellData = {
cellKind: cell.cellKind,
language: cell.language,
outputs: [],
source: textModel.getText().substring(splitOffset),
};
// add new cell below
const index = notebookModel.cells.indexOf(cell);
notebookModel.applyEdits([{ editType: CellEditType.Replace, index: index + 1, count: 0, cells: [newCell] }], true);
// update current cell text (undo-able)
const selection = new Selection(cellSelection.start.line + 1, cellSelection.start.character + 1, cellSelection.end.line + 1, cellSelection.end.character + 1);
const endPosition = textModel.positionAt(textModel.getText().length);
const deleteOp = {
range: {
startLineNumber: selection.startLineNumber,
startColumn: selection.startColumn,
endLineNumber: endPosition.line + 1,
endColumn: endPosition.character + 1
},
// eslint-disable-next-line no-null/no-null
text: null
};
// Create a new undo/redo stack entry
textModel.textEditorModel.pushStackElement();
textModel.textEditorModel.pushEditOperations([selection], [deleteOp], () => [selection]);
})
);
commands.registerCommand(NotebookCellCommands.EXECUTE_SINGLE_CELL_COMMAND, this.editableCellCommandHandler(
(notebookModel, cell) => {
this.notebookExecutionService.executeNotebookCells(notebookModel, [cell]);
})
);
commands.registerCommand(NotebookCellCommands.EXECUTE_SINGLE_CELL_AND_FOCUS_NEXT_COMMAND, this.editableCellCommandHandler(
(notebookModel, cell) => {
const viewModel = this.notebookEditorWidgetService.focusedEditor?.viewModel;
if (cell.cellKind === CellKind.Code) {
commands.executeCommand(NotebookCellCommands.EXECUTE_SINGLE_CELL_COMMAND.id, notebookModel, cell);
} else {
commands.executeCommand(NotebookCellCommands.STOP_EDIT_COMMAND.id, notebookModel, cell);
}
const index = notebookModel.cells.indexOf(cell);
if (index < notebookModel.cells.length - 1) {
viewModel?.setSelectedCell(notebookModel.cells[index + 1]);
} else if (cell.cellKind === CellKind.Code) {
commands.executeCommand(NotebookCellCommands.INSERT_NEW_CELL_BELOW_COMMAND.id);
} else {
commands.executeCommand(NotebookCellCommands.INSERT_MARKDOWN_CELL_BELOW_COMMAND.id);
}
})
);
commands.registerCommand(NotebookCellCommands.EXECUTE_SINGLE_CELL_AND_INSERT_BELOW_COMMAND, this.editableCellCommandHandler(
async (notebookModel, cell) => {
if (cell.cellKind === CellKind.Code) {
await commands.executeCommand(NotebookCellCommands.EXECUTE_SINGLE_CELL_COMMAND.id, notebookModel, cell);
}
await commands.executeCommand(NotebookCellCommands.STOP_EDIT_COMMAND.id, notebookModel, cell);
if (cell.cellKind === CellKind.Code) {
await commands.executeCommand(NotebookCellCommands.INSERT_NEW_CELL_BELOW_COMMAND.id);
} else {
await commands.executeCommand(NotebookCellCommands.INSERT_MARKDOWN_CELL_BELOW_COMMAND.id);
}
const index = notebookModel.cells.indexOf(cell);
const viewModel = this.notebookEditorWidgetService.focusedEditor?.viewModel;
viewModel?.setSelectedCell(notebookModel.cells[index + 1]);
})
);
commands.registerCommand(NotebookCellCommands.EXECUTE_ABOVE_CELLS_COMMAND, this.editableCellCommandHandler(
(notebookModel, cell) => {
const index = notebookModel.cells.indexOf(cell);
if (index > 0) {
this.notebookExecutionService.executeNotebookCells(notebookModel, notebookModel.cells.slice(0, index).filter(c => c.cellKind === CellKind.Code));
}
})
);
commands.registerCommand(NotebookCellCommands.EXECUTE_CELL_AND_BELOW_COMMAND, this.editableCellCommandHandler(
(notebookModel, cell) => {
const index = notebookModel.cells.indexOf(cell);
if (index >= 0) {
this.notebookExecutionService.executeNotebookCells(notebookModel, notebookModel.cells.slice(index).filter(c => c.cellKind === CellKind.Code));
}
})
);
commands.registerCommand(NotebookCellCommands.STOP_CELL_EXECUTION_COMMAND, {
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(
(notebook, cell) => (notebook ?? this.notebookEditorWidgetService.focusedEditor?.model)?.applyEdits([{
editType: CellEditType.Output,
handle: cell.handle,
outputs: [],
deleteCount: cell.outputs.length,
append: false
}], true)
));
commands.registerCommand(NotebookCellCommands.CHANGE_OUTPUT_PRESENTATION_COMMAND, this.editableCellCommandHandler(
(notebook, cell, output) => {
this.notebookEditorWidgetService.getNotebookEditor(NOTEBOOK_EDITOR_ID_PREFIX + notebook.uri.toString())?.requestOuputPresentationChange(cell.handle, output);
}
));
const insertCommand = (type: CellKind, index: number | 'above' | 'below', focusContainer: boolean): CommandHandler => this.editableCellCommandHandler(() =>
commands.executeCommand(NotebookCommands.ADD_NEW_CELL_COMMAND.id, undefined, type, index, focusContainer)
);
commands.registerCommand(NotebookCellCommands.INSERT_NEW_CELL_ABOVE_COMMAND, insertCommand(CellKind.Code, 'above', true));
commands.registerCommand(NotebookCellCommands.INSERT_NEW_CELL_BELOW_COMMAND, insertCommand(CellKind.Code, 'below', true));
commands.registerCommand(NotebookCellCommands.INSERT_MARKDOWN_CELL_ABOVE_COMMAND, insertCommand(CellKind.Markup, 'above', false));
commands.registerCommand(NotebookCellCommands.INSERT_MARKDOWN_CELL_BELOW_COMMAND, insertCommand(CellKind.Markup, 'below', false));
commands.registerCommand(NotebookCellCommands.TO_CODE_CELL_COMMAND, this.editableCellCommandHandler((notebookModel, cell) => {
changeCellType(notebookModel, cell, CellKind.Code, this.notebookService.getCodeCellLanguage(notebookModel));
}));
commands.registerCommand(NotebookCellCommands.TO_MARKDOWN_CELL_COMMAND, this.editableCellCommandHandler((notebookModel, cell) => {
changeCellType(notebookModel, cell, CellKind.Markup);
}));
commands.registerCommand(NotebookCellCommands.TOGGLE_CELL_OUTPUT, {
execute: () => {
const selectedCell = this.notebookEditorWidgetService.focusedEditor?.viewModel?.selectedCell;
if (selectedCell) {
selectedCell.outputVisible = !selectedCell.outputVisible;
}
}
});
commands.registerCommand(NotebookCellCommands.CHANGE_CELL_LANGUAGE, {
isVisible: () => !!this.notebookEditorWidgetService.focusedEditor?.viewModel?.selectedCell,
execute: async (notebook?: NotebookModel, cell?: NotebookCellModel) => {
const selectedCell = cell ?? this.notebookEditorWidgetService.focusedEditor?.viewModel?.selectedCell;
const activeNotebook = notebook ?? this.notebookEditorWidgetService.focusedEditor?.model;
if (!selectedCell || !activeNotebook) {
return;
}
const language = await this.languageQuickPickService.pickEditorLanguage(selectedCell.language);
if (!language?.value || language.value === 'autoDetect' || language.value.id === selectedCell.language) {
return;
}
const isMarkdownCell = selectedCell.cellKind === CellKind.Markup;
const isMarkdownLanguage = language.value.id === 'markdown';
if (isMarkdownLanguage) {
changeCellType(activeNotebook, selectedCell, CellKind.Markup, language.value.id);
} else {
if (isMarkdownCell) {
changeCellType(activeNotebook, selectedCell, CellKind.Code, language.value.id);
} else {
this.notebookEditorWidgetService.focusedEditor?.model?.applyEdits([{
editType: CellEditType.CellLanguage,
index: activeNotebook.cells.indexOf(selectedCell),
language: language.value.id
}], true);
}
}
}
});
commands.registerCommand(NotebookCellCommands.TOGGLE_LINE_NUMBERS, {
execute: () => {
const selectedCell = this.notebookEditorWidgetService.focusedEditor?.viewModel?.selectedCell;
if (selectedCell) {
const currentLineNumber = selectedCell.editorOptions?.lineNumbers;
selectedCell.editorOptions = { ...selectedCell.editorOptions, lineNumbers: !currentLineNumber || currentLineNumber === 'off' ? 'on' : 'off' };
}
}
});
}
protected editableCellCommandHandler(execute: (notebookModel: NotebookModel, cell: NotebookCellModel, output?: NotebookCellOutputModel) => void): CommandHandler {
return {
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?.viewModel?.selectedCell;
}
registerKeybindings(keybindings: KeybindingRegistry): void {
keybindings.registerKeybindings(
{
command: NotebookCellCommands.EDIT_COMMAND.id,
keybinding: 'Enter',
when: `!editorTextFocus && !inputFocus && ${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED}`,
},
{
command: NotebookCellCommands.STOP_EDIT_COMMAND.id,
keybinding: KeyCode.createKeyCode({ first: Key.ENTER, modifiers: [KeyModifier.Alt, KeyModifier.CtrlCmd] }).toString(),
when: `editorTextFocus && ${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_TYPE} == 'markdown'`,
},
{
command: NotebookCellCommands.STOP_EDIT_COMMAND.id,
keybinding: 'esc',
when: `editorTextFocus && ${NOTEBOOK_EDITOR_FOCUSED} && !suggestWidgetVisible`,
},
{
command: NotebookCellCommands.EXECUTE_SINGLE_CELL_COMMAND.id,
keybinding: KeyCode.createKeyCode({ first: Key.ENTER, modifiers: [KeyModifier.CtrlCmd] }).toString(),
when: `${NOTEBOOK_CELL_LIST_FOCUSED} && ${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_CELL_LIST_FOCUSED} && ${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED}`,
},
{
command: NotebookCellCommands.EXECUTE_SINGLE_CELL_AND_INSERT_BELOW_COMMAND.id,
keybinding: KeyCode.createKeyCode({ first: Key.ENTER, modifiers: [KeyModifier.Alt] }).toString(),
when: `${NOTEBOOK_CELL_LIST_FOCUSED} && ${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED}`,
},
{
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: `!editorTextFocus && ${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED} && ${NOTEBOOK_CELL_TYPE} == 'markdown'`,
},
{
command: NotebookCellCommands.TO_MARKDOWN_CELL_COMMAND.id,
keybinding: 'M',
when: `!editorTextFocus && ${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED} && ${NOTEBOOK_CELL_TYPE} == 'code'`,
},
{
command: NotebookCellCommands.SPLIT_CELL_COMMAND.id,
keybinding: KeyCode.createKeyCode({ first: Key.MINUS, modifiers: [KeyModifier.CtrlCmd, KeyModifier.Shift] }).toString(),
when: `editorTextFocus && ${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED}`,
}
);
}
}
export namespace NotebookCellActionContribution {
export const ACTION_MENU = ['notebook-cell-actions-menu'];
export const ADDITIONAL_ACTION_MENU = [...ACTION_MENU, 'more'];
export const CONTRIBUTED_CELL_ACTION_MENU = ['notebook/cell/title'];
export const CONTRIBUTED_CELL_EXECUTION_MENU = ['notebook/cell/execute'];
export const CODE_CELL_SIDEBAR_MENU = ['code-cell-sidebar-menu'];
export const OUTPUT_SIDEBAR_MENU = ['code-cell-output-sidebar-menu'];
export const ADDITIONAL_OUTPUT_SIDEBAR_MENU = [...OUTPUT_SIDEBAR_MENU, 'more'];
}

View File

@@ -0,0 +1,268 @@
// *****************************************************************************
// 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 { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
import { Color } from '@theia/core/lib/common/color';
import { injectable } from '@theia/core/shared/inversify';
@injectable()
export class NotebookColorContribution implements ColorContribution {
registerColors(colors: ColorRegistry): void {
colors.register(
{
id: 'notebook.cellBorderColor',
defaults: {
dark: Color.transparent('list.inactiveSelectionBackground', 1),
light: Color.transparent('list.inactiveSelectionBackground', 1),
hcDark: 'panel.border',
hcLight: 'panel.border'
},
description: 'The border color for notebook cells.'
},
{
id: 'notebook.focusedEditorBorder',
defaults: {
dark: 'focusBorder',
light: 'focusBorder',
hcDark: 'focusBorder',
hcLight: 'focusBorder'
},
description: 'The color of the notebook cell editor border.'
},
{
id: 'notebookStatusSuccessIcon.foreground',
defaults: {
dark: 'debugIcon.startForeground',
light: 'debugIcon.startForeground',
hcDark: 'debugIcon.startForeground',
hcLight: 'debugIcon.startForeground'
},
description: 'The error icon color of notebook cells in the cell status bar.'
},
{
id: 'notebookEditorOverviewRuler.runningCellForeground',
defaults: {
dark: 'debugIcon.startForeground',
light: 'debugIcon.startForeground',
hcDark: 'debugIcon.startForeground',
hcLight: 'debugIcon.startForeground'
},
description: 'The color of the running cell decoration in the notebook editor overview ruler.'
},
{
id: 'notebookStatusErrorIcon.foreground',
defaults: {
dark: 'errorForeground',
light: 'errorForeground',
hcDark: 'errorForeground',
hcLight: 'errorForeground'
},
description: 'The error icon color of notebook cells in the cell status bar.'
},
{
id: 'notebookStatusRunningIcon.foreground',
defaults: {
dark: 'foreground',
light: 'foreground',
hcDark: 'foreground',
hcLight: 'foreground'
},
description: 'The running icon color of notebook cells in the cell status bar.'
},
{
id: 'notebook.outputContainerBorderColor',
defaults: {
dark: undefined,
light: undefined,
hcDark: undefined,
hcLight: undefined
},
description: 'The border color of the notebook output container.'
},
{
id: 'notebook.outputContainerBackgroundColor',
defaults: {
dark: undefined,
light: undefined,
hcDark: undefined,
hcLight: undefined
},
description: 'The color of the notebook output container background.'
},
{
id: 'notebook.cellToolbarSeparator',
defaults: {
dark: Color.rgba(128, 128, 128, 0.35),
light: Color.rgba(128, 128, 128, 0.35),
hcDark: 'contrastBorder',
hcLight: 'contrastBorder'
},
description: 'The color of the separator in the cell bottom toolbar'
},
{
id: 'notebook.focusedCellBackground',
defaults: {
dark: undefined,
light: undefined,
hcDark: undefined,
hcLight: undefined
},
description: 'The background color of a cell when the cell is focused.'
},
{
id: 'notebook.selectedCellBackground',
defaults: {
dark: 'list.inactiveSelectionBackground',
light: 'list.inactiveSelectionBackground',
hcDark: undefined,
hcLight: undefined
},
description: 'The background color of a cell when the cell is selected.'
},
{
id: 'notebook.cellHoverBackground',
defaults: {
dark: Color.transparent('notebook.focusedCellBackground', 0.5),
light: Color.transparent('notebook.focusedCellBackground', 0.7),
hcDark: undefined,
hcLight: undefined
},
description: 'The background color of a cell when the cell is hovered.'
},
{
id: 'notebook.selectedCellBorder',
defaults: {
dark: 'notebook.cellBorderColor',
light: 'notebook.cellBorderColor',
hcDark: 'contrastBorder',
hcLight: 'contrastBorder'
},
description: "The color of the cell's top and bottom border when the cell is selected but not focused."
},
{
id: 'notebook.inactiveSelectedCellBorder',
defaults: {
dark: undefined,
light: undefined,
hcDark: 'focusBorder',
hcLight: 'focusBorder'
},
description: "The color of the cell's borders when multiple cells are selected."
},
{
id: 'notebook.focusedCellBorder',
defaults: {
dark: 'focusBorder',
light: 'focusBorder',
hcDark: 'focusBorder',
hcLight: 'focusBorder'
},
description: "The color of the cell's focus indicator borders when the cell is focused."
},
{
id: 'notebook.inactiveFocusedCellBorder',
defaults: {
dark: 'notebook.cellBorderColor',
light: 'notebook.cellBorderColor',
hcDark: 'notebook.cellBorderColor',
hcLight: 'notebook.cellBorderColor'
},
description: "The color of the cell's top and bottom border when a cell is focused while the primary focus is outside of the editor."
},
{
id: 'notebook.cellStatusBarItemHoverBackground',
defaults: {
dark: Color.rgba(0, 0, 0, 0.08),
light: Color.rgba(255, 255, 255, 0.15),
hcDark: Color.rgba(0, 0, 0, 0.08),
hcLight: Color.rgba(255, 255, 255, 0.15)
},
description: 'The background color of notebook cell status bar items.'
},
{
id: 'notebook.cellInsertionIndicator',
defaults: {
dark: 'focusBorder',
light: 'focusBorder',
hcDark: 'focusBorder',
hcLight: undefined
},
description: 'Notebook background color.'
},
{
id: 'notebookScrollbarSlider.background',
defaults: {
dark: 'scrollbarSlider.background',
light: 'scrollbarSlider.background',
hcDark: 'scrollbarSlider.background',
hcLight: 'scrollbarSlider.background'
},
description: 'Notebook scrollbar slider background color.'
},
{
id: 'notebookScrollbarSlider.hoverBackground',
defaults: {
dark: 'scrollbarSlider.hoverBackground',
light: 'scrollbarSlider.hoverBackground',
hcDark: 'scrollbarSlider.hoverBackground',
hcLight: 'scrollbarSlider.hoverBackground'
},
description: 'Notebook scrollbar slider background color when hovering.'
},
{
id: 'notebookScrollbarSlider.activeBackground',
defaults: {
dark: 'scrollbarSlider.activeBackground',
light: 'scrollbarSlider.activeBackground',
hcDark: 'scrollbarSlider.activeBackground',
hcLight: 'scrollbarSlider.activeBackground'
},
description: 'Notebook scrollbar slider background color when clicked on.'
},
{
id: 'notebook.symbolHighlightBackground',
defaults: {
dark: Color.rgba(255, 255, 255, 0.04),
light: Color.rgba(253, 255, 0, 0.2),
hcDark: undefined,
hcLight: undefined
},
description: 'Background color of highlighted cell'
},
{
id: 'notebook.cellEditorBackground',
defaults: {
dark: 'sideBar.background',
light: 'sideBar.background',
hcDark: undefined,
hcLight: undefined
},
description: 'Cell editor background color.'
},
{
id: 'notebook.editorBackground',
defaults: {
dark: 'editorPane.background',
light: 'editorPane.background',
hcDark: undefined,
hcLight: undefined
},
description: 'Notebook background color.'
}
);
}
}

View File

@@ -0,0 +1,113 @@
// *****************************************************************************
// 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
export type NotebookCellExecutionStateContext = 'idle' | 'pending' | 'executing' | 'succeeded' | 'failed';
/**
* Context Keys for the Notebook Editor as defined by vscode in https://github.com/microsoft/vscode/blob/main/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts
*/
export const HAS_OPENED_NOTEBOOK = 'userHasOpenedNotebook';
export const KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED = 'notebookFindWidgetFocused';
export const NOTEBOOK_EDITOR_FOCUSED = 'notebookEditorFocused';
export const NOTEBOOK_CELL_LIST_FOCUSED = 'notebookCellListFocused';
export const NOTEBOOK_OUTPUT_FOCUSED = 'notebookOutputFocused';
export const NOTEBOOK_OUTPUT_INPUT_FOCUSED = 'notebookOutputInputFocused';
export const NOTEBOOK_EDITOR_EDITABLE = 'notebookEditable';
export const NOTEBOOK_HAS_RUNNING_CELL = 'notebookHasRunningCell';
export const NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON = 'notebookUseConsolidatedOutputButton';
export const NOTEBOOK_BREAKPOINT_MARGIN_ACTIVE = 'notebookBreakpointMargin';
export const NOTEBOOK_CELL_TOOLBAR_LOCATION = 'notebookCellToolbarLocation';
export const NOTEBOOK_CURSOR_NAVIGATION_MODE = 'notebookCursorNavigationMode';
export const NOTEBOOK_LAST_CELL_FAILED = 'notebookLastCellFailed';
export const NOTEBOOK_VIEW_TYPE = 'notebookType';
export const NOTEBOOK_CELL_TYPE = 'notebookCellType';
export const NOTEBOOK_CELL_EDITABLE = 'notebookCellEditable';
export const NOTEBOOK_CELL_FOCUSED = 'notebookCellFocused';
export const NOTEBOOK_CELL_EDITOR_FOCUSED = 'notebookCellEditorFocused';
export const NOTEBOOK_CELL_MARKDOWN_EDIT_MODE = 'notebookCellMarkdownEditMode';
export const NOTEBOOK_CELL_LINE_NUMBERS = 'notebookCellLineNumbers';
export const NOTEBOOK_CELL_EXECUTION_STATE = 'notebookCellExecutionState';
export const NOTEBOOK_CELL_EXECUTING = 'notebookCellExecuting';
export const NOTEBOOK_CELL_HAS_OUTPUTS = 'notebookCellHasOutputs';
export const NOTEBOOK_CELL_INPUT_COLLAPSED = 'notebookCellInputIsCollapsed';
export const NOTEBOOK_CELL_OUTPUT_COLLAPSED = 'notebookCellOutputIsCollapsed';
export const NOTEBOOK_CELL_RESOURCE = 'notebookCellResource';
export const NOTEBOOK_KERNEL = 'notebookKernel';
export const NOTEBOOK_KERNEL_COUNT = 'notebookKernelCount';
export const NOTEBOOK_KERNEL_SOURCE_COUNT = 'notebookKernelSourceCount';
export const NOTEBOOK_KERNEL_SELECTED = 'notebookKernelSelected';
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);
service.createKey(KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, false);
// // Is Notebook
// export const NOTEBOOK_IS_ACTIVE_EDITOR = ContextKeyExpr.equals('activeEditor', NOTEBOOK_EDITOR_ID);
// export const INTERACTIVE_WINDOW_IS_ACTIVE_EDITOR = ContextKeyExpr.equals('activeEditor', INTERACTIVE_WINDOW_EDITOR_ID);
// Editor keys
service.createKey(NOTEBOOK_EDITOR_FOCUSED, false);
service.createKey(NOTEBOOK_CELL_LIST_FOCUSED, false);
service.createKey(NOTEBOOK_OUTPUT_FOCUSED, false);
service.createKey(NOTEBOOK_OUTPUT_INPUT_FOCUSED, false);
service.createKey(NOTEBOOK_EDITOR_EDITABLE, true);
service.createKey(NOTEBOOK_HAS_RUNNING_CELL, false);
service.createKey(NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON, false);
service.createKey(NOTEBOOK_BREAKPOINT_MARGIN_ACTIVE, false);
service.createKey(NOTEBOOK_CELL_TOOLBAR_LOCATION, 'left');
service.createKey(NOTEBOOK_CURSOR_NAVIGATION_MODE, false);
service.createKey(NOTEBOOK_LAST_CELL_FAILED, false);
// Cell keys
service.createKey(NOTEBOOK_VIEW_TYPE, undefined);
service.createKey(NOTEBOOK_CELL_TYPE, undefined);
service.createKey(NOTEBOOK_CELL_EDITABLE, false);
service.createKey(NOTEBOOK_CELL_FOCUSED, false);
service.createKey(NOTEBOOK_CELL_EDITOR_FOCUSED, false);
service.createKey(NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, false);
service.createKey(NOTEBOOK_CELL_LINE_NUMBERS, 'inherit');
service.createKey(NOTEBOOK_CELL_EXECUTION_STATE, undefined);
service.createKey(NOTEBOOK_CELL_EXECUTING, false);
service.createKey(NOTEBOOK_CELL_HAS_OUTPUTS, false);
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);
service.createKey(NOTEBOOK_KERNEL_COUNT, 0);
service.createKey(NOTEBOOK_KERNEL_SOURCE_COUNT, 0);
service.createKey(NOTEBOOK_KERNEL_SELECTED, false);
service.createKey(NOTEBOOK_INTERRUPTIBLE_KERNEL, false);
service.createKey(NOTEBOOK_MISSING_KERNEL_EXTENSION, false);
service.createKey(NOTEBOOK_HAS_OUTPUTS, false);
}
}

View File

@@ -0,0 +1,87 @@
// *****************************************************************************
// 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 { codicon, LabelProvider, LabelProviderContribution } from '@theia/core/lib/browser';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CellKind, CellUri } from '../../common';
import { NotebookService } from '../service/notebook-service';
import { NotebookCellOutlineNode } from './notebook-outline-contribution';
import MarkdownIt = require('markdown-it');
type Token = MarkdownIt.Token;
import markdownit = require('@theia/core/shared/markdown-it');
import * as markdownitemoji from '@theia/core/shared/markdown-it-emoji';
import { NotebookCellModel } from '../view-model/notebook-cell-model';
import { URI } from '@theia/core';
@injectable()
export class NotebookLabelProviderContribution implements LabelProviderContribution {
@inject(NotebookService)
protected readonly notebookService: NotebookService;
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
protected markdownIt = markdownit().use(markdownitemoji.full);
canHandle(element: object): number {
if (NotebookCellOutlineNode.is(element)) {
return 200;
}
return 0;
}
getIcon(element: NotebookCellOutlineNode): string {
const cell = this.findCellByUri(element.uri);
if (cell) {
return cell.cellKind === CellKind.Markup ? codicon('markdown') : codicon('code');
}
return '';
}
getName(element: NotebookCellOutlineNode): string {
const cell = this.findCellByUri(element.uri);
if (cell) {
return cell.cellKind === CellKind.Code ?
cell.text.split('\n')[0] :
this.extractPlaintext(this.markdownIt.parse(cell.text.split('\n')[0], {}));
}
return '';
}
getLongName(element: NotebookCellOutlineNode): string {
const cell = this.findCellByUri(element.uri);
if (cell) {
return cell.cellKind === CellKind.Code ?
cell.text.split('\n')[0] :
this.extractPlaintext(this.markdownIt.parse(cell.text.split('\n')[0], {}));
}
return '';
}
extractPlaintext(parsedMarkdown: Token[]): string {
return parsedMarkdown.map(token => token.children ? this.extractPlaintext(token.children) : token.content).join('');
}
findCellByUri(uri: URI): NotebookCellModel | undefined {
const parsed = CellUri.parse(uri);
if (parsed) {
return this.notebookService.getNotebookEditorModel(parsed.notebook)?.cells.find(cell => cell.handle === parsed?.handle);
}
return undefined;
}
}

View File

@@ -0,0 +1,116 @@
// *****************************************************************************
// 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 { codicon, FrontendApplicationContribution, LabelProvider, TreeNode } from '@theia/core/lib/browser';
import { NotebookEditorWidgetService } from '../service/notebook-editor-widget-service';
import { OutlineViewService } from '@theia/outline-view/lib/browser/outline-view-service';
import { NotebookModel } from '../view-model/notebook-model';
import { OutlineSymbolInformationNode } from '@theia/outline-view/lib/browser/outline-view-widget';
import { NotebookEditorWidget } from '../notebook-editor-widget';
import { DisposableCollection, isObject, URI } from '@theia/core';
import { CellKind, CellUri } from '../../common';
import { NotebookService } from '../service/notebook-service';
import { NotebookViewModel } from '../view-model/notebook-view-model';
export interface NotebookCellOutlineNode extends OutlineSymbolInformationNode {
uri: URI;
}
export namespace NotebookCellOutlineNode {
export function is(element: object): element is NotebookCellOutlineNode {
return TreeNode.is(element)
&& OutlineSymbolInformationNode.is(element)
&& isObject<NotebookCellOutlineNode>(element)
&& element.uri instanceof URI
&& element.uri.scheme === CellUri.cellUriScheme;
}
}
@injectable()
export class NotebookOutlineContribution implements FrontendApplicationContribution {
@inject(NotebookEditorWidgetService)
protected readonly notebookEditorWidgetService: NotebookEditorWidgetService;
@inject(OutlineViewService)
protected readonly outlineViewService: OutlineViewService;
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
@inject(NotebookService)
protected readonly notebookService: NotebookService;
protected currentEditor?: NotebookEditorWidget;
protected editorListeners: DisposableCollection = new DisposableCollection();
protected editorModelListeners: DisposableCollection = new DisposableCollection();
onStart(): void {
this.notebookEditorWidgetService.onDidChangeFocusedEditor(editor => this.updateOutline(editor));
this.outlineViewService.onDidSelect(node => this.selectCell(node));
this.outlineViewService.onDidTapNode(node => this.selectCell(node));
}
protected async updateOutline(editor: NotebookEditorWidget | undefined): Promise<void> {
if (editor && !editor.isDisposed) {
await editor.ready;
this.currentEditor = editor;
this.editorListeners.dispose();
this.editorListeners.push(editor.onDidChangeVisibility(() => {
if (this.currentEditor === editor && !editor.isVisible) {
this.outlineViewService.publish([]);
}
}));
if (editor.model) {
this.editorModelListeners.dispose();
this.editorModelListeners.push(editor.viewModel.onDidChangeSelectedCell(() => {
if (editor === this.currentEditor) {
this.updateOutline(editor);
}
}));
const roots = editor && editor.model && await this.createRoots(editor.model, editor.viewModel);
this.outlineViewService.publish(roots || []);
}
}
}
protected async createRoots(model: NotebookModel, viewModel: NotebookViewModel): Promise<OutlineSymbolInformationNode[] | undefined> {
return model.cells.map(cell => ({
id: cell.uri.toString(),
iconClass: cell.cellKind === CellKind.Markup ? codicon('markdown') : codicon('code'),
parent: undefined,
children: [],
selected: viewModel.selectedCell === cell,
expanded: false,
uri: cell.uri,
} as NotebookCellOutlineNode));
}
protected selectCell(node: object): void {
if (NotebookCellOutlineNode.is(node)) {
const parsed = CellUri.parse(node.uri);
const model = parsed && this.notebookService.getNotebookEditorModel(parsed.notebook);
const viewModel = this.currentEditor?.viewModel;
const cell = model?.cells.find(c => c.handle === parsed?.handle);
if (model && cell) {
viewModel?.setSelectedCell(cell);
}
}
}
}

View File

@@ -0,0 +1,80 @@
// *****************************************************************************
// 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 { Command, CommandContribution, CommandRegistry } from '@theia/core';
import { inject, injectable } from '@theia/core/shared/inversify';
import { NotebookEditorWidgetService } from '../service/notebook-editor-widget-service';
import { CellOutput, CellUri } from '../../common';
import { NotebookCellModel } from '../view-model/notebook-cell-model';
import { EditorManager } from '@theia/editor/lib/browser';
export namespace NotebookOutputCommands {
export const ENABLE_SCROLLING: Command = {
id: 'cellOutput.enableScrolling',
};
export const OPEN_LARGE_OUTPUT: Command = {
id: 'workbench.action.openLargeOutput'
};
}
@injectable()
export class NotebookOutputActionContribution implements CommandContribution {
@inject(NotebookEditorWidgetService)
protected readonly notebookEditorService: NotebookEditorWidgetService;
@inject(EditorManager)
protected readonly editorManager: EditorManager;
registerCommands(commands: CommandRegistry): void {
commands.registerCommand(NotebookOutputCommands.ENABLE_SCROLLING, {
execute: outputId => {
const [cell, output] = this.findOutputAndCell(outputId) ?? [];
if (cell && output?.metadata) {
output.metadata['scrollable'] = true;
cell.restartOutputRenderer(output.outputId);
}
}
});
commands.registerCommand(NotebookOutputCommands.OPEN_LARGE_OUTPUT, {
execute: outputId => {
const [cell, output] = this.findOutputAndCell(outputId) ?? [];
if (cell && output) {
this.editorManager.open(CellUri.generateCellOutputUri(CellUri.parse(cell.uri)!.notebook, output.outputId));
}
}
});
}
protected findOutputAndCell(output: string): [NotebookCellModel, CellOutput] | undefined {
const model = this.notebookEditorService.focusedEditor?.model;
if (!model) {
return undefined;
}
const outputId = output.slice(0, output.lastIndexOf('-'));
for (const cell of model.cells) {
for (const outputModel of cell.outputs) {
if (outputModel.outputId === outputId) {
return [cell, outputModel];
}
}
}
}
}

View File

@@ -0,0 +1,64 @@
// *****************************************************************************
// 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 { injectable } from '@theia/core/shared/inversify';
import { StatusBar, StatusBarAlignment, Widget, WidgetStatusBarContribution } from '@theia/core/lib/browser';
import { Disposable } from '@theia/core/lib/common';
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 WidgetStatusBarContribution<NotebookEditorWidget> {
protected onDeactivate: Disposable | undefined;
canHandle(widget: Widget): widget is NotebookEditorWidget {
return widget instanceof NotebookEditorWidget;
}
activate(statusBar: StatusBar, widget: NotebookEditorWidget): void {
this.onDeactivate = widget.viewModel.onDidChangeSelectedCell(() => {
this.updateStatusbar(statusBar, widget);
});
this.updateStatusbar(statusBar, widget);
}
deactivate(statusBar: StatusBar): void {
this.onDeactivate?.dispose();
this.updateStatusbar(statusBar);
}
protected async updateStatusbar(statusBar: StatusBar, editor?: NotebookEditorWidget): Promise<void> {
const model = await editor?.ready;
if (!model || model.cells.length === 0 || !editor?.viewModel.selectedCell) {
statusBar.removeElement(NOTEBOOK_CELL_SELECTION_STATUS_BAR_ID);
return;
}
const selectedCellIndex = model.cells.indexOf(editor.viewModel.selectedCell) + 1;
statusBar.setElement(NOTEBOOK_CELL_SELECTION_STATUS_BAR_ID, {
text: nls.localizeByDefault('Cell {0} of {1}', selectedCellIndex, model.cells.length),
alignment: StatusBarAlignment.RIGHT,
priority: 100,
command: NotebookCommands.CENTER_ACTIVE_CELL.id,
arguments: [editor]
});
}
}

View File

@@ -0,0 +1,41 @@
// *****************************************************************************
// 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 { ApplicationShell, UndoRedoHandler } from '@theia/core/lib/browser';
import { NotebookEditorWidget } from '../notebook-editor-widget';
@injectable()
export class NotebookUndoRedoHandler implements UndoRedoHandler<NotebookEditorWidget> {
@inject(ApplicationShell)
protected readonly applicationShell: ApplicationShell;
priority = 200;
select(): NotebookEditorWidget | undefined {
const current = this.applicationShell.currentWidget;
if (current instanceof NotebookEditorWidget) {
return current;
}
return undefined;
}
undo(item: NotebookEditorWidget): void {
item.undo();
}
redo(item: NotebookEditorWidget): void {
item.redo();
}
}

View File

@@ -0,0 +1,29 @@
// *****************************************************************************
// 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
// *****************************************************************************
export * from './notebook-type-registry';
export * from './notebook-renderer-registry';
export * from './notebook-editor-widget';
export * from './service/notebook-service';
export * from './service/notebook-editor-widget-service';
export * from './service/notebook-kernel-service';
export * from './service/notebook-execution-state-service';
export * from './service/notebook-model-resolver-service';
export * from './service/notebook-renderer-messaging-service';
export * from './service/notebook-cell-editor-service';
export * from './renderers/cell-output-webview';
export * from './notebook-types';
export * from './notebook-editor-split-contribution';

View File

@@ -0,0 +1,52 @@
// *****************************************************************************
// 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 { URI, MaybePromise } from '@theia/core';
import { OpenHandler, OpenerOptions } from '@theia/core/lib/browser';
import { inject, injectable } from '@theia/core/shared/inversify';
import { NotebookEditorWidgetService } from './service/notebook-editor-widget-service';
import { CellUri } from '../common';
@injectable()
export class NotebookCellOpenHandler implements OpenHandler {
@inject(NotebookEditorWidgetService)
protected readonly notebookEditorWidgetService: NotebookEditorWidgetService;
id: string = 'notebook-cell-opener';
canHandle(uri: URI, options?: OpenerOptions | undefined): MaybePromise<number> {
return uri.scheme === CellUri.cellUriScheme ? 200 : 0;
}
open(uri: URI, options?: OpenerOptions | undefined): undefined {
const params = new URLSearchParams(uri.query);
const executionCountParam = params.get('execution_count');
const lineParam = params.get('line');
if (!executionCountParam || !lineParam) {
console.error('Invalid vscode-notebook-cell URI: missing execution_count or line parameter', uri.toString(true));
return;
}
const executionCount = parseInt(executionCountParam);
const cell = this.notebookEditorWidgetService.currentEditor?.model?.cells
.find(c => c.metadata.execution_count === executionCount);
this.notebookEditorWidgetService.currentEditor?.viewModel.cellViewModels.get(cell?.handle ?? -1)?.requestFocusEditor();
}
}

View File

@@ -0,0 +1,130 @@
// *****************************************************************************
// 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 { Event, Emitter, Resource, ResourceReadOptions, ResourceResolver, URI } from '@theia/core';
import { inject, injectable } from '@theia/core/shared/inversify';
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
import { CellUri } from '../common';
import { NotebookService } from './service/notebook-service';
import { NotebookCellModel } from './view-model/notebook-cell-model';
import { NotebookModel } from './view-model/notebook-model';
export class NotebookCellResource implements Resource {
protected readonly onDidChangeContentsEmitter = new Emitter<void>();
get onDidChangeContents(): Event<void> {
return this.onDidChangeContentsEmitter.event;
}
get onDidChangeReadOnly(): Event<boolean | MarkdownString> | undefined {
return this.notebook.onDidChangeReadOnly;
}
get readOnly(): boolean | MarkdownString | undefined {
return this.notebook.readOnly;
}
protected cell: NotebookCellModel;
protected notebook: NotebookModel;
uri: URI;
constructor(uri: URI, notebook: NotebookModel, cell: NotebookCellModel) {
this.uri = uri;
this.notebook = notebook;
this.cell = cell;
}
readContents(options?: ResourceReadOptions | undefined): Promise<string> {
return Promise.resolve(this.cell.source);
}
dispose(): void {
this.onDidChangeContentsEmitter.dispose();
}
}
@injectable()
export class NotebookCellResourceResolver implements ResourceResolver {
@inject(NotebookService)
protected readonly notebookService: NotebookService;
async resolve(uri: URI): Promise<Resource> {
if (uri.scheme !== CellUri.cellUriScheme) {
throw new Error(`Cannot resolve cell uri with scheme '${uri.scheme}'`);
}
const parsedUri = CellUri.parse(uri);
if (!parsedUri) {
throw new Error(`Cannot parse uri '${uri.toString()}'`);
}
const notebookModel = this.notebookService.getNotebookEditorModel(parsedUri.notebook);
if (!notebookModel) {
throw new Error(`No notebook found for uri '${parsedUri.notebook}'`);
}
const notebookCellModel = notebookModel.cells.find(cell => cell.handle === parsedUri.handle);
if (!notebookCellModel) {
throw new Error(`No cell found with handle '${parsedUri.handle}' in '${parsedUri.notebook}'`);
}
return new NotebookCellResource(uri, notebookModel, notebookCellModel);
}
}
@injectable()
export class NotebookOutputResourceResolver implements ResourceResolver {
@inject(NotebookService)
protected readonly notebookService: NotebookService;
async resolve(uri: URI): Promise<Resource> {
if (uri.scheme !== CellUri.outputUriScheme) {
throw new Error(`Cannot resolve output uri with scheme '${uri.scheme}'`);
}
const parsedUri = CellUri.parseCellOutputUri(uri);
if (!parsedUri) {
throw new Error(`Cannot parse uri '${uri.toString()}'`);
}
const notebookModel = this.notebookService.getNotebookEditorModel(parsedUri.notebook);
if (!notebookModel) {
throw new Error(`No notebook found for uri '${parsedUri.notebook}'`);
}
const ouputModel = notebookModel.cells.flatMap(cell => cell.outputs).find(output => output.outputId === parsedUri.outputId);
if (!ouputModel) {
throw new Error(`No output found with id '${parsedUri.outputId}' in '${parsedUri.notebook}'`);
}
return {
uri: uri,
dispose: () => { },
readContents: async () => ouputModel.outputs[0].data.toString(),
readOnly: true,
};
}
}

View File

@@ -0,0 +1,51 @@
// *****************************************************************************
// Copyright (C) 2025 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 { Widget, DockLayout } from '@theia/core/lib/browser';
import { SplitEditorContribution } from '@theia/editor/lib/browser/split-editor-contribution';
import { NotebookEditorWidget } from './notebook-editor-widget';
import { NotebookOpenHandler } from './notebook-open-handler';
/**
* Implementation of SplitEditorContribution for notebook editors (NotebookEditorWidget).
* Delegates to NotebookOpenHandler.openToSide which handles counter management for splits.
*/
@injectable()
export class NotebookEditorSplitContribution implements SplitEditorContribution<NotebookEditorWidget> {
@inject(NotebookOpenHandler)
protected readonly notebookOpenHandler: NotebookOpenHandler;
canHandle(widget: Widget): number {
return widget instanceof NotebookEditorWidget ? 100 : 0;
}
async split(widget: NotebookEditorWidget, splitMode: DockLayout.InsertMode): Promise<NotebookEditorWidget | undefined> {
const uri = widget.getResourceUri();
if (!uri) {
return undefined;
}
const newNotebook = await this.notebookOpenHandler.openToSide(uri, {
notebookType: widget.notebookType,
widgetOptions: { mode: splitMode, ref: widget }
});
return newNotebook;
}
}

View File

@@ -0,0 +1,103 @@
// *****************************************************************************
// 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 { nls, URI } from '@theia/core';
import { WidgetFactory, NavigatableWidgetOptions, LabelProvider } from '@theia/core/lib/browser';
import { inject, injectable } from '@theia/core/shared/inversify';
import { NotebookEditorWidget, NotebookEditorWidgetContainerFactory, NotebookEditorProps, NOTEBOOK_EDITOR_ID_PREFIX } from './notebook-editor-widget';
import { NotebookService } from './service/notebook-service';
import { NotebookModelResolverService } from './service/notebook-model-resolver-service';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { NotebookModel } from './view-model/notebook-model';
export interface NotebookEditorWidgetOptions extends NavigatableWidgetOptions {
notebookType: string;
}
@injectable()
export class NotebookEditorWidgetFactory implements WidgetFactory {
static createID(uri: URI, counter?: number): string {
return NOTEBOOK_EDITOR_ID_PREFIX
+ uri.toString()
+ (counter !== undefined ? `:${counter}` : '');
}
readonly id: string = NotebookEditorWidget.ID;
@inject(NotebookService)
protected readonly notebookService: NotebookService;
@inject(NotebookModelResolverService)
protected readonly notebookModelResolver: NotebookModelResolverService;
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
@inject(NotebookEditorWidgetContainerFactory)
protected readonly createNotebookEditorWidget: (props: NotebookEditorProps) => NotebookEditorWidget;
async createWidget(options?: NotebookEditorWidgetOptions): Promise<NotebookEditorWidget> {
if (!options) {
throw new Error('no options found for widget. Need at least uri and notebookType');
}
const uri = new URI(options.uri);
await this.notebookService.willOpenNotebook(options.notebookType);
const editor = await this.createEditor(uri, options.notebookType);
// Set the widget ID with counter to support multiple instances
editor.id = NotebookEditorWidgetFactory.createID(uri, options.counter);
this.setLabels(editor, uri);
const labelListener = this.labelProvider.onDidChange(event => {
if (event.affects(uri)) {
this.setLabels(editor, uri);
}
});
editor.onDidDispose(() => labelListener.dispose());
return editor;
}
protected async createEditor(uri: URI, notebookType: string): Promise<NotebookEditorWidget> {
const notebookData = new Deferred<NotebookModel>();
const resolverError = new Deferred<string>();
this.notebookModelResolver.resolve(uri, notebookType).then(model => {
notebookData.resolve(model);
}).catch((reason: Error) => {
resolverError.resolve(reason.message);
});
return this.createNotebookEditorWidget({
uri,
notebookType,
notebookData: notebookData.promise,
error: resolverError.promise
});
}
protected setLabels(editor: NotebookEditorWidget, uri: URI): void {
editor.title.caption = uri.path.fsPath();
if (editor.model?.readOnly) {
editor.title.caption += `${nls.localizeByDefault('Read-only')}`;
}
const icon = this.labelProvider.getIcon(uri);
editor.title.label = this.labelProvider.getName(uri);
editor.title.iconClass = icon + ' file-icon';
}
}

View File

@@ -0,0 +1,381 @@
// *****************************************************************************
// 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 * as React from '@theia/core/shared/react';
import { CommandRegistry, MenuModelRegistry, URI, nls } from '@theia/core';
import { ReactWidget, Navigatable, SaveableSource, Message, DelegatingSaveable, lock, unlock, animationFrame, codicon } from '@theia/core/lib/browser';
import { ReactNode } from '@theia/core/shared/react';
import { CellKind, NotebookCellsChangeType } from '../common';
import { CellRenderer as CellRenderer, NotebookCellListView } from './view/notebook-cell-list-view';
import { NotebookCodeCellRenderer } from './view/notebook-code-cell-view';
import { NotebookMarkdownCellRenderer } from './view/notebook-markdown-cell-view';
import { NotebookModel } from './view-model/notebook-model';
import { NotebookCellToolbarFactory } from './view/notebook-cell-toolbar-factory';
import { inject, injectable, interfaces, postConstruct } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/shared/vscode-languageserver-protocol';
import { NotebookEditorWidgetService } from './service/notebook-editor-widget-service';
import { NotebookMainToolbarRenderer } from './view/notebook-main-toolbar';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
import { NotebookContextManager } from './service/notebook-context-manager';
import { NotebookViewportService } from './view/notebook-viewport-service';
import { NotebookCellCommands } from './contributions/notebook-cell-actions-contribution';
import { NotebookFindWidget } from './view/notebook-find-widget';
import debounce = require('@theia/core/shared/lodash.debounce');
import { CellOutputWebview, CellOutputWebviewFactory } from './renderers/cell-output-webview';
import { NotebookCellOutputModel } from './view-model/notebook-cell-output-model';
import { NotebookViewModel } from './view-model/notebook-view-model';
const PerfectScrollbar = require('react-perfect-scrollbar');
export const NotebookEditorWidgetContainerFactory = Symbol('NotebookEditorWidgetContainerFactory');
export function createNotebookEditorWidgetContainer(parent: interfaces.Container, props: NotebookEditorProps): interfaces.Container {
const child = parent.createChild();
child.bind(NotebookEditorProps).toConstantValue(props);
const cellOutputWebviewFactory: CellOutputWebviewFactory = parent.get(CellOutputWebviewFactory);
child.bind(CellOutputWebview).toConstantValue(cellOutputWebviewFactory());
child.bind(NotebookViewModel).toSelf().inSingletonScope();
child.bind(NotebookContextManager).toSelf().inSingletonScope();
child.bind(NotebookMainToolbarRenderer).toSelf().inSingletonScope();
child.bind(NotebookCellToolbarFactory).toSelf().inSingletonScope();
child.bind(NotebookCodeCellRenderer).toSelf().inSingletonScope();
child.bind(NotebookMarkdownCellRenderer).toSelf().inSingletonScope();
child.bind(NotebookViewportService).toSelf().inSingletonScope();
child.bind(NotebookEditorWidget).toSelf();
return child;
}
export const NotebookEditorProps = Symbol('NotebookEditorProps');
interface RenderMessage {
rendererId: string;
message: unknown;
}
export interface NotebookEditorProps {
uri: URI,
readonly notebookType: string,
notebookData: Promise<NotebookModel>
error?: Promise<string>
}
export const NOTEBOOK_EDITOR_ID_PREFIX = 'notebook:';
@injectable()
export class NotebookEditorWidget extends ReactWidget implements Navigatable, SaveableSource {
static readonly ID = 'notebook';
readonly saveable = new DelegatingSaveable();
@inject(NotebookCellToolbarFactory)
protected readonly cellToolbarFactory: NotebookCellToolbarFactory;
@inject(CommandRegistry)
protected commandRegistry: CommandRegistry;
@inject(MenuModelRegistry)
protected menuRegistry: MenuModelRegistry;
@inject(NotebookEditorWidgetService)
protected notebookEditorService: NotebookEditorWidgetService;
@inject(NotebookMainToolbarRenderer)
protected notebookMainToolbarRenderer: NotebookMainToolbarRenderer;
@inject(NotebookContextManager)
protected notebookContextManager: NotebookContextManager;
@inject(NotebookCodeCellRenderer)
protected codeCellRenderer: NotebookCodeCellRenderer;
@inject(NotebookMarkdownCellRenderer)
protected markdownCellRenderer: NotebookMarkdownCellRenderer;
@inject(NotebookEditorProps)
protected readonly props: NotebookEditorProps;
@inject(NotebookViewportService)
protected readonly viewportService: NotebookViewportService;
@inject(CellOutputWebview)
protected readonly cellOutputWebview: CellOutputWebview;
@inject(NotebookViewModel)
protected readonly _viewModel: NotebookViewModel;
protected readonly onDidChangeModelEmitter = new Emitter<void>();
readonly onDidChangeModel = this.onDidChangeModelEmitter.event;
protected readonly onDidChangeReadOnlyEmitter = new Emitter<boolean | MarkdownString>();
readonly onDidChangeReadOnly = this.onDidChangeReadOnlyEmitter.event;
protected readonly onPostKernelMessageEmitter = new Emitter<unknown>();
readonly onPostKernelMessage = this.onPostKernelMessageEmitter.event;
protected readonly onDidPostKernelMessageEmitter = new Emitter<unknown>();
readonly onDidPostKernelMessage = this.onDidPostKernelMessageEmitter.event;
protected readonly onPostRendererMessageEmitter = new Emitter<RenderMessage>();
readonly onPostRendererMessage = this.onPostRendererMessageEmitter.event;
protected readonly onDidReceiveKernelMessageEmitter = new Emitter<unknown>();
readonly onDidReceiveKernelMessage = this.onDidReceiveKernelMessageEmitter.event;
protected readonly onDidChangeOutputInputFocusEmitter = new Emitter<boolean>();
readonly onDidChangeOutputInputFocus = this.onDidChangeOutputInputFocusEmitter.event;
protected readonly renderers = new Map<CellKind, CellRenderer>();
protected _model?: NotebookModel;
protected error?: string;
protected _ready: Deferred<NotebookModel> = new Deferred();
protected _findWidgetVisible = false;
protected _findWidgetRef = React.createRef<NotebookFindWidget>();
protected scrollBarRef = React.createRef<{ updateScroll(): void }>();
protected debounceFind = debounce(() => {
this._findWidgetRef.current?.search({});
}, 30, {
trailing: true,
maxWait: 100,
leading: false
});
get notebookType(): string {
return this.props.notebookType;
}
get ready(): Promise<NotebookModel> {
return this._ready.promise;
}
get model(): NotebookModel | undefined {
return this._model;
}
get viewModel(): NotebookViewModel {
return this._viewModel;
}
@postConstruct()
protected init(): void {
// ID is set by NotebookEditorWidgetFactory to include counter for multiple instances
if (!this.id) {
this.id = NOTEBOOK_EDITOR_ID_PREFIX + this.props.uri.toString();
}
this.scrollOptions = {
suppressScrollY: true
};
this.title.closable = true;
this.update();
this.toDispose.push(this.onDidChangeModelEmitter);
this.toDispose.push(this.onDidChangeReadOnlyEmitter);
this.renderers.set(CellKind.Markup, this.markdownCellRenderer);
this.renderers.set(CellKind.Code, this.codeCellRenderer);
this._ready.resolve(this.waitForData());
this.ready.then(model => {
if (model.cells.length === 1 && model.cells[0].source === '') {
this.commandRegistry.executeCommand(NotebookCellCommands.EDIT_COMMAND.id, model, model.cells[0]);
this.viewModel.setSelectedCell(model.cells[0]);
}
model.onDidChangeContent(changeEvents => {
const cellEvent = changeEvents.filter(event => event.kind === NotebookCellsChangeType.Move || event.kind === NotebookCellsChangeType.ModelChange);
if (cellEvent.length > 0) {
this.cellOutputWebview.cellsChanged(cellEvent);
}
});
});
this.props.error?.then(error => {
this.error = error;
this.update();
});
}
protected async waitForData(): Promise<NotebookModel> {
this._model = await this.props.notebookData;
this.viewModel.initDataModel(this._model);
this.cellOutputWebview.init(this._model, this);
this.saveable.delegate = this._model;
this.toDispose.push(this._model);
this.toDispose.push(this._model.onDidChangeContent(() => {
// Update the scroll bar content after the content has changed
// Wait one frame to ensure that the content has been rendered
animationFrame().then(() => this.scrollBarRef.current?.updateScroll());
}));
this.toDispose.push(this._model.onContentChanged(() => {
if (this._findWidgetVisible) {
this.debounceFind();
}
}));
this.toDispose.push(this._model.onDidChangeReadOnly(readOnly => {
if (readOnly) {
lock(this.title);
} else {
unlock(this.title);
}
this.onDidChangeReadOnlyEmitter.fire(readOnly);
this.update();
}));
if (this._model.readOnly) {
lock(this.title);
}
// Ensure that the model is loaded before adding the editor
this.notebookEditorService.addNotebookEditor(this);
this.viewModel.selectedCell = this._model.cells[0];
this.update();
this.notebookContextManager.init(this);
return this._model;
}
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
(this.node.getElementsByClassName('theia-notebook-main-container')[0] as HTMLDivElement)?.focus();
}
getResourceUri(): URI | undefined {
return this.props.uri;
}
createMoveToUri(resourceUri: URI): URI | undefined {
return this.model?.uri.withPath(resourceUri.path);
}
undo(): void {
this._model?.undo();
}
redo(): void {
this._model?.redo();
}
protected render(): ReactNode {
if (this._model) {
return <div className='theia-notebook-main-container' tabIndex={-1}>
<div className='theia-notebook-overlay'>
<NotebookFindWidget
ref={this._findWidgetRef}
hidden={!this._findWidgetVisible}
onClose={() => {
this._findWidgetVisible = false;
this._model?.findMatches({
activeFilters: [],
matchCase: false,
regex: false,
search: '',
wholeWord: false
});
this.update();
}}
onSearch={options => this._model?.findMatches(options) ?? []}
onReplace={(matches, replaceText) => this._model?.replaceAll(matches, replaceText)}
/>
</div>
{this.notebookMainToolbarRenderer.render(this._model, this.node)}
<div
className='theia-notebook-viewport'
ref={(ref: HTMLDivElement) => this.viewportService.viewportElement = ref}
>
<PerfectScrollbar className='theia-notebook-scroll-container'
ref={this.scrollBarRef}
onScrollY={(e: HTMLDivElement) => this.viewportService.onScroll(e)}>
<div className='theia-notebook-scroll-area'>
{this.cellOutputWebview.render()}
<NotebookCellListView renderers={this.renderers}
notebookModel={this._model}
notebookViewModel={this.viewModel}
notebookContext={this.notebookContextManager}
toolbarRenderer={this.cellToolbarFactory}
commandRegistry={this.commandRegistry}
menuRegistry={this.menuRegistry} />
</div>
</PerfectScrollbar>
</div>
</div>;
} else if (this.error) {
return <div className='theia-notebook-main-container error-message' tabIndex={-1}>
<span className={codicon('error')}></span>
<h3>{nls.localizeByDefault('The editor could not be opened because the file was not found.')}</h3>
</div>;
} else {
return <div className='theia-notebook-main-container' tabIndex={-1}>
<div className='theia-notebook-main-loading-indicator'></div>
</div>;
}
}
protected override onCloseRequest(msg: Message): void {
super.onCloseRequest(msg);
this.notebookEditorService.removeNotebookEditor(this);
}
requestOuputPresentationChange(cellHandle: number, output?: NotebookCellOutputModel): void {
if (output) {
this.cellOutputWebview.requestOutputPresentationUpdate(cellHandle, output);
}
}
postKernelMessage(message: unknown): void {
this.onDidPostKernelMessageEmitter.fire(message);
}
postRendererMessage(rendererId: string, message: unknown): void {
this.onPostRendererMessageEmitter.fire({ rendererId, message });
}
recieveKernelMessage(message: unknown): void {
this.onDidReceiveKernelMessageEmitter.fire(message);
}
outputInputFocusChanged(focused: boolean): void {
this.onDidChangeOutputInputFocusEmitter.fire(focused);
}
showFindWidget(): void {
if (!this._findWidgetVisible) {
this._findWidgetVisible = true;
this.update();
}
this._findWidgetRef.current?.focusSearch(this._model?.selectedText);
}
override dispose(): void {
this.cellOutputWebview.dispose();
this.notebookContextManager.dispose();
this.onDidChangeModelEmitter.dispose();
this.onDidPostKernelMessageEmitter.dispose();
this.onDidReceiveKernelMessageEmitter.dispose();
this.onPostRendererMessageEmitter.dispose();
this.onDidChangeOutputInputFocusEmitter.dispose();
this.viewportService.dispose();
this._model?.dispose();
super.dispose();
}
protected override onAfterShow(msg: Message): void {
super.onAfterShow(msg);
this.notebookEditorService.notebookEditorFocusChanged(this, true);
}
protected override onAfterHide(msg: Message): void {
super.onAfterHide(msg);
this.notebookEditorService.notebookEditorFocusChanged(this, false);
}
}

View File

@@ -0,0 +1,138 @@
// *****************************************************************************
// 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 '../../src/browser/style/index.css';
import { ContainerModule } from '@theia/core/shared/inversify';
import {
FrontendApplicationContribution, KeybindingContribution, LabelProviderContribution, OpenHandler, UndoRedoHandler, WidgetFactory, WidgetStatusBarContribution
} 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';
import { NotebookTypeRegistry } from './notebook-type-registry';
import { NotebookRendererRegistry } from './notebook-renderer-registry';
import { NotebookService } from './service/notebook-service';
import { NotebookEditorWidgetFactory } from './notebook-editor-widget-factory';
import { NotebookCellResourceResolver, NotebookOutputResourceResolver } from './notebook-cell-resource-resolver';
import { NotebookModelResolverService } from './service/notebook-model-resolver-service';
import { NotebookCellActionContribution } from './contributions/notebook-cell-actions-contribution';
import { createNotebookModelContainer, NotebookModel, NotebookModelFactory, NotebookModelProps, NotebookModelResolverServiceProxy } 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 { NotebookActionsContribution } from './contributions/notebook-actions-contribution';
import { NotebookExecutionService } from './service/notebook-execution-service';
import { NotebookExecutionStateService } from './service/notebook-execution-state-service';
import { NotebookKernelService } from './service/notebook-kernel-service';
import { NotebookKernelQuickPickService } from './service/notebook-kernel-quick-pick-service';
import { NotebookKernelHistoryService } from './service/notebook-kernel-history-service';
import { NotebookEditorWidgetService } from './service/notebook-editor-widget-service';
import { NotebookRendererMessagingService } from './service/notebook-renderer-messaging-service';
import { NotebookColorContribution } from './contributions/notebook-color-contribution';
import { NotebookMonacoEditorModelFilter, NotebookMonacoTextModelService } from './service/notebook-monaco-text-model-service';
import { NotebookOutlineContribution } from './contributions/notebook-outline-contribution';
import { NotebookLabelProviderContribution } from './contributions/notebook-label-provider-contribution';
import { NotebookOutputActionContribution } from './contributions/notebook-output-action-contribution';
import { NotebookClipboardService } from './service/notebook-clipboard-service';
import { bindNotebookPreferences } from '../common/notebook-preferences';
import { NotebookOptionsService } from './service/notebook-options';
import { NotebookUndoRedoHandler } from './contributions/notebook-undo-redo-handler';
import { NotebookStatusBarContribution } from './contributions/notebook-status-bar-contribution';
import { NotebookCellEditorService } from './service/notebook-cell-editor-service';
import { NotebookCellStatusBarService } from './service/notebook-cell-status-bar-service';
import { MonacoEditorModelFilter } from '@theia/monaco/lib/browser/monaco-text-model-service';
import { ActiveMonacoEditorContribution } from '@theia/monaco/lib/browser/monaco-editor-service';
import { NotebookCellOpenHandler } from './notebook-cell-open-handler';
import { SplitEditorContribution } from '@theia/editor/lib/browser/split-editor-contribution';
import { NotebookEditorSplitContribution } from './notebook-editor-split-contribution';
export default new ContainerModule(bind => {
bind(NotebookColorContribution).toSelf().inSingletonScope();
bind(ColorContribution).toService(NotebookColorContribution);
bind(NotebookEditorSplitContribution).toSelf().inSingletonScope();
bind(SplitEditorContribution).toService(NotebookEditorSplitContribution);
bind(NotebookOpenHandler).toSelf().inSingletonScope();
bind(OpenHandler).toService(NotebookOpenHandler);
bind(NotebookCellOpenHandler).toSelf().inSingletonScope();
bind(OpenHandler).toService(NotebookCellOpenHandler);
bind(NotebookTypeRegistry).toSelf().inSingletonScope();
bind(NotebookRendererRegistry).toSelf().inSingletonScope();
bind(WidgetFactory).to(NotebookEditorWidgetFactory).inSingletonScope();
bind(NotebookService).toSelf().inSingletonScope();
bind(NotebookEditorWidgetService).toSelf().inSingletonScope();
bind(NotebookExecutionService).toSelf().inSingletonScope();
bind(NotebookExecutionStateService).toSelf().inSingletonScope();
bind(NotebookKernelService).toSelf().inSingletonScope();
bind(NotebookRendererMessagingService).toSelf().inSingletonScope();
bind(NotebookKernelHistoryService).toSelf().inSingletonScope();
bind(NotebookKernelQuickPickService).toSelf().inSingletonScope();
bind(NotebookClipboardService).toSelf().inSingletonScope();
bind(NotebookCellEditorService).toSelf().inSingletonScope();
bind(ActiveMonacoEditorContribution).toService(NotebookCellEditorService);
bind(NotebookCellStatusBarService).toSelf().inSingletonScope();
bind(NotebookCellResourceResolver).toSelf().inSingletonScope();
bind(ResourceResolver).toService(NotebookCellResourceResolver);
bind(NotebookModelResolverService).toSelf().inSingletonScope();
bind(NotebookModelResolverServiceProxy).toService(NotebookModelResolverService);
bind(NotebookOutputResourceResolver).toSelf().inSingletonScope();
bind(ResourceResolver).toService(NotebookOutputResourceResolver);
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(KeybindingContribution).toService(NotebookActionsContribution);
bind(NotebookOutputActionContribution).toSelf().inSingletonScope();
bind(CommandContribution).toService(NotebookOutputActionContribution);
bind(NotebookEditorWidgetContainerFactory).toFactory(ctx => (props: NotebookEditorProps) =>
createNotebookEditorWidgetContainer(ctx.container, props).get(NotebookEditorWidget)
);
bind(NotebookModelFactory).toFactory(ctx => (props: NotebookModelProps) =>
createNotebookModelContainer(ctx.container, props).get(NotebookModel)
);
bind(NotebookCellModelFactory).toFactory(ctx => (props: NotebookCellModelProps) =>
createNotebookCellModelContainer(ctx.container, props).get(NotebookCellModel)
);
bind(NotebookMonacoTextModelService).toSelf().inSingletonScope();
bind(NotebookMonacoEditorModelFilter).toSelf().inSingletonScope();
bind(MonacoEditorModelFilter).toService(NotebookMonacoEditorModelFilter);
bind(NotebookOutlineContribution).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(NotebookOutlineContribution);
bind(NotebookLabelProviderContribution).toSelf().inSingletonScope();
bind(LabelProviderContribution).toService(NotebookLabelProviderContribution);
bindNotebookPreferences(bind);
bind(NotebookOptionsService).toSelf().inSingletonScope();
bind(NotebookUndoRedoHandler).toSelf().inSingletonScope();
bind(UndoRedoHandler).toService(NotebookUndoRedoHandler);
bind(NotebookStatusBarContribution).toSelf().inSingletonScope();
bind(WidgetStatusBarContribution).toService(NotebookStatusBarContribution);
});

View File

@@ -0,0 +1,128 @@
// *****************************************************************************
// 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 { URI, MaybePromise, Disposable, PreferenceService } from '@theia/core';
import { NavigatableWidgetOpenHandler, WidgetOpenerOptions, getDefaultHandler, defaultHandlerPriority } from '@theia/core/lib/browser';
import { inject, injectable } from '@theia/core/shared/inversify';
import { NotebookFileSelector, NotebookTypeDescriptor } from '../common/notebook-protocol';
import { NotebookEditorWidget } from './notebook-editor-widget';
import { match } from '@theia/core/lib/common/glob';
import { NotebookEditorWidgetOptions } from './notebook-editor-widget-factory';
export interface NotebookWidgetOpenerOptions extends WidgetOpenerOptions {
notebookType?: string;
counter?: number;
}
@injectable()
export class NotebookOpenHandler extends NavigatableWidgetOpenHandler<NotebookEditorWidget> {
readonly id = NotebookEditorWidget.ID;
protected notebookTypes: NotebookTypeDescriptor[] = [];
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
registerNotebookType(notebookType: NotebookTypeDescriptor): Disposable {
this.notebookTypes.push(notebookType);
return Disposable.create(() => {
this.notebookTypes.splice(this.notebookTypes.indexOf(notebookType), 1);
});
}
canHandle(uri: URI, options?: NotebookWidgetOpenerOptions): MaybePromise<number> {
const defaultHandler = getDefaultHandler(uri, this.preferenceService);
if (options?.notebookType) {
return this.canHandleType(uri, this.notebookTypes.find(type => type.type === options.notebookType), defaultHandler);
}
return Math.max(...this.notebookTypes.map(type => this.canHandleType(uri, type), defaultHandler));
}
canHandleType(uri: URI, notebookType?: NotebookTypeDescriptor, defaultHandler?: string): number {
if (notebookType?.selector && this.matches(notebookType.selector, uri)) {
return notebookType.type === defaultHandler ? defaultHandlerPriority : this.calculatePriority(notebookType);
} else {
return 0;
}
}
protected calculatePriority(notebookType: NotebookTypeDescriptor | undefined): number {
if (!notebookType) {
return 0;
}
return notebookType.priority === 'option' ? 100 : 200;
}
protected findHighestPriorityType(uri: URI): NotebookTypeDescriptor | undefined {
const matchingTypes = this.notebookTypes
.filter(notebookType => notebookType.selector && this.matches(notebookType.selector, uri))
.map(notebookType => ({ descriptor: notebookType, priority: this.calculatePriority(notebookType) }));
if (matchingTypes.length === 0) {
return undefined;
}
let type = matchingTypes[0];
for (let i = 1; i < matchingTypes.length; i++) {
const notebookType = matchingTypes[i];
if (notebookType.priority > type.priority) {
type = notebookType;
}
}
return type.descriptor;
}
// Override for better options typing
override open(uri: URI, options?: NotebookWidgetOpenerOptions): Promise<NotebookEditorWidget> {
return super.open(uri, options);
}
protected override createWidgetOptions(uri: URI, options?: NotebookWidgetOpenerOptions): NotebookEditorWidgetOptions {
const widgetOptions = super.createWidgetOptions(uri, options);
const notebookType = options?.notebookType
? options.notebookType
: (this.notebookTypes.find(type => type.type === getDefaultHandler(uri, this.preferenceService))
|| this.findHighestPriorityType(uri))?.type;
if (!notebookType) {
throw new Error('No notebook types registered for uri: ' + uri.toString());
}
return {
notebookType,
...widgetOptions,
counter: options?.counter ?? widgetOptions.counter
};
}
/**
* Opens a notebook to the side of the current notebook.
* Uses timestamp to ensure unique widget IDs for multiple instances without needing to track state.
*/
openToSide(uri: URI, options?: NotebookWidgetOpenerOptions): Promise<NotebookEditorWidget> {
const counter = Date.now();
const splitOptions: NotebookWidgetOpenerOptions = { widgetOptions: { mode: 'split-right' }, ...options, counter };
return this.open(uri, splitOptions);
}
protected matches(selectors: readonly NotebookFileSelector[], resource: URI): boolean {
return selectors.some(selector => this.selectorMatches(selector, resource));
}
protected selectorMatches(selector: NotebookFileSelector, resource: URI): boolean {
return !!selector.filenamePattern
&& match(selector.filenamePattern, resource.path.name + resource.path.ext);
}
}

View File

@@ -0,0 +1,119 @@
// *****************************************************************************
// 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* Copied from commit 18b2c92451b076943e5b508380e0eba66ba7d934 from file src\vs\workbench\contrib\notebook\common\notebookCommon.ts
*--------------------------------------------------------------------------------------------*/
import { BinaryBuffer } from '@theia/core/lib/common/buffer';
const textDecoder = new TextDecoder();
/**
* Given a stream of individual stdout outputs, this function will return the compressed lines, escaping some of the common terminal escape codes.
* E.g. some terminal escape codes would result in the previous line getting cleared, such if we had 3 lines and
* last line contained such a code, then the result string would be just the first two lines.
* @returns a single VSBuffer with the concatenated and compressed data, and whether any compression was done.
*/
export function compressOutputItemStreams(outputs: Uint8Array[]): { data: BinaryBuffer, didCompression: boolean } {
const buffers: Uint8Array[] = [];
let startAppending = false;
// Pick the first set of outputs with the same mime type.
for (const output of outputs) {
if ((buffers.length === 0 || startAppending)) {
buffers.push(output);
startAppending = true;
}
}
let didCompression = compressStreamBuffer(buffers);
const concatenated = BinaryBuffer.concat(buffers.map(buffer => BinaryBuffer.wrap(buffer)));
const data = formatStreamText(concatenated);
didCompression = didCompression || data.byteLength !== concatenated.byteLength;
return { data, didCompression };
}
export const MOVE_CURSOR_1_LINE_COMMAND = `${String.fromCharCode(27)}[A`;
const MOVE_CURSOR_1_LINE_COMMAND_BYTES = MOVE_CURSOR_1_LINE_COMMAND.split('').map(c => c.charCodeAt(0));
const LINE_FEED = 10;
function compressStreamBuffer(streams: Uint8Array[]): boolean {
let didCompress = false;
streams.forEach((stream, index) => {
if (index === 0 || stream.length < MOVE_CURSOR_1_LINE_COMMAND.length) {
return;
}
const previousStream = streams[index - 1];
// Remove the previous line if required.
const command = stream.subarray(0, MOVE_CURSOR_1_LINE_COMMAND.length);
if (command[0] === MOVE_CURSOR_1_LINE_COMMAND_BYTES[0] && command[1] === MOVE_CURSOR_1_LINE_COMMAND_BYTES[1] && command[2] === MOVE_CURSOR_1_LINE_COMMAND_BYTES[2]) {
const lastIndexOfLineFeed = previousStream.lastIndexOf(LINE_FEED);
if (lastIndexOfLineFeed === -1) {
return;
}
didCompress = true;
streams[index - 1] = previousStream.subarray(0, lastIndexOfLineFeed);
streams[index] = stream.subarray(MOVE_CURSOR_1_LINE_COMMAND.length);
}
});
return didCompress;
}
const BACKSPACE_CHARACTER = '\b'.charCodeAt(0);
const CARRIAGE_RETURN_CHARACTER = '\r'.charCodeAt(0);
function formatStreamText(buffer: BinaryBuffer): BinaryBuffer {
// We have special handling for backspace and carriage return characters.
// Don't unnecessary decode the bytes if we don't need to perform any processing.
if (!buffer.buffer.includes(BACKSPACE_CHARACTER) && !buffer.buffer.includes(CARRIAGE_RETURN_CHARACTER)) {
return buffer;
}
// Do the same thing jupyter is doing
return BinaryBuffer.fromString(fixCarriageReturn(fixBackspace(textDecoder.decode(buffer.buffer))));
}
/**
* Took this from jupyter/notebook
* https://github.com/jupyter/notebook/blob/b8b66332e2023e83d2ee04f83d8814f567e01a4e/notebook/static/base/js/utils.js
* Remove characters that are overridden by backspace characters
*/
function fixBackspace(txt: string): string {
let tmp = txt;
do {
txt = tmp;
// Cancel out anything-but-newline followed by backspace
tmp = txt.replace(/[^\n]\x08/gm, '');
} while (tmp.length < txt.length);
return txt;
}
/**
* Remove chunks that should be overridden by the effect of carriage return characters
* From https://github.com/jupyter/notebook/blob/master/notebook/static/base/js/utils.js
*/
function fixCarriageReturn(txt: string): string {
txt = txt.replace(/\r+\n/gm, '\n'); // \r followed by \n --> newline
while (txt.search(/\r[^$]/g) > -1) {
const base = txt.match(/^(.*)\r+/m)![1];
let insert = txt.match(/\r+(.*)$/m)![1];
insert = insert + base.slice(insert.length, base.length);
txt = txt.replace(/\r+.*$/m, '\r').replace(/^.*\r/m, insert);
}
return txt;
}

View File

@@ -0,0 +1,85 @@
// *****************************************************************************
// 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, Path } from '@theia/core';
import { injectable } from '@theia/core/shared/inversify';
import { NotebookRendererDescriptor } from '../common/notebook-protocol';
export interface NotebookRendererInfo {
readonly id: string;
readonly displayName: string;
readonly mimeTypes: string[];
readonly entrypoint: { readonly extends?: string; readonly uri: string };
readonly requiresMessaging: boolean;
}
export interface NotebookPreloadInfo {
readonly type: string;
readonly entrypoint: string;
}
@injectable()
export class NotebookRendererRegistry {
private readonly _notebookRenderers: NotebookRendererInfo[] = [];
get notebookRenderers(): readonly NotebookRendererInfo[] {
return this._notebookRenderers;
}
private readonly _staticNotebookPreloads: NotebookPreloadInfo[] = [];
get staticNotebookPreloads(): readonly NotebookPreloadInfo[] {
return this._staticNotebookPreloads;
}
registerNotebookRenderer(type: NotebookRendererDescriptor, basePath: string): Disposable {
let entrypoint;
if (typeof type.entrypoint === 'string') {
entrypoint = {
uri: new Path(basePath).join(type.entrypoint).toString()
};
} else {
entrypoint = {
uri: new Path(basePath).join(type.entrypoint.path).toString(),
extends: type.entrypoint.extends
};
}
this._notebookRenderers.push({
...type,
mimeTypes: type.mimeTypes || [],
requiresMessaging: type.requiresMessaging === 'always' || type.requiresMessaging === 'optional',
entrypoint
});
return Disposable.create(() => {
this._notebookRenderers.splice(this._notebookRenderers.findIndex(renderer => renderer.id === type.id), 1);
});
}
registerStaticNotebookPreload(type: string, entrypoint: string, basePath: string): Disposable {
const staticPreload = { type, entrypoint: new Path(basePath).join(entrypoint).toString() };
this._staticNotebookPreloads.push(staticPreload);
return Disposable.create(() => {
this._staticNotebookPreloads.splice(this._staticNotebookPreloads.indexOf(staticPreload), 1);
});
}
}

View File

@@ -0,0 +1,54 @@
// *****************************************************************************
// 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 { Disposable, DisposableCollection } from '@theia/core';
import { inject, injectable } from '@theia/core/shared/inversify';
import { OpenWithService } from '@theia/core/lib/browser';
import { NotebookTypeDescriptor } from '../common/notebook-protocol';
import { NotebookOpenHandler } from './notebook-open-handler';
@injectable()
export class NotebookTypeRegistry {
@inject(OpenWithService)
protected readonly openWithService: OpenWithService;
@inject(NotebookOpenHandler)
protected readonly notebookOpenHandler: NotebookOpenHandler;
private readonly _notebookTypes: NotebookTypeDescriptor[] = [];
get notebookTypes(): readonly NotebookTypeDescriptor[] {
return this._notebookTypes;
}
registerNotebookType(type: NotebookTypeDescriptor, providerName: string): Disposable {
const toDispose = new DisposableCollection();
toDispose.push(Disposable.create(() => {
this._notebookTypes.splice(this._notebookTypes.indexOf(type), 1);
}));
this._notebookTypes.push(type);
toDispose.push(this.notebookOpenHandler.registerNotebookType(type));
toDispose.push(this.openWithService.registerHandler({
id: type.type,
label: type.displayName,
providerName,
canHandle: uri => this.notebookOpenHandler.canHandleType(uri, type),
open: uri => this.notebookOpenHandler.open(uri, { notebookType: type.type })
}));
return toDispose;
}
}

View File

@@ -0,0 +1,187 @@
// *****************************************************************************
// 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 {
CellData, CellEditType, CellMetadataEdit, CellOutput, CellOutputItem, CellRange, NotebookCellContentChangeEvent,
NotebookCellInternalMetadata,
NotebookCellMetadata,
NotebookCellsChangeInternalMetadataEvent,
NotebookCellsChangeLanguageEvent,
NotebookCellsChangeMetadataEvent,
NotebookCellsChangeType, NotebookCellTextModelSplice, NotebookDocumentMetadata
} from '../common';
import { NotebookCell } from './view-model/notebook-cell-model';
export interface NotebookTextModelChangedEvent {
readonly rawEvents: NotebookContentChangedEvent[];
// readonly versionId: number;
readonly synchronous?: boolean;
readonly endSelectionState?: SelectionState;
};
export type NotebookContentChangedEvent = (NotebookCellsInitializeEvent<NotebookCell> | NotebookDocumentChangeMetadataEvent | NotebookCellContentChangeEvent |
NotebookCellsModelChangedEvent<NotebookCell> | NotebookCellsModelMoveEvent<NotebookCell> | NotebookOutputChangedEvent | NotebookOutputItemChangedEvent |
NotebookCellsChangeLanguageEvent | NotebookCellsChangeMetadataEvent |
NotebookCellsChangeInternalMetadataEvent | NotebookDocumentUnknownChangeEvent); // & { transient: boolean };
export interface NotebookCellsInitializeEvent<T> {
readonly kind: NotebookCellsChangeType.Initialize;
readonly changes: NotebookCellTextModelSplice<T>[];
}
export interface NotebookDocumentChangeMetadataEvent {
readonly kind: NotebookCellsChangeType.ChangeDocumentMetadata;
readonly metadata: NotebookDocumentMetadata;
}
export interface NotebookCellsModelChangedEvent<T> {
readonly kind: NotebookCellsChangeType.ModelChange;
readonly changes: NotebookCellTextModelSplice<T>[];
}
export interface NotebookModelWillAddRemoveEvent {
readonly rawEvent: NotebookCellsModelChangedEvent<CellData>;
};
export interface NotebookCellsModelMoveEvent<T> {
readonly kind: NotebookCellsChangeType.Move;
readonly index: number;
readonly length: number;
readonly newIdx: number;
readonly cells: T[];
}
export interface NotebookOutputChangedEvent {
readonly kind: NotebookCellsChangeType.Output;
readonly index: number;
readonly outputs: CellOutput[];
readonly append: boolean;
}
export interface NotebookOutputItemChangedEvent {
readonly kind: NotebookCellsChangeType.OutputItem;
readonly index: number;
readonly outputId: string;
readonly outputItems: CellOutputItem[];
readonly append: boolean;
}
export interface NotebookDocumentUnknownChangeEvent {
readonly kind: NotebookCellsChangeType.Unknown;
}
export enum SelectionStateType {
Handle = 0,
Index = 1
}
export interface SelectionHandleState {
kind: SelectionStateType.Handle;
primary: number | null;
selections: number[];
}
export interface SelectionIndexState {
kind: SelectionStateType.Index;
focus: CellRange;
selections: CellRange[];
}
export type SelectionState = SelectionHandleState | SelectionIndexState;
export interface NotebookModelWillAddRemoveEvent {
readonly newCellIds?: number[];
readonly rawEvent: NotebookCellsModelChangedEvent<CellData>;
readonly externalEvent?: boolean;
};
export interface CellOutputEdit {
editType: CellEditType.Output;
index: number;
outputs: CellOutput[];
deleteCount?: number;
append?: boolean;
}
export interface CellOutputEditByHandle {
editType: CellEditType.Output;
handle: number;
outputs: CellOutput[];
deleteCount?: number;
append?: boolean;
}
export interface CellOutputItemEdit {
editType: CellEditType.OutputItems;
items: CellOutputItem[];
outputId: string;
append?: boolean;
}
export interface CellLanguageEdit {
editType: CellEditType.CellLanguage;
index: number;
language: string;
}
export interface DocumentMetadataEdit {
editType: CellEditType.DocumentMetadata;
metadata: NotebookDocumentMetadata;
}
export interface CellMoveEdit {
editType: CellEditType.Move;
index: number;
length: number;
newIdx: number;
}
export interface CellReplaceEdit {
editType: CellEditType.Replace;
index: number;
count: number;
cells: CellData[];
}
export interface CellPartialMetadataEdit {
editType: CellEditType.PartialMetadata;
index: number;
metadata: NullablePartialNotebookCellMetadata;
}
export type ImmediateCellEditOperation = CellOutputEditByHandle | CellOutputItemEdit | CellPartialInternalMetadataEditByHandle; // add more later on
export type CellEditOperation = ImmediateCellEditOperation | CellReplaceEdit | CellOutputEdit |
CellMetadataEdit | CellLanguageEdit | DocumentMetadataEdit | CellMoveEdit | CellPartialMetadataEdit; // add more later on
export type NullablePartialNotebookCellInternalMetadata = {
[Key in keyof Partial<NotebookCellInternalMetadata>]: NotebookCellInternalMetadata[Key] | null
};
export type NullablePartialNotebookCellMetadata = {
[Key in keyof Partial<NotebookCellMetadata>]: NotebookCellMetadata[Key] | null
};
export interface CellPartialInternalMetadataEditByHandle {
editType: CellEditType.PartialInternalMetadata;
handle: number;
internalMetadata: NullablePartialNotebookCellInternalMetadata;
}
export interface NotebookCellOutputsSplice {
start: number;
deleteCount: number;
newOutputs: CellOutput[];
};

View File

@@ -0,0 +1,49 @@
// *****************************************************************************
// 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 { Disposable, Event } from '@theia/core';
import { NotebookModel } from '../view-model/notebook-model';
import { NotebookEditorWidget } from '../notebook-editor-widget';
import { NotebookContentChangedEvent } from '../notebook-types';
import { NotebookCellOutputModel } from '../view-model/notebook-cell-output-model';
import { NotebookCellModel } from '../view-model/notebook-cell-model';
export const CellOutputWebviewFactory = Symbol('outputWebviewFactory');
export const CellOutputWebview = Symbol('outputWebview');
export type CellOutputWebviewFactory = () => Promise<CellOutputWebview>;
export interface OutputRenderEvent {
cellHandle: number;
outputId: string;
outputHeight: number;
}
export interface CellOutputWebview extends Disposable {
readonly id: string;
init(notebook: NotebookModel, editor: NotebookEditorWidget): void;
render(): React.ReactNode;
setCellHeight(cell: NotebookCellModel, height: number): void;
cellsChanged(cellEvent: NotebookContentChangedEvent[]): void;
onDidRenderOutput: Event<OutputRenderEvent>
requestOutputPresentationUpdate(cellHandle: number, output: NotebookCellOutputModel): void;
isAttached(): boolean
}

View File

@@ -0,0 +1,86 @@
// *****************************************************************************
// 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 { Emitter, URI } from '@theia/core';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { SimpleMonacoEditor } from '@theia/monaco/lib/browser/simple-monaco-editor';
import { NotebookEditorWidgetService } from './notebook-editor-widget-service';
import { CellUri } from '../../common';
import { ActiveMonacoEditorContribution, MonacoEditorService } from '@theia/monaco/lib/browser/monaco-editor-service';
import { ICodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorBrowser';
@injectable()
export class NotebookCellEditorService implements ActiveMonacoEditorContribution {
@inject(NotebookEditorWidgetService)
protected readonly notebookEditorWidgetService: NotebookEditorWidgetService;
@inject(MonacoEditorService)
protected readonly monacoEditorService: MonacoEditorService;
protected onDidChangeCellEditorsEmitter = new Emitter<void>();
readonly onDidChangeCellEditors = this.onDidChangeCellEditorsEmitter.event;
protected onDidChangeFocusedCellEditorEmitter = new Emitter<SimpleMonacoEditor | undefined>();
readonly onDidChangeFocusedCellEditor = this.onDidChangeFocusedCellEditorEmitter.event;
protected currentActiveCell?: SimpleMonacoEditor;
protected currentCellEditors: Map<string, SimpleMonacoEditor> = new Map();
@postConstruct()
protected init(): void {
this.notebookEditorWidgetService.onDidChangeCurrentEditor(editor => {
// if defocus notebook editor or another notebook editor is focused, clear the active cell
if (!editor || (this.currentActiveCell && CellUri.parse(this.currentActiveCell.uri)?.notebook.toString() !== editor?.model?.uri.toString())) {
this.currentActiveCell = undefined;
// eslint-disable-next-line no-null/no-null
this.monacoEditorService.setActiveCodeEditor(null);
this.onDidChangeFocusedCellEditorEmitter.fire(undefined);
}
});
}
get allCellEditors(): SimpleMonacoEditor[] {
return Array.from(this.currentCellEditors.values());
}
editorCreated(uri: URI, editor: SimpleMonacoEditor): void {
this.currentCellEditors.set(uri.toString(), editor);
this.onDidChangeCellEditorsEmitter.fire();
}
editorDisposed(uri: URI): void {
this.currentCellEditors.delete(uri.toString());
this.onDidChangeCellEditorsEmitter.fire();
}
editorFocusChanged(editor?: SimpleMonacoEditor): void {
if (editor) {
this.currentActiveCell = editor;
this.monacoEditorService.setActiveCodeEditor(editor.getControl());
this.onDidChangeFocusedCellEditorEmitter.fire(editor);
}
}
getActiveCell(): SimpleMonacoEditor | undefined {
return this.currentActiveCell;
}
getActiveEditor(): ICodeEditor | undefined {
return this.getActiveCell()?.getControl();
}
}

View File

@@ -0,0 +1,94 @@
// *****************************************************************************
// 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken, Command, Disposable, Emitter, Event, URI } from '@theia/core';
import { CellStatusbarAlignment } from '../../common';
import { ThemeColor } from '@theia/core/lib/common/theme';
import { AccessibilityInformation } from '@theia/core/lib/common/accessibility';
import { injectable } from '@theia/core/shared/inversify';
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
export interface NotebookCellStatusBarItem {
readonly alignment: CellStatusbarAlignment;
readonly priority?: number;
readonly text: string;
readonly color?: string | ThemeColor;
readonly backgroundColor?: string | ThemeColor;
readonly tooltip?: string | MarkdownString;
readonly command?: string | (Command & { arguments?: unknown[] });
readonly accessibilityInformation?: AccessibilityInformation;
readonly opacity?: string;
readonly onlyShowWhenActive?: boolean;
}
export interface NotebookCellStatusBarItemList {
items: NotebookCellStatusBarItem[];
dispose?(): void;
}
export interface NotebookCellStatusBarItemProvider {
viewType: string;
onDidChangeStatusBarItems?: Event<void>;
provideCellStatusBarItems(uri: URI, index: number, token: CancellationToken): Promise<NotebookCellStatusBarItemList | undefined>;
}
@injectable()
export class NotebookCellStatusBarService implements Disposable {
protected readonly onDidChangeProvidersEmitter = new Emitter<void>();
readonly onDidChangeProviders: Event<void> = this.onDidChangeProvidersEmitter.event;
protected readonly onDidChangeItemsEmitter = new Emitter<void>();
readonly onDidChangeItems: Event<void> = this.onDidChangeItemsEmitter.event;
protected readonly providers: NotebookCellStatusBarItemProvider[] = [];
registerCellStatusBarItemProvider(provider: NotebookCellStatusBarItemProvider): Disposable {
this.providers.push(provider);
let changeListener: Disposable | undefined;
if (provider.onDidChangeStatusBarItems) {
changeListener = provider.onDidChangeStatusBarItems(() => this.onDidChangeItemsEmitter.fire());
}
this.onDidChangeProvidersEmitter.fire();
return Disposable.create(() => {
changeListener?.dispose();
const idx = this.providers.findIndex(p => p === provider);
this.providers.splice(idx, 1);
});
}
async getStatusBarItemsForCell(notebookUri: URI, cellIndex: number, viewType: string, token: CancellationToken): Promise<NotebookCellStatusBarItemList[]> {
const providers = this.providers.filter(p => p.viewType === viewType || p.viewType === '*');
return Promise.all(providers.map(async p => {
try {
return await p.provideCellStatusBarItems(notebookUri, cellIndex, token) ?? { items: [] };
} catch (e) {
console.error(e);
return { items: [] };
}
}));
}
dispose(): void {
this.onDidChangeItemsEmitter.dispose();
this.onDidChangeProvidersEmitter.dispose();
}
}

View File

@@ -0,0 +1,43 @@
// *****************************************************************************
// 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 { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { NotebookCellModel } from '../view-model/notebook-cell-model';
import { environment } from '@theia/core';
import { CellData } from '../../common';
@injectable()
export class NotebookClipboardService {
protected copiedCell: CellData | undefined;
@inject(ClipboardService)
protected readonly clipboardService: ClipboardService;
copyCell(cell: NotebookCellModel): void {
this.copiedCell = cell.getData();
if (environment.electron.is()) {
this.clipboardService.writeText(cell.text);
}
}
getCell(): CellData | undefined {
return this.copiedCell;
}
}

View File

@@ -0,0 +1,157 @@
// *****************************************************************************
// 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 { ContextKeyChangeEvent, ContextKeyService, ContextMatcher, ScopedValueStore } from '@theia/core/lib/browser/context-key-service';
import { DisposableCollection } from '@theia/core';
import { NotebookKernelService } from './notebook-kernel-service';
import {
NOTEBOOK_CELL_EDITABLE,
NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE,
NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE,
NOTEBOOK_CELL_TYPE, NOTEBOOK_HAS_OUTPUTS, NOTEBOOK_KERNEL, NOTEBOOK_KERNEL_SELECTED,
NOTEBOOK_OUTPUT_INPUT_FOCUSED,
NOTEBOOK_VIEW_TYPE
} from '../contributions/notebook-context-keys';
import { NotebookEditorWidget } from '../notebook-editor-widget';
import { NotebookCellModel } from '../view-model/notebook-cell-model';
import { CellKind, NotebookCellsChangeType } from '../../common';
import { NotebookExecutionStateService } from './notebook-execution-state-service';
import { NotebookViewModel } from '../view-model/notebook-view-model';
@injectable()
export class NotebookContextManager {
@inject(ContextKeyService) protected contextKeyService: ContextKeyService;
@inject(NotebookKernelService)
protected readonly notebookKernelService: NotebookKernelService;
@inject(NotebookExecutionStateService)
protected readonly executionStateService: NotebookExecutionStateService;
protected readonly toDispose = new DisposableCollection();
protected _context?: HTMLElement;
scopedStore: ScopedValueStore;
get context(): HTMLElement | undefined {
return this._context;
}
protected cellContexts: Map<number, Record<string, unknown>> = new Map();
protected notebookViewModel: NotebookViewModel;
init(widget: NotebookEditorWidget): void {
this._context = widget.node;
this.scopedStore = this.contextKeyService.createScoped(widget.node);
this.toDispose.dispose();
this.scopedStore.setContext(NOTEBOOK_VIEW_TYPE, widget?.notebookType);
this.notebookViewModel = widget.viewModel;
// Kernel related keys
const kernel = widget?.model ? this.notebookKernelService.getSelectedNotebookKernel(widget.model) : undefined;
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()) {
this.scopedStore.setContext(NOTEBOOK_KERNEL_SELECTED, !!e.newKernel);
this.scopedStore.setContext(NOTEBOOK_KERNEL, e.newKernel);
}
}));
widget.model?.onDidChangeContent(events => {
if (events.some(e => e.kind === NotebookCellsChangeType.ModelChange || e.kind === NotebookCellsChangeType.Output)) {
this.scopedStore.setContext(NOTEBOOK_HAS_OUTPUTS, widget.model?.cells.some(cell => cell.outputs.length > 0));
}
});
this.scopedStore.setContext(NOTEBOOK_HAS_OUTPUTS, !!widget.model?.cells.find(cell => cell.outputs.length > 0));
// Cell Selection related keys
this.scopedStore.setContext(NOTEBOOK_CELL_FOCUSED, !!widget.viewModel.selectedCell);
this.selectedCellChanged(widget.viewModel.selectedCell);
widget.viewModel.onDidChangeSelectedCell(e => {
this.selectedCellChanged(e.cell);
this.scopedStore.setContext(NOTEBOOK_CELL_FOCUSED, !!e);
});
this.toDispose.push(this.executionStateService.onDidChangeExecution(e => {
if (e.notebook.toString() === widget.model?.uri.toString()) {
this.setCellContext(e.cellHandle, NOTEBOOK_CELL_EXECUTING, !!e.changed);
this.setCellContext(e.cellHandle, NOTEBOOK_CELL_EXECUTION_STATE, e.changed?.state ?? 'idle');
}
}));
widget.onDidChangeOutputInputFocus(focus => {
this.scopedStore.setContext(NOTEBOOK_OUTPUT_INPUT_FOCUSED, focus);
});
}
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) {
const cellViewModel = this.notebookViewModel.cellViewModels.get(cell.handle);
this.scopedStore.setContext(NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, cellViewModel?.editing);
this.scopedStore.setContext(NOTEBOOK_CELL_EDITABLE, cell.cellKind === CellKind.Markup && !cellViewModel?.editing);
if (cellViewModel) {
this.cellDisposables.push(cellViewModel.onDidRequestCellEditChange(cellEdit => {
this.scopedStore.setContext(NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, cellEdit);
this.scopedStore.setContext(NOTEBOOK_CELL_EDITABLE, cell.cellKind === CellKind.Markup && !cellEdit);
}));
}
}
}
protected setCellContext(cellHandle: number, key: string, value: unknown): void {
let cellContext = this.cellContexts.get(cellHandle);
if (!cellContext) {
cellContext = {};
this.cellContexts.set(cellHandle, cellContext);
}
cellContext[key] = value;
}
getCellContext(cellHandle: number): ContextMatcher {
return this.contextKeyService.createOverlay(Object.entries(this.cellContexts.get(cellHandle) ?? {}));
}
changeCellFocus(focus: boolean): void {
this.scopedStore.setContext(NOTEBOOK_CELL_FOCUSED, focus);
}
changeCellListFocus(focus: boolean): void {
this.scopedStore.setContext(NOTEBOOK_CELL_LIST_FOCUSED, focus);
}
createContextKeyChangedEvent(affectedKeys: string[]): ContextKeyChangeEvent {
return { affects: keys => affectedKeys.some(key => keys.has(key)) };
}
dispose(): void {
this.toDispose.dispose();
}
}

View File

@@ -0,0 +1,121 @@
// *****************************************************************************
// 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
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 {
@inject(ApplicationShell)
protected applicationShell: ApplicationShell;
@inject(ContextKeyService)
protected contextKeyService: ContextKeyService;
protected readonly notebookEditors = new Map<string, NotebookEditorWidget>();
protected readonly onNotebookEditorAddEmitter = new Emitter<NotebookEditorWidget>();
protected readonly onNotebookEditorRemoveEmitter = new Emitter<NotebookEditorWidget>();
readonly onDidAddNotebookEditor = this.onNotebookEditorAddEmitter.event;
readonly onDidRemoveNotebookEditor = this.onNotebookEditorRemoveEmitter.event;
protected readonly onDidChangeFocusedEditorEmitter = new Emitter<NotebookEditorWidget | undefined>();
readonly onDidChangeFocusedEditor = this.onDidChangeFocusedEditorEmitter.event;
protected readonly onDidChangeCurrentEditorEmitter = new Emitter<NotebookEditorWidget | undefined>();
readonly onDidChangeCurrentEditor = this.onDidChangeCurrentEditorEmitter.event;
focusedEditor?: NotebookEditorWidget = undefined;
currentEditor?: NotebookEditorWidget = undefined;
@postConstruct()
protected init(): void {
this.applicationShell.onDidChangeActiveWidget(event => {
this.notebookEditorFocusChanged(event.newValue as NotebookEditorWidget, event.newValue instanceof NotebookEditorWidget);
});
this.applicationShell.onDidChangeCurrentWidget(event => {
if (event.newValue instanceof NotebookEditorWidget || event.oldValue instanceof NotebookEditorWidget) {
this.currentNotebookEditorChanged(event.newValue);
}
});
}
// --- editor management
addNotebookEditor(editor: NotebookEditorWidget): void {
if (this.notebookEditors.has(editor.id)) {
console.warn('Attempting to add duplicated notebook editor: ' + editor.id);
}
this.notebookEditors.set(editor.id, editor);
this.onNotebookEditorAddEmitter.fire(editor);
if (editor.isVisible) {
this.notebookEditorFocusChanged(editor, true);
}
}
removeNotebookEditor(editor: NotebookEditorWidget): void {
if (this.notebookEditors.has(editor.id)) {
this.notebookEditors.delete(editor.id);
this.onNotebookEditorRemoveEmitter.fire(editor);
} else {
console.warn('Attempting to remove not registered editor: ' + editor.id);
}
}
getNotebookEditor(editorId: string): NotebookEditorWidget | undefined {
return this.notebookEditors.get(editorId);
}
getNotebookEditors(): readonly NotebookEditorWidget[] {
return Array.from(this.notebookEditors.values());
}
notebookEditorFocusChanged(editor: NotebookEditorWidget, focus: boolean): void {
if (focus) {
if (editor !== this.focusedEditor) {
this.focusedEditor = editor;
this.contextKeyService.setContext(NOTEBOOK_EDITOR_FOCUSED, true);
this.onDidChangeFocusedEditorEmitter.fire(this.focusedEditor);
}
} else if (this.focusedEditor) {
this.focusedEditor = undefined;
this.contextKeyService.setContext(NOTEBOOK_EDITOR_FOCUSED, false);
this.onDidChangeFocusedEditorEmitter.fire(undefined);
}
}
currentNotebookEditorChanged(newEditor: unknown): void {
if (newEditor instanceof NotebookEditorWidget) {
this.currentEditor = newEditor;
this.onDidChangeCurrentEditorEmitter.fire(newEditor);
} else if (this.currentEditor?.isDisposed || !this.currentEditor?.isVisible) {
this.currentEditor = undefined;
this.onDidChangeCurrentEditorEmitter.fire(undefined);
}
}
}

View File

@@ -0,0 +1,157 @@
// *****************************************************************************
// 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { inject, injectable, named } from '@theia/core/shared/inversify';
import { CellExecution, NotebookExecutionStateService } from '../service/notebook-execution-state-service';
import { CellKind, NotebookCellExecutionState } from '../../common';
import { NotebookCellModel } from '../view-model/notebook-cell-model';
import { NotebookModel } from '../view-model/notebook-model';
import { NotebookKernelService } from './notebook-kernel-service';
import { CommandService, Disposable, ILogger } from '@theia/core';
import { NotebookKernelQuickPickService } from './notebook-kernel-quick-pick-service';
import { NotebookKernelHistoryService } from './notebook-kernel-history-service';
export interface CellExecutionParticipant {
onWillExecuteCell(executions: CellExecution[]): Promise<void>;
}
@injectable()
export class NotebookExecutionService {
@inject(NotebookExecutionStateService)
protected notebookExecutionStateService: NotebookExecutionStateService;
@inject(NotebookKernelService)
protected notebookKernelService: NotebookKernelService;
@inject(NotebookKernelHistoryService)
protected notebookKernelHistoryService: NotebookKernelHistoryService;
@inject(CommandService)
protected commandService: CommandService;
@inject(NotebookKernelQuickPickService)
protected notebookKernelQuickPickService: NotebookKernelQuickPickService;
@inject(ILogger) @named('notebook')
protected readonly logger: ILogger;
protected readonly cellExecutionParticipants = new Set<CellExecutionParticipant>();
async executeNotebookCells(notebook: NotebookModel, cells: Iterable<NotebookCellModel>): Promise<void> {
const cellsArr = Array.from(cells)
.filter(c => c.cellKind === CellKind.Code);
if (!cellsArr.length) {
return;
}
this.logger.debug('Executing notebook cells', {
notebook: notebook.uri.toString(),
cells: cellsArr.map(c => c.handle)
});
// create cell executions
const cellExecutions: [NotebookCellModel, CellExecution][] = [];
for (const cell of cellsArr) {
const cellExe = this.notebookExecutionStateService.getCellExecution(cell.uri);
if (!cellExe) {
cellExecutions.push([cell, this.notebookExecutionStateService.getOrCreateCellExecution(notebook.uri, cell.handle)]);
}
}
const kernel = await this.notebookKernelHistoryService.resolveSelectedKernel(notebook);
if (!kernel) {
this.logger.debug('Failed to resolve kernel for execution', notebook.uri.toString());
// clear all pending cell executions
cellExecutions.forEach(cellExe => cellExe[1].complete({}));
return;
}
// filter cell executions based on selected kernel
const validCellExecutions: CellExecution[] = [];
for (const [cell, cellExecution] of cellExecutions) {
if (!kernel.supportedLanguages.includes(cell.language)) {
cellExecution.complete({});
} else {
validCellExecutions.push(cellExecution);
}
}
// request execution
if (validCellExecutions.length > 0) {
const cellRemoveListener = notebook.onDidAddOrRemoveCell(e => {
if (e.rawEvent.changes.some(c => c.deleteCount > 0)) {
const executionsToCancel = validCellExecutions.filter(exec => !notebook.cells.find(cell => cell.handle === exec.cellHandle));
if (executionsToCancel.length > 0) {
kernel.cancelNotebookCellExecution(notebook.uri, executionsToCancel.map(c => c.cellHandle));
executionsToCancel.forEach(exec => exec.complete({}));
}
}
});
await this.runExecutionParticipants(validCellExecutions);
this.logger.debug('Selecting kernel for cell execution', {
notebook: notebook.uri.toString(),
kernel: kernel.id
});
this.notebookKernelService.selectKernelForNotebook(kernel, notebook);
this.logger.debug('Running cell execution request', {
notebook: notebook.uri.toString(),
cells: validCellExecutions.map(c => c.cellHandle)
});
await kernel.executeNotebookCellsRequest(notebook.uri, validCellExecutions.map(c => c.cellHandle));
// the connecting state can change before the kernel resolves executeNotebookCellsRequest
const unconfirmed = validCellExecutions.filter(exe => exe.state === NotebookCellExecutionState.Unconfirmed);
if (unconfirmed.length) {
unconfirmed.forEach(exe => exe.complete({}));
}
cellRemoveListener.dispose();
}
}
registerExecutionParticipant(participant: CellExecutionParticipant): Disposable {
this.cellExecutionParticipants.add(participant);
return Disposable.create(() => this.cellExecutionParticipants.delete(participant));
}
protected async runExecutionParticipants(executions: CellExecution[]): Promise<void> {
for (const participant of this.cellExecutionParticipants) {
await participant.onWillExecuteCell(executions);
}
return;
}
async cancelNotebookCellHandles(notebook: NotebookModel, cells: Iterable<number>): Promise<void> {
const cellsArr = Array.from(cells);
const kernel = this.notebookKernelService.getSelectedOrSuggestedKernel(notebook);
if (kernel) {
await kernel.cancelNotebookCellExecution(notebook.uri, cellsArr);
}
}
async cancelNotebookCells(notebook: NotebookModel, cells: Iterable<NotebookCellModel>): Promise<void> {
this.cancelNotebookCellHandles(notebook, Array.from(cells, cell => cell.handle));
}
}

View File

@@ -0,0 +1,306 @@
// *****************************************************************************
// 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, DisposableCollection, Emitter, URI, generateUuid } from '@theia/core';
import { inject, injectable } from '@theia/core/shared/inversify';
import { NotebookService } from './notebook-service';
import {
CellEditType, CellExecuteOutputEdit, CellExecuteOutputItemEdit, CellExecutionUpdateType,
CellUri, NotebookCellExecutionState, NotebookCellInternalMetadata
} from '../../common';
import { CellPartialInternalMetadataEditByHandle, CellEditOperation } from '../notebook-types';
import { NotebookModel } from '../view-model/notebook-model';
export type CellExecuteUpdate = CellExecuteOutputEdit | CellExecuteOutputItemEdit | CellExecutionStateUpdate;
export interface CellExecutionComplete {
runEndTime?: number;
lastRunSuccess?: boolean;
}
export interface CellExecutionStateUpdate {
editType: CellExecutionUpdateType.ExecutionState;
executionOrder?: number;
runStartTime?: number;
didPause?: boolean;
isPaused?: boolean;
}
export enum NotebookExecutionType {
cell,
notebook
}
export interface NotebookFailStateChangedEvent {
visible: boolean;
notebook: URI;
}
export interface FailedCellInfo {
cellHandle: number;
disposable: Disposable;
visible: boolean;
}
@injectable()
export class NotebookExecutionStateService implements Disposable {
@inject(NotebookService)
protected notebookService: NotebookService;
protected toDispose: DisposableCollection = new DisposableCollection();
protected readonly executions = new Map<string, Map<number, CellExecution>>();
protected readonly onDidChangeExecutionEmitter = new Emitter<CellExecutionStateChangedEvent>();
onDidChangeExecution = this.onDidChangeExecutionEmitter.event;
protected readonly onDidChangeLastRunFailStateEmitter = new Emitter<NotebookFailStateChangedEvent>();
onDidChangeLastRunFailState = this.onDidChangeLastRunFailStateEmitter.event;
getOrCreateCellExecution(notebookUri: URI, cellHandle: number): CellExecution {
const notebook = this.notebookService.getNotebookEditorModel(notebookUri);
if (!notebook) {
throw new Error(`Notebook not found: ${notebookUri.toString()}`);
}
let execution = this.executions.get(notebookUri.toString())?.get(cellHandle);
if (!execution) {
execution = this.createNotebookCellExecution(notebook, cellHandle);
if (!this.executions.has(notebookUri.toString())) {
this.executions.set(notebookUri.toString(), new Map());
}
this.executions.get(notebookUri.toString())?.set(cellHandle, execution);
execution.initialize();
this.onDidChangeExecutionEmitter.fire(new CellExecutionStateChangedEvent(notebookUri, cellHandle, execution));
}
return execution;
}
protected createNotebookCellExecution(notebook: NotebookModel, cellHandle: number): CellExecution {
const notebookUri = notebook.uri;
const execution = new CellExecution(cellHandle, notebook);
execution.toDispose.push(execution.onDidUpdate(() => this.onDidChangeExecutionEmitter.fire(new CellExecutionStateChangedEvent(notebookUri, cellHandle, execution))));
execution.toDispose.push(execution.onDidComplete(lastRunSuccess => this.onCellExecutionDidComplete(notebookUri, cellHandle, execution, lastRunSuccess)));
return execution;
}
protected onCellExecutionDidComplete(notebookUri: URI, cellHandle: number, exe: CellExecution, lastRunSuccess?: boolean): void {
const notebookExecutions = this.executions.get(notebookUri.toString())?.get(cellHandle);
if (!notebookExecutions) {
throw new Error('Notebook Cell Execution not found while trying to complete it');
}
exe.dispose();
this.executions.get(notebookUri.toString())?.delete(cellHandle);
this.onDidChangeExecutionEmitter.fire(new CellExecutionStateChangedEvent(notebookUri, cellHandle));
}
getCellExecution(cellUri: URI): CellExecution | undefined {
const parsed = CellUri.parse(cellUri);
if (!parsed) {
throw new Error(`Not a cell URI: ${cellUri}`);
}
return this.executions.get(parsed.notebook.toString())?.get(parsed.handle);
}
dispose(): void {
this.onDidChangeExecutionEmitter.dispose();
this.onDidChangeLastRunFailStateEmitter.dispose();
this.executions.forEach(notebookExecutions => notebookExecutions.forEach(execution => execution.dispose()));
}
}
export class CellExecution implements Disposable {
protected readonly onDidUpdateEmitter = new Emitter<void>();
readonly onDidUpdate = this.onDidUpdateEmitter.event;
protected readonly onDidCompleteEmitter = new Emitter<boolean | undefined>();
readonly onDidComplete = this.onDidCompleteEmitter.event;
toDispose = new DisposableCollection();
protected _state: NotebookCellExecutionState = NotebookCellExecutionState.Unconfirmed;
get state(): NotebookCellExecutionState {
return this._state;
}
get notebookURI(): URI {
return this.notebook.uri;
}
protected _didPause = false;
get didPause(): boolean {
return this._didPause;
}
protected _isPaused = false;
get isPaused(): boolean {
return this._isPaused;
}
constructor(
readonly cellHandle: number,
protected readonly notebook: NotebookModel,
) {
}
initialize(): void {
const startExecuteEdit: CellPartialInternalMetadataEditByHandle = {
editType: CellEditType.PartialInternalMetadata,
handle: this.cellHandle,
internalMetadata: {
executionId: generateUuid(),
runStartTime: undefined,
runEndTime: undefined,
lastRunSuccess: undefined,
executionOrder: undefined,
renderDuration: undefined,
}
};
this.applyCellExecutionEditsToNotebook([startExecuteEdit]);
}
confirm(): void {
this._state = NotebookCellExecutionState.Pending;
this.onDidUpdateEmitter.fire();
}
update(updates: CellExecuteUpdate[]): void {
if (updates.some(u => u.editType === CellExecutionUpdateType.ExecutionState)) {
this._state = NotebookCellExecutionState.Executing;
}
if (!this._didPause && updates.some(u => u.editType === CellExecutionUpdateType.ExecutionState && u.didPause)) {
this._didPause = true;
}
const lastIsPausedUpdate = [...updates].reverse().find(u => u.editType === CellExecutionUpdateType.ExecutionState && typeof u.isPaused === 'boolean');
if (lastIsPausedUpdate) {
this._isPaused = (lastIsPausedUpdate as CellExecutionStateUpdate).isPaused!;
}
const cellModel = this.notebook.cells.find(c => c.handle === this.cellHandle);
if (!cellModel) {
console.debug(`CellExecution#update, updating cell not in notebook: ${this.notebook.uri.toString()}, ${this.cellHandle}`);
} else {
const edits = updates.map(update => updateToEdit(update, this.cellHandle));
this.applyCellExecutionEditsToNotebook(edits);
}
if (updates.some(u => u.editType === CellExecutionUpdateType.ExecutionState)) {
this.onDidUpdateEmitter.fire();
}
}
complete(completionData: CellExecutionComplete): void {
const cellModel = this.notebook.cells.find(c => c.handle === this.cellHandle);
if (!cellModel) {
console.debug(`CellExecution#complete, completing cell not in notebook: ${this.notebook.uri.toString()}, ${this.cellHandle}`);
} else {
const edit: CellEditOperation = {
editType: CellEditType.PartialInternalMetadata,
handle: this.cellHandle,
internalMetadata: {
lastRunSuccess: completionData.lastRunSuccess,
// eslint-disable-next-line no-null/no-null
runStartTime: this._didPause ? null : cellModel.internalMetadata.runStartTime,
// eslint-disable-next-line no-null/no-null
runEndTime: this._didPause ? null : completionData.runEndTime,
}
};
this.applyCellExecutionEditsToNotebook([edit]);
}
this.onDidCompleteEmitter.fire(completionData.lastRunSuccess);
}
dispose(): void {
this.onDidUpdateEmitter.dispose();
this.onDidCompleteEmitter.dispose();
this.toDispose.dispose();
}
protected applyCellExecutionEditsToNotebook(edits: CellEditOperation[]): void {
this.notebook.applyEdits(edits, false);
}
}
export class CellExecutionStateChangedEvent {
readonly type = NotebookExecutionType.cell;
constructor(
readonly notebook: URI,
readonly cellHandle: number,
readonly changed?: CellExecution
) { }
affectsCell(cell: URI): boolean {
const parsedUri = CellUri.parse(cell);
return !!parsedUri && this.notebook.isEqual(parsedUri.notebook) && this.cellHandle === parsedUri.handle;
}
affectsNotebook(notebook: URI): boolean {
return this.notebook.toString() === notebook.toString();
}
}
export function updateToEdit(update: CellExecuteUpdate, cellHandle: number): CellEditOperation {
if (update.editType === CellExecutionUpdateType.Output) {
return {
editType: CellEditType.Output,
handle: update.cellHandle,
append: update.append,
outputs: update.outputs,
};
} else if (update.editType === CellExecutionUpdateType.OutputItems) {
return {
editType: CellEditType.OutputItems,
items: update.items,
outputId: update.outputId,
append: update.append,
};
} else if (update.editType === CellExecutionUpdateType.ExecutionState) {
const newInternalMetadata: Partial<NotebookCellInternalMetadata> = {};
if (typeof update.executionOrder !== 'undefined') {
newInternalMetadata.executionOrder = update.executionOrder;
}
if (typeof update.runStartTime !== 'undefined') {
newInternalMetadata.runStartTime = update.runStartTime;
}
return {
editType: CellEditType.PartialInternalMetadata,
handle: cellHandle,
internalMetadata: newInternalMetadata
};
}
throw new Error('Unknown cell update type');
}

View File

@@ -0,0 +1,124 @@
// *****************************************************************************
// 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { StorageService } from '@theia/core/lib/browser';
import { NotebookKernel, NotebookTextModelLike, NotebookKernelService } from './notebook-kernel-service';
import { CommandService, Disposable } from '@theia/core';
import { NotebookModel } from '../view-model/notebook-model';
import { NotebookCommands } from '../contributions/notebook-actions-contribution';
interface KernelsList {
[viewType: string]: string[];
}
interface MostRecentKernelsResult {
selected?: NotebookKernel,
all: NotebookKernel[]
}
const MAX_KERNELS_IN_HISTORY = 5;
@injectable()
export class NotebookKernelHistoryService implements Disposable {
@inject(StorageService)
protected storageService: StorageService;
@inject(NotebookKernelService)
protected notebookKernelService: NotebookKernelService;
@inject(CommandService)
protected commandService: CommandService;
protected static STORAGE_KEY = 'notebook.kernelHistory';
protected mostRecentKernelsMap: KernelsList = {};
@postConstruct()
protected init(): void {
this.loadState();
}
getKernels(notebook: NotebookTextModelLike): MostRecentKernelsResult {
const allAvailableKernels = this.notebookKernelService.getMatchingKernel(notebook);
const allKernels = allAvailableKernels.all;
const selectedKernel = allAvailableKernels.selected;
// We will suggest the only kernel
const suggested = allAvailableKernels.all.length === 1 ? allAvailableKernels.all[0] : undefined;
const mostRecentKernelIds = this.mostRecentKernelsMap[notebook.viewType] ? this.mostRecentKernelsMap[notebook.viewType].map(kernel => kernel[1]) : [];
const all = mostRecentKernelIds.map(kernelId => allKernels.find(kernel => kernel.id === kernelId)).filter(kernel => !!kernel) as NotebookKernel[];
return {
selected: selectedKernel ?? suggested,
all
};
}
async resolveSelectedKernel(notebook: NotebookModel): Promise<NotebookKernel | undefined> {
const alreadySelected = this.getKernels(notebook);
if (alreadySelected.selected) {
return alreadySelected.selected;
}
await this.commandService.executeCommand(NotebookCommands.SELECT_KERNEL_COMMAND.id, notebook);
const { selected } = this.getKernels(notebook);
return selected;
}
addMostRecentKernel(kernel: NotebookKernel): void {
const viewType = kernel.viewType;
const recentKernels = this.mostRecentKernelsMap[viewType] ?? [kernel.id];
if (recentKernels.length > MAX_KERNELS_IN_HISTORY) {
recentKernels.splice(MAX_KERNELS_IN_HISTORY);
}
this.mostRecentKernelsMap[viewType] = recentKernels;
this.saveState();
}
protected saveState(): void {
let notEmpty = false;
for (const kernels of Object.values(this.mostRecentKernelsMap)) {
notEmpty = notEmpty || Object.entries(kernels).length > 0;
}
this.storageService.setData(NotebookKernelHistoryService.STORAGE_KEY, notEmpty ? this.mostRecentKernelsMap : undefined);
}
protected async loadState(): Promise<void> {
const kernelMap = await this.storageService.getData(NotebookKernelHistoryService.STORAGE_KEY);
if (kernelMap) {
this.mostRecentKernelsMap = kernelMap as KernelsList;
} else {
this.mostRecentKernelsMap = {};
}
}
clear(): void {
this.mostRecentKernelsMap = {};
this.saveState();
}
dispose(): void {
}
}

View File

@@ -0,0 +1,487 @@
// *****************************************************************************
// 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ArrayUtils, CommandService, DisposableCollection, Event, ILogger, nls, QuickInputButton, QuickInputService, QuickPickInput, QuickPickItem, URI, } from '@theia/core';
import { inject, injectable, named } from '@theia/core/shared/inversify';
import { NotebookKernelService, NotebookKernel, NotebookKernelMatchResult, SourceCommand } from './notebook-kernel-service';
import { NotebookModel } from '../view-model/notebook-model';
import { NotebookEditorWidget } from '../notebook-editor-widget';
import { codicon, OpenerService } from '@theia/core/lib/browser';
import { NotebookKernelHistoryService } from './notebook-kernel-history-service';
import { NotebookCommand, NotebookModelResource } from '../../common';
import debounce = require('@theia/core/shared/lodash.debounce');
export const JUPYTER_EXTENSION_ID = 'ms-toolsai.jupyter';
type KernelPick = QuickPickItem & { kernel: NotebookKernel };
function isKernelPick(item: QuickPickInput<QuickPickItem>): item is KernelPick {
return 'kernel' in item;
}
type GroupedKernelsPick = QuickPickItem & { kernels: NotebookKernel[]; source: string };
function isGroupedKernelsPick(item: QuickPickInput<QuickPickItem>): item is GroupedKernelsPick {
return 'kernels' in item;
}
type SourcePick = QuickPickItem & { action: SourceCommand };
function isSourcePick(item: QuickPickInput<QuickPickItem>): item is SourcePick {
return 'action' in item;
}
type InstallExtensionPick = QuickPickItem & { extensionIds: string[] };
type KernelSourceQuickPickItem = QuickPickItem & { command: NotebookCommand; documentation?: string };
function isKernelSourceQuickPickItem(item: QuickPickItem): item is KernelSourceQuickPickItem {
return 'command' in item;
}
function supportAutoRun(item: QuickPickInput<KernelQuickPickItem>): item is QuickPickItem {
return 'autoRun' in item && !!item.autoRun;
}
type KernelQuickPickItem = (QuickPickItem & { autoRun?: boolean }) | InstallExtensionPick | KernelPick | GroupedKernelsPick | SourcePick | KernelSourceQuickPickItem;
const KERNEL_PICKER_UPDATE_DEBOUNCE = 200;
export type KernelQuickPickContext =
{ id: string; extension: string } |
{ notebookEditorId: string } |
{ id: string; extension: string; notebookEditorId: string } |
{ ui?: boolean; notebookEditor?: NotebookEditorWidget };
function toKernelQuickPick(kernel: NotebookKernel, selected: NotebookKernel | undefined): KernelPick {
const res: KernelPick = {
kernel,
label: kernel.label,
description: kernel.description,
detail: kernel.detail
};
if (kernel.id === selected?.id) {
if (!res.description) {
res.description = nls.localizeByDefault('Currently Selected');
} else {
res.description = nls.localizeByDefault('{0} - Currently Selected', res.description);
}
}
return res;
}
@injectable()
export class NotebookKernelQuickPickService {
@inject(NotebookKernelService)
protected readonly notebookKernelService: NotebookKernelService;
@inject(QuickInputService)
protected readonly quickInputService: QuickInputService;
@inject(CommandService)
protected readonly commandService: CommandService;
@inject(OpenerService)
protected readonly openerService: OpenerService;
@inject(NotebookKernelHistoryService)
protected readonly notebookKernelHistoryService: NotebookKernelHistoryService;
@inject(ILogger) @named('notebook')
protected readonly logger: ILogger;
async showQuickPick(editor: NotebookModel, wantedId?: string, skipAutoRun?: boolean): Promise<boolean> {
const notebook = editor;
const matchResult = this.getMatchingResult(notebook);
const { selected, all } = matchResult;
let newKernel: NotebookKernel | undefined;
if (wantedId) {
for (const candidate of all) {
if (candidate.id === wantedId) {
newKernel = candidate;
break;
}
}
if (!newKernel) {
console.warn(`wanted kernel DOES NOT EXIST, wanted: ${wantedId}, all: ${all.map(k => k.id)}`);
return false;
}
}
if (newKernel) {
this.selectKernel(notebook, newKernel);
return true;
}
const quickPick = this.quickInputService.createQuickPick<KernelQuickPickItem>();
const quickPickItems = this.getKernelPickerQuickPickItems(matchResult);
if (quickPickItems.length === 1 && supportAutoRun(quickPickItems[0]) && !skipAutoRun) {
return this.handleQuickPick(editor, quickPickItems[0], quickPickItems as KernelQuickPickItem[]);
}
quickPick.items = quickPickItems;
quickPick.canSelectMany = false;
quickPick.placeholder = selected
? nls.localizeByDefault("Change kernel for '{0}'", 'current') // TODO get label for current notebook from a label provider
: nls.localizeByDefault("Select kernel for '{0}'", 'current');
quickPick.busy = this.notebookKernelService.getKernelDetectionTasks(notebook).length > 0;
const kernelDetectionTaskListener = this.notebookKernelService.onDidChangeKernelDetectionTasks(() => {
quickPick.busy = this.notebookKernelService.getKernelDetectionTasks(notebook).length > 0;
});
const kernelChangeEventListener = debounce(
Event.any(
this.notebookKernelService.onDidChangeSourceActions,
this.notebookKernelService.onDidAddKernel,
this.notebookKernelService.onDidRemoveKernel,
this.notebookKernelService.onDidChangeNotebookAffinity
),
KERNEL_PICKER_UPDATE_DEBOUNCE
)(async () => {
// reset quick pick progress
quickPick.busy = false;
const currentActiveItems = quickPick.activeItems;
const newMatchResult = this.getMatchingResult(notebook);
const newQuickPickItems = this.getKernelPickerQuickPickItems(newMatchResult);
quickPick.keepScrollPosition = true;
// recalculate active items
const activeItems: KernelQuickPickItem[] = [];
for (const item of currentActiveItems) {
if (isKernelPick(item)) {
const kernelId = item.kernel.id;
const sameItem = newQuickPickItems.find(pi => isKernelPick(pi) && pi.kernel.id === kernelId) as KernelPick | undefined;
if (sameItem) {
activeItems.push(sameItem);
}
} else if (isSourcePick(item)) {
const sameItem = newQuickPickItems.find(pi => isSourcePick(pi) && pi.action.command.id === item.action.command.id) as SourcePick | undefined;
if (sameItem) {
activeItems.push(sameItem);
}
}
}
quickPick.items = newQuickPickItems;
quickPick.activeItems = activeItems;
}, this);
const pick = await new Promise<{ selected: KernelQuickPickItem | undefined; items: KernelQuickPickItem[] }>((resolve, reject) => {
quickPick.onDidAccept(() => {
const item = quickPick.selectedItems[0];
if (item) {
resolve({ selected: item, items: quickPick.items as KernelQuickPickItem[] });
} else {
resolve({ selected: undefined, items: quickPick.items as KernelQuickPickItem[] });
}
quickPick.hide();
});
quickPick.onDidHide(() => {
kernelDetectionTaskListener.dispose();
kernelChangeEventListener?.dispose();
quickPick.dispose();
resolve({ selected: undefined, items: quickPick.items as KernelQuickPickItem[] });
});
quickPick.show();
});
if (pick.selected) {
return this.handleQuickPick(editor, pick.selected, pick.items);
}
return false;
}
protected getKernelPickerQuickPickItems(matchResult: NotebookKernelMatchResult): QuickPickInput<KernelQuickPickItem>[] {
const quickPickItems: QuickPickInput<KernelQuickPickItem>[] = [];
if (matchResult.selected) {
const kernelItem = toKernelQuickPick(matchResult.selected, matchResult.selected);
quickPickItems.push(kernelItem);
}
// TODO use suggested here when kernel affinity is implemented. For now though show all kernels
matchResult.all.filter(kernel => kernel.id !== matchResult.selected?.id).map(kernel => toKernelQuickPick(kernel, matchResult.selected))
.forEach(kernel => {
quickPickItems.push(kernel);
});
const shouldAutoRun = quickPickItems.length === 0;
if (quickPickItems.length > 0) {
quickPickItems.push({
type: 'separator'
});
}
// select another kernel quick pick
quickPickItems.push({
id: 'selectAnother',
label: nls.localizeByDefault('Select Another Kernel...'),
autoRun: shouldAutoRun
});
return quickPickItems;
}
protected selectKernel(notebook: NotebookModel, kernel: NotebookKernel): void {
this.logger.debug('Selected notebook kernel', {
notebook: notebook.uri.toString(),
kernel: kernel.id
});
const currentInfo = this.notebookKernelService.getMatchingKernel(notebook);
if (currentInfo.selected) {
// there is already a selected kernel
this.notebookKernelHistoryService.addMostRecentKernel(currentInfo.selected);
}
this.notebookKernelService.selectKernelForNotebook(kernel, notebook);
this.notebookKernelHistoryService.addMostRecentKernel(kernel);
}
protected getMatchingResult(notebook: NotebookModel): NotebookKernelMatchResult {
const { selected, all } = this.notebookKernelHistoryService.getKernels(notebook);
const matchingResult = this.notebookKernelService.getMatchingKernel(notebook);
return {
selected: selected,
all: matchingResult.all,
suggestions: all,
hidden: []
};
}
protected async handleQuickPick(editor: NotebookModel, pick: KernelQuickPickItem, items: KernelQuickPickItem[]): Promise<boolean> {
if (pick.id === 'selectAnother') {
return this.displaySelectAnotherQuickPick(editor, items.length === 1 && items[0] === pick);
}
if (isKernelPick(pick)) {
const newKernel = pick.kernel;
this.selectKernel(editor, newKernel);
return true;
}
if (isSourcePick(pick)) {
this.logger.debug('Selected notebook kernel command', {
notebook: editor.uri.toString(),
command: pick.action.command.id
});
// selected explicitly, it should trigger the execution?
pick.action.run(this.commandService);
}
return true;
}
protected async displaySelectAnotherQuickPick(editor: NotebookModel, kernelListEmpty: boolean): Promise<boolean> {
const notebook: NotebookModel = editor;
const disposables = new DisposableCollection();
const quickPick = this.quickInputService.createQuickPick<KernelQuickPickItem>();
const quickPickItem = await new Promise<KernelQuickPickItem | QuickInputButton | undefined>(resolve => {
// select from kernel sources
quickPick.title = kernelListEmpty ? nls.localizeByDefault('Select Kernel') : nls.localizeByDefault('Select Another Kernel');
quickPick.placeholder = nls.localizeByDefault('Type to choose a kernel source');
quickPick.busy = true;
// quickPick.buttons = [this.quickInputService.backButton];
quickPick.show();
disposables.push(quickPick.onDidTriggerButton(button => {
if (button === this.quickInputService.backButton) {
resolve(button);
}
}));
quickPick.onDidTriggerItemButton(async e => {
if (isKernelSourceQuickPickItem(e.item) && e.item.documentation !== undefined) {
const uri: URI | undefined = this.isUri(e.item.documentation) ? new URI(e.item.documentation) : await this.commandService.executeCommand(e.item.documentation);
if (uri) {
(await this.openerService.getOpener(uri, { openExternal: true })).open(uri, { openExternal: true });
}
}
});
disposables.push(quickPick.onDidAccept(async () => {
resolve(quickPick.selectedItems[0]);
}));
disposables.push(quickPick.onDidHide(() => {
resolve(undefined);
}));
this.calculateKernelSources(editor).then(quickPickItems => {
quickPick.items = quickPickItems;
if (quickPick.items.length > 0) {
quickPick.busy = false;
}
});
debounce(
Event.any(
this.notebookKernelService.onDidChangeSourceActions,
this.notebookKernelService.onDidAddKernel,
this.notebookKernelService.onDidRemoveKernel
),
KERNEL_PICKER_UPDATE_DEBOUNCE,
)(async () => {
quickPick.busy = true;
const quickPickItems = await this.calculateKernelSources(editor);
quickPick.items = quickPickItems;
quickPick.busy = false;
});
});
quickPick.hide();
disposables.dispose();
if (quickPickItem === this.quickInputService.backButton) {
return this.showQuickPick(editor, undefined, true);
}
if (quickPickItem) {
const selectedKernelPickItem = quickPickItem as KernelQuickPickItem;
if (isKernelSourceQuickPickItem(selectedKernelPickItem)) {
try {
const selectedKernelId = await this.executeCommand<string>(notebook, selectedKernelPickItem.command);
if (selectedKernelId) {
const { all } = this.getMatchingResult(notebook);
const notebookKernel = all.find(kernel => kernel.id === `ms-toolsai.jupyter/${selectedKernelId}`);
if (notebookKernel) {
this.selectKernel(notebook, notebookKernel);
return true;
}
return true;
} else {
return this.displaySelectAnotherQuickPick(editor, false);
}
} catch (ex) {
console.error('Failed to select notebook kernel', ex);
return false;
}
} else if (isKernelPick(selectedKernelPickItem)) {
this.selectKernel(notebook, selectedKernelPickItem.kernel);
return true;
} else if (isGroupedKernelsPick(selectedKernelPickItem)) {
await this.selectOneKernel(notebook, selectedKernelPickItem.source, selectedKernelPickItem.kernels);
return true;
} else if (isSourcePick(selectedKernelPickItem)) {
// selected explicitly, it should trigger the execution?
try {
await selectedKernelPickItem.action.run(this.commandService);
return true;
} catch (ex) {
console.error('Failed to select notebook kernel', ex);
return false;
}
}
// } else if (isSearchMarketplacePick(selectedKernelPickItem)) {
// await this.showKernelExtension(
// this.paneCompositePartService,
// this.extensionWorkbenchService,
// this.extensionService,
// editor.textModel.viewType,
// []
// );
// return true;
// } else if (isInstallExtensionPick(selectedKernelPickItem)) {
// await this.showKernelExtension(
// this.paneCompositePartService,
// this.extensionWorkbenchService,
// this.extensionService,
// editor.textModel.viewType,
// selectedKernelPickItem.extensionIds,
// );
// return true;
// }
}
return false;
}
protected isUri(value: string): boolean {
return /^(?<scheme>\w[\w\d+.-]*):/.test(value);
}
protected async calculateKernelSources(editor: NotebookModel): Promise<QuickPickInput<KernelQuickPickItem>[]> {
const notebook: NotebookModel = editor;
const actions = await this.notebookKernelService.getKernelSourceActionsFromProviders(notebook);
const matchResult = this.getMatchingResult(notebook);
const others = matchResult.all.filter(item => item.extensionId !== JUPYTER_EXTENSION_ID);
const quickPickItems: QuickPickInput<KernelQuickPickItem>[] = [];
// group controllers by extension
for (const group of ArrayUtils.groupBy(others, (a, b) => a.extensionId === b.extensionId ? 0 : 1)) {
const source = group[0].extensionId;
if (group.length > 1) {
quickPickItems.push({
label: source,
kernels: group
});
} else {
quickPickItems.push({
label: group[0].label,
kernel: group[0]
});
}
}
const validActions = actions.filter(action => action.command);
quickPickItems.push(...validActions.map(action => {
const buttons = action.documentation ? [{
iconClass: codicon('info'),
tooltip: nls.localizeByDefault('Learn More'),
}] : [];
return {
id: typeof action.command! === 'string' ? action.command! : action.command!.id,
label: action.label,
description: action.description,
command: action.command,
documentation: action.documentation,
buttons
};
}));
return quickPickItems;
}
protected async selectOneKernel(notebook: NotebookModel, source: string, kernels: NotebookKernel[]): Promise<void> {
const quickPickItems: QuickPickInput<KernelPick>[] = kernels.map(kernel => toKernelQuickPick(kernel, undefined));
const quickPick = this.quickInputService.createQuickPick<KernelQuickPickItem>();
quickPick.items = quickPickItems;
quickPick.canSelectMany = false;
quickPick.title = nls.localizeByDefault('Select Kernel from {0}', source);
quickPick.onDidAccept(async () => {
if (quickPick.selectedItems && quickPick.selectedItems.length > 0 && isKernelPick(quickPick.selectedItems[0])) {
this.selectKernel(notebook, quickPick.selectedItems[0].kernel);
}
quickPick.hide();
quickPick.dispose();
});
quickPick.onDidHide(() => {
quickPick.dispose();
});
quickPick.show();
}
protected async executeCommand<T>(notebook: NotebookModel, command: NotebookCommand): Promise<T | undefined | void> {
const args = (command.arguments || []).concat([NotebookModelResource.create(notebook.uri)]);
return this.commandService.executeCommand(command.id, ...args);
}
}

View File

@@ -0,0 +1,357 @@
// *****************************************************************************
// 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Command, CommandService, Disposable, Emitter, Event, URI } from '@theia/core';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { StorageService } from '@theia/core/lib/browser';
import { NotebookKernelSourceAction } from '../../common';
import { NotebookModel } from '../view-model/notebook-model';
import { NotebookService } from './notebook-service';
export interface SelectedNotebookKernelChangeEvent {
notebook: URI;
oldKernel: string | undefined;
newKernel: string | undefined;
}
export interface NotebookKernelMatchResult {
readonly selected: NotebookKernel | undefined;
readonly suggestions: NotebookKernel[];
readonly all: NotebookKernel[];
readonly hidden: NotebookKernel[];
}
export interface NotebookKernelChangeEvent {
label?: true;
description?: true;
detail?: true;
supportedLanguages?: true;
hasExecutionOrder?: true;
hasInterruptHandler?: true;
}
export interface NotebookKernel {
readonly id: string;
readonly viewType: string;
readonly onDidChange: Event<Readonly<NotebookKernelChangeEvent>>;
// ID of the extension providing this kernel
readonly extensionId: string;
readonly localResourceRoot: URI;
readonly preloadUris: URI[];
readonly preloadProvides: string[];
readonly handle: number;
label: string;
description?: string;
detail?: string;
supportedLanguages: string[];
implementsInterrupt?: boolean;
implementsExecutionOrder?: boolean;
executeNotebookCellsRequest(uri: URI, cellHandles: number[]): Promise<void>;
cancelNotebookCellExecution(uri: URI, cellHandles: number[]): Promise<void>;
}
export const enum ProxyKernelState {
Disconnected = 1,
Connected = 2,
Initializing = 3
}
export interface INotebookProxyKernelChangeEvent extends NotebookKernelChangeEvent {
connectionState?: true;
}
export interface NotebookTextModelLike { uri: URI; viewType: string }
class KernelInfo {
protected static instanceCounter = 0;
score: number;
readonly kernel: NotebookKernel;
readonly handle: number;
constructor(kernel: NotebookKernel) {
this.kernel = kernel;
this.score = -1;
this.handle = KernelInfo.instanceCounter++;
}
}
export interface NotebookSourceActionChangeEvent {
notebook?: URI;
viewType: string;
}
export interface KernelSourceActionProvider {
readonly viewType: string;
onDidChangeSourceActions?: Event<void>;
provideKernelSourceActions(): Promise<NotebookKernelSourceAction[]>;
}
export class SourceCommand implements Disposable {
execution: Promise<void> | undefined;
protected readonly onDidChangeStateEmitter = new Emitter<void>();
readonly onDidChangeState = this.onDidChangeStateEmitter.event;
constructor(
readonly command: Command,
readonly model: NotebookTextModelLike,
) { }
async run(commandService: CommandService): Promise<void> {
if (this.execution) {
return this.execution;
}
this.execution = this.runCommand(commandService);
this.onDidChangeStateEmitter.fire();
await this.execution;
this.execution = undefined;
this.onDidChangeStateEmitter.fire();
}
protected async runCommand(commandService: CommandService): Promise<void> {
try {
await commandService.executeCommand(this.command.id, {
uri: this.model.uri,
});
} catch (error) {
console.warn(`Kernel source command failed: ${error}`);
}
}
dispose(): void {
this.onDidChangeStateEmitter.dispose();
}
}
const NOTEBOOK_KERNEL_BINDING_STORAGE_KEY = 'notebook.kernel.bindings';
@injectable()
export class NotebookKernelService {
@inject(NotebookService)
protected notebookService: NotebookService;
@inject(StorageService)
protected storageService: StorageService;
protected readonly kernels = new Map<string, KernelInfo>();
protected notebookBindings: Record<string, string> = {};
protected readonly kernelDetectionTasks = new Map<string, string[]>();
protected readonly onDidChangeKernelDetectionTasksEmitter = new Emitter<string>();
readonly onDidChangeKernelDetectionTasks = this.onDidChangeKernelDetectionTasksEmitter.event;
protected readonly onDidChangeSourceActionsEmitter = new Emitter<NotebookSourceActionChangeEvent>();
protected readonly kernelSourceActionProviders = new Map<string, KernelSourceActionProvider[]>();
readonly onDidChangeSourceActions: Event<NotebookSourceActionChangeEvent> = this.onDidChangeSourceActionsEmitter.event;
protected readonly onDidAddKernelEmitter = new Emitter<NotebookKernel>();
readonly onDidAddKernel: Event<NotebookKernel> = this.onDidAddKernelEmitter.event;
protected readonly onDidRemoveKernelEmitter = new Emitter<NotebookKernel>();
readonly onDidRemoveKernel: Event<NotebookKernel> = this.onDidRemoveKernelEmitter.event;
protected readonly onDidChangeSelectedNotebookKernelBindingEmitter = new Emitter<SelectedNotebookKernelChangeEvent>();
readonly onDidChangeSelectedKernel: Event<SelectedNotebookKernelChangeEvent> = this.onDidChangeSelectedNotebookKernelBindingEmitter.event;
protected readonly onDidChangeNotebookAffinityEmitter = new Emitter<void>();
readonly onDidChangeNotebookAffinity: Event<void> = this.onDidChangeNotebookAffinityEmitter.event;
@postConstruct()
init(): void {
this.notebookService.onDidAddNotebookDocument(model => this.tryAutoBindNotebook(model));
this.storageService.getData(NOTEBOOK_KERNEL_BINDING_STORAGE_KEY).then((value: Record<string, string> | undefined) => {
if (value) {
this.notebookBindings = value;
}
});
}
registerKernel(kernel: NotebookKernel): Disposable {
if (this.kernels.has(kernel.id)) {
throw new Error(`Notebook Controller with id '${kernel.id}' already exists`);
}
this.kernels.set(kernel.id, new KernelInfo(kernel));
this.onDidAddKernelEmitter.fire(kernel);
// auto associate the new kernel to existing notebooks it was
// associated to in the past.
for (const notebook of this.notebookService.getNotebookModels()) {
this.tryAutoBindNotebook(notebook, kernel);
}
return Disposable.create(() => {
if (this.kernels.delete(kernel.id)) {
this.onDidRemoveKernelEmitter.fire(kernel);
}
});
}
/**
* Helps to find the best matching kernel for a notebook.
* @param notebook notebook to get the matching kernel for
* @returns and object containing:
* all kernels sorted to match the notebook best first (affinity ascending, score descending, label))
* the selected kernel (if any)
* specific suggested kernels (if any)
* hidden kernels (if any)
*/
getMatchingKernel(notebook: NotebookTextModelLike): NotebookKernelMatchResult {
const kernels: { kernel: NotebookKernel; instanceAffinity: number; score: number }[] = [];
for (const info of this.kernels.values()) {
const score = NotebookKernelService.score(info.kernel, notebook);
if (score) {
kernels.push({
score,
kernel: info.kernel,
instanceAffinity: 1 /* vscode.NotebookControllerPriority.Default */,
});
}
}
kernels
.sort((a, b) => b.instanceAffinity - a.instanceAffinity || a.score - b.score || a.kernel.label.localeCompare(b.kernel.label));
const all = kernels.map(obj => obj.kernel);
// bound kernel
const selected = this.getSelectedNotebookKernel(notebook);
const suggestions = kernels.filter(item => item.instanceAffinity > 1).map(item => item.kernel); // TODO implement notebookAffinity
const hidden = kernels.filter(item => item.instanceAffinity < 0).map(item => item.kernel);
return { all, selected, suggestions, hidden };
}
getSelectedNotebookKernel(notebook: NotebookTextModelLike): NotebookKernel | undefined {
const selectedId = this.notebookBindings[`${notebook.viewType}/${notebook.uri}`];
return selectedId ? this.kernels.get(selectedId)?.kernel : undefined;
}
selectKernelForNotebook(kernel: NotebookKernel | undefined, notebook: NotebookTextModelLike): void {
const key = `${notebook.viewType}/${notebook.uri}`;
const oldKernel = this.notebookBindings[key];
if (oldKernel !== kernel?.id) {
if (kernel) {
this.notebookBindings[key] = kernel.id;
} else {
delete this.notebookBindings[key];
}
this.storageService.setData(NOTEBOOK_KERNEL_BINDING_STORAGE_KEY, this.notebookBindings);
this.onDidChangeSelectedNotebookKernelBindingEmitter.fire({ notebook: notebook.uri, oldKernel, newKernel: kernel?.id });
}
}
getSelectedOrSuggestedKernel(notebook: NotebookModel): NotebookKernel | undefined {
const info = this.getMatchingKernel(notebook);
if (info.selected) {
return info.selected;
}
return info.all.length === 1 ? info.all[0] : undefined;
}
getKernel(id: string): NotebookKernel | undefined {
return this.kernels.get(id)?.kernel;
}
protected static score(kernel: NotebookKernel, notebook: NotebookTextModelLike): number {
if (kernel.viewType === notebook.viewType) {
return 10;
} else if (kernel.viewType === '*') {
return 5;
} else {
return 0;
}
}
protected tryAutoBindNotebook(notebook: NotebookModel, onlyThisKernel?: NotebookKernel): void {
const id = this.notebookBindings[`${notebook.viewType}/${notebook.uri}`];
if (!id) {
// no kernel associated
return;
}
const existingKernel = this.kernels.get(id);
if (!existingKernel || !NotebookKernelService.score(existingKernel.kernel, notebook)) {
// associated kernel not known, not matching
return;
}
if (!onlyThisKernel || existingKernel.kernel === onlyThisKernel) {
this.onDidChangeSelectedNotebookKernelBindingEmitter.fire({ notebook: notebook.uri, oldKernel: undefined, newKernel: existingKernel.kernel.id });
}
}
registerNotebookKernelDetectionTask(notebookType: string): Disposable {
const all = this.kernelDetectionTasks.get(notebookType) ?? [];
all.push(notebookType);
this.kernelDetectionTasks.set(notebookType, all);
this.onDidChangeKernelDetectionTasksEmitter.fire(notebookType);
return Disposable.create(() => {
const allTasks = this.kernelDetectionTasks.get(notebookType) ?? [];
const taskIndex = allTasks.indexOf(notebookType);
if (taskIndex >= 0) {
allTasks.splice(taskIndex, 1);
this.kernelDetectionTasks.set(notebookType, allTasks);
this.onDidChangeKernelDetectionTasksEmitter.fire(notebookType);
}
});
}
getKernelDetectionTasks(notebook: NotebookTextModelLike): string[] {
return this.kernelDetectionTasks.get(notebook.viewType) ?? [];
}
registerKernelSourceActionProvider(viewType: string, provider: KernelSourceActionProvider): Disposable {
const providers = this.kernelSourceActionProviders.get(viewType) ?? [];
providers.push(provider);
this.kernelSourceActionProviders.set(viewType, providers);
this.onDidChangeSourceActionsEmitter.fire({ viewType: viewType });
const eventEmitterDisposable = provider.onDidChangeSourceActions?.(() => {
this.onDidChangeSourceActionsEmitter.fire({ viewType: viewType });
});
return Disposable.create(() => {
const sourceProviders = this.kernelSourceActionProviders.get(viewType) ?? [];
const providerIndex = sourceProviders.indexOf(provider);
if (providerIndex >= 0) {
sourceProviders.splice(providerIndex, 1);
this.kernelSourceActionProviders.set(viewType, sourceProviders);
}
eventEmitterDisposable?.dispose();
});
}
async getKernelSourceActionsFromProviders(notebook: NotebookTextModelLike): Promise<NotebookKernelSourceAction[]> {
const viewType = notebook.viewType;
const providers = this.kernelSourceActionProviders.get(viewType) ?? [];
const promises = providers.map(provider => provider.provideKernelSourceActions());
const allActions = await Promise.all(promises);
return allActions.flat();
}
}

View File

@@ -0,0 +1,167 @@
// *****************************************************************************
// 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 { Emitter, Resource, ResourceProvider, UNTITLED_SCHEME, URI } from '@theia/core';
import { inject, injectable } from '@theia/core/shared/inversify';
import { UriComponents } from '@theia/core/lib/common/uri';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { NotebookData } from '../../common';
import { NotebookModel } from '../view-model/notebook-model';
import { NotebookService } from './notebook-service';
import { NotebookTypeRegistry } from '../notebook-type-registry';
import { NotebookFileSelector } from '../../common/notebook-protocol';
import { match } from '@theia/core/lib/common/glob';
export interface UntitledResource {
untitledResource: URI | undefined
}
@injectable()
export class NotebookModelResolverService {
@inject(FileService)
protected fileService: FileService;
@inject(ResourceProvider)
protected resourceProvider: ResourceProvider;
@inject(NotebookService)
protected notebookService: NotebookService;
@inject(NotebookTypeRegistry)
protected notebookTypeRegistry: NotebookTypeRegistry;
protected onDidChangeDirtyEmitter = new Emitter<NotebookModel>();
readonly onDidChangeDirty = this.onDidChangeDirtyEmitter.event;
protected onDidSaveNotebookEmitter = new Emitter<UriComponents>();
readonly onDidSaveNotebook = this.onDidSaveNotebookEmitter.event;
async resolve(resource: URI, viewType?: string): Promise<NotebookModel> {
const existingModel = this.notebookService.getNotebookEditorModel(resource);
if (!viewType) {
if (existingModel) {
return existingModel;
} else {
viewType = this.findViewTypeForResource(resource);
}
} else if (existingModel?.viewType === viewType) {
return existingModel;
}
if (!viewType) {
throw new Error(`Missing viewType for '${resource}'`);
}
try {
const actualResource = await this.resourceProvider(resource);
const notebookData = await this.resolveExistingNotebookData(actualResource, viewType!);
const notebookModel = await this.notebookService.createNotebookModel(notebookData, viewType, actualResource);
notebookModel.onDirtyChanged(() => this.onDidChangeDirtyEmitter.fire(notebookModel));
notebookModel.onDidSaveNotebook(() => this.onDidSaveNotebookEmitter.fire(notebookModel.uri.toComponents()));
return notebookModel;
} catch (e) {
const message = `Error resolving notebook model for: \n ${resource.path.fsPath()} \n with view type ${viewType}. \n ${e}`;
console.error(message);
throw new Error(message);
}
}
async resolveUntitledResource(arg: UntitledResource, viewType: string): Promise<NotebookModel> {
let resource: URI;
// let hasAssociatedFilePath = false;
arg = arg as UntitledResource;
if (!arg.untitledResource) {
const notebookTypeInfo = this.notebookTypeRegistry.notebookTypes.find(info => info.type === viewType);
if (!notebookTypeInfo) {
throw new Error('UNKNOWN view type: ' + viewType);
}
const suffix = this.getPossibleFileEnding(notebookTypeInfo.selector ?? []) ?? '';
for (let counter = 1; ; counter++) {
const candidate = new URI()
.withScheme(UNTITLED_SCHEME)
.withPath(`Untitled-notebook-${counter}${suffix}`)
.withQuery(viewType);
if (!this.notebookService.getNotebookEditorModel(candidate)) {
resource = candidate;
break;
}
}
} else if (arg.untitledResource.scheme === UNTITLED_SCHEME) {
resource = arg.untitledResource;
} else {
throw new Error('Invalid untitled resource: ' + arg.untitledResource.toString() + ' untitled resources with associated file path are not supported yet');
// TODO implement associated file path support
// resource = arg.untitledResource.withScheme('untitled');
// hasAssociatedFilePath = true;
}
return this.resolve(resource, viewType);
}
async resolveExistingNotebookData(resource: Resource, viewType: string): Promise<NotebookData> {
if (resource.uri.scheme === 'untitled') {
return {
cells: [],
metadata: {}
};
} else {
const [dataProvider, contents] = await Promise.all([
this.notebookService.getNotebookDataProvider(viewType),
this.fileService.readFile(resource.uri)
]);
const notebook = await dataProvider.serializer.toNotebook(contents.value);
return notebook;
}
}
protected getPossibleFileEnding(selectors: readonly NotebookFileSelector[]): string | undefined {
for (const selector of selectors) {
const ending = this.possibleFileEnding(selector);
if (ending) {
return ending;
}
}
return undefined;
}
protected possibleFileEnding(selector: NotebookFileSelector): string | undefined {
const pattern = /^.*(\.[a-zA-Z0-9_-]+)$/;
const candidate = typeof selector === 'string' ? selector : selector.filenamePattern;
if (candidate) {
const matches = pattern.exec(candidate);
if (matches) {
return matches[1];
}
}
return undefined;
}
protected findViewTypeForResource(resource: URI): string | undefined {
return this.notebookTypeRegistry.notebookTypes.find(info =>
info.selector?.some(selector => selector.filenamePattern && match(selector.filenamePattern, resource.path.name + resource.path.ext))
)?.type;
}
}

View File

@@ -0,0 +1,69 @@
// *****************************************************************************
// 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 { URI, Reference, Event, Emitter } from '@theia/core';
import { inject, injectable } from '@theia/core/shared/inversify';
import { MonacoTextModelService, MonacoEditorModelFilter } from '@theia/monaco/lib/browser/monaco-text-model-service';
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
import { NotebookModel } from '../view-model/notebook-model';
import { CellUri } from '../../common/notebook-common';
@injectable()
export class NotebookMonacoEditorModelFilter implements MonacoEditorModelFilter {
protected readonly onDidCreateCellModelEmitter = new Emitter<MonacoEditorModel>();
get onDidCreateCellModel(): Event<MonacoEditorModel> {
return this.onDidCreateCellModelEmitter.event;
}
filter(model: MonacoEditorModel): boolean {
const applies = model.uri.startsWith(CellUri.cellUriScheme);
if (applies) {
// If the model is for a notebook cell, we emit the event to notify the listeners.
// We create our own event here, as we don't want to propagate the creation of the cell to the plugin host.
// Instead, we want to do that ourselves once the notebook model is completely initialized.
this.onDidCreateCellModelEmitter.fire(model);
}
return applies;
}
}
/**
* special service for creating monaco textmodels for notebook cells.
* Its for optimization purposes since there is alot of overhead otherwise with calling the backend to create a document for each cell and other smaller things.
*/
@injectable()
export class NotebookMonacoTextModelService {
@inject(MonacoTextModelService)
protected readonly monacoTextModelService: MonacoTextModelService;
@inject(NotebookMonacoEditorModelFilter)
protected readonly notebookMonacoEditorModelFilter: NotebookMonacoEditorModelFilter;
getOrCreateNotebookCellModelReference(uri: URI): Promise<Reference<MonacoEditorModel>> {
return this.monacoTextModelService.createModelReference(uri);
}
async createTextModelsForNotebook(notebook: NotebookModel): Promise<void> {
await Promise.all(notebook.cells.map(cell => cell.resolveTextModel()));
}
get onDidCreateNotebookCellModel(): Event<MonacoEditorModel> {
return this.notebookMonacoEditorModelFilter.onDidCreateCellModel;
}
}

View File

@@ -0,0 +1,155 @@
// *****************************************************************************
// 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, postConstruct } from '@theia/core/shared/inversify';
import { PreferenceService } from '@theia/core/lib/common';
import { Emitter } from '@theia/core';
import { NotebookPreferences, notebookPreferenceSchema } from '../../common/notebook-preferences';
import { EditorPreferences } from '@theia/editor/lib/common/editor-preferences';
import { BareFontInfo } from '@theia/monaco-editor-core/esm/vs/editor/common/config/fontInfo';
import { PixelRatio } from '@theia/monaco-editor-core/esm/vs/base/browser/pixelRatio';
const notebookOutputOptionsRelevantPreferences = [
'editor.fontSize',
'editor.fontFamily',
NotebookPreferences.NOTEBOOK_LINE_NUMBERS,
NotebookPreferences.OUTPUT_LINE_HEIGHT,
NotebookPreferences.OUTPUT_FONT_SIZE,
NotebookPreferences.OUTPUT_FONT_FAMILY,
NotebookPreferences.OUTPUT_SCROLLING,
NotebookPreferences.OUTPUT_WORD_WRAP,
NotebookPreferences.OUTPUT_LINE_LIMIT
];
export interface NotebookOutputOptions {
// readonly outputNodePadding: number;
readonly outputNodeLeftPadding: number;
// readonly previewNodePadding: number;
// readonly markdownLeftMargin: number;
// readonly leftMargin: number;
// readonly rightMargin: number;
// readonly runGutter: number;
// readonly dragAndDropEnabled: boolean;
readonly fontSize: number;
readonly outputFontSize?: number;
readonly fontFamily: string;
readonly outputFontFamily?: string;
// readonly markupFontSize: number;
// readonly markdownLineHeight: number;
readonly outputLineHeight: number;
readonly outputScrolling: boolean;
readonly outputWordWrap: boolean;
readonly outputLineLimit: number;
// readonly outputLinkifyFilePaths: boolean;
// readonly minimalError: boolean;
}
@injectable()
export class NotebookOptionsService {
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
@inject(EditorPreferences)
protected readonly editorPreferences: EditorPreferences;
protected outputOptionsChangedEmitter = new Emitter<NotebookOutputOptions>();
onDidChangeOutputOptions = this.outputOptionsChangedEmitter.event;
protected fontInfo?: BareFontInfo;
get editorFontInfo(): BareFontInfo {
return this.getOrCreateMonacoFontInfo();
}
@postConstruct()
protected init(): void {
this.preferenceService.onPreferencesChanged(async preferenceChanges => {
if (notebookOutputOptionsRelevantPreferences.some(p => p in preferenceChanges)) {
this.outputOptionsChangedEmitter.fire(this.computeOutputOptions());
}
});
}
computeOutputOptions(): NotebookOutputOptions {
const outputLineHeight = this.getNotebookPreferenceWithDefault<number>(NotebookPreferences.OUTPUT_LINE_HEIGHT);
const fontSize = this.preferenceService.get<number>('editor.fontSize')!;
const outputFontSize = this.getNotebookPreferenceWithDefault<number>(NotebookPreferences.OUTPUT_FONT_SIZE);
return {
fontSize,
outputFontSize: outputFontSize,
fontFamily: this.preferenceService.get<string>('editor.fontFamily')!,
outputNodeLeftPadding: 8,
outputFontFamily: this.getNotebookPreferenceWithDefault<string>(NotebookPreferences.OUTPUT_FONT_FAMILY),
outputLineHeight: this.computeOutputLineHeight(outputLineHeight, outputFontSize ?? fontSize),
outputScrolling: this.getNotebookPreferenceWithDefault<boolean>(NotebookPreferences.OUTPUT_SCROLLING)!,
outputWordWrap: this.getNotebookPreferenceWithDefault<boolean>(NotebookPreferences.OUTPUT_WORD_WRAP)!,
outputLineLimit: this.getNotebookPreferenceWithDefault<number>(NotebookPreferences.OUTPUT_LINE_LIMIT)!
};
}
protected getNotebookPreferenceWithDefault<T>(key: string): T {
return this.preferenceService.get<T>(key, notebookPreferenceSchema.properties?.[key]?.default as T);
}
protected computeOutputLineHeight(lineHeight: number, outputFontSize: number): number {
const minimumLineHeight = 9;
if (lineHeight === 0) {
// use editor line height
lineHeight = this.editorFontInfo.lineHeight;
} else if (lineHeight < minimumLineHeight) {
// Values too small to be line heights in pixels are in ems.
let fontSize = outputFontSize;
if (fontSize === 0) {
fontSize = this.preferenceService.get<number>('editor.fontSize')!;
}
lineHeight = lineHeight * fontSize;
}
// Enforce integer, minimum constraints
lineHeight = Math.round(lineHeight);
if (lineHeight < minimumLineHeight) {
lineHeight = minimumLineHeight;
}
return lineHeight;
}
protected getOrCreateMonacoFontInfo(): BareFontInfo {
if (!this.fontInfo) {
this.fontInfo = this.createFontInfo();
this.editorPreferences.onPreferenceChanged(e => this.fontInfo = this.createFontInfo());
}
return this.fontInfo;
}
protected createFontInfo(): BareFontInfo {
return BareFontInfo.createFromRawSettings({
fontFamily: this.editorPreferences['editor.fontFamily'],
fontWeight: String(this.editorPreferences['editor.fontWeight']),
fontSize: this.editorPreferences['editor.fontSize'],
fontLigatures: this.editorPreferences['editor.fontLigatures'],
lineHeight: this.editorPreferences['editor.lineHeight'],
letterSpacing: this.editorPreferences['editor.letterSpacing'],
}, PixelRatio.getInstance(window).value);
}
}

View File

@@ -0,0 +1,121 @@
// *****************************************************************************
// 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter } from '@theia/core';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol';
import { NotebookEditorWidgetService } from './notebook-editor-widget-service';
interface RendererMessage {
editorId: string;
rendererId: string;
message: unknown
};
export interface RendererMessaging extends Disposable {
/**
* Method called when a message is received. Should return a boolean
* indicating whether a renderer received it.
*/
receiveMessage?: (rendererId: string, message: unknown) => Promise<boolean>;
/**
* Sends a message to an extension from a renderer.
*/
postMessage(rendererId: string, message: unknown): void;
}
@injectable()
export class NotebookRendererMessagingService implements Disposable {
protected readonly postMessageEmitter = new Emitter<RendererMessage>();
readonly onPostMessage = this.postMessageEmitter.event;
protected readonly willActivateRendererEmitter = new Emitter<string>();
readonly onWillActivateRenderer = this.willActivateRendererEmitter.event;
@inject(NotebookEditorWidgetService)
protected readonly editorWidgetService: NotebookEditorWidgetService;
protected readonly activations = new Map<string /* rendererId */, undefined | RendererMessage[]>();
protected readonly scopedMessaging = new Map<string /* editorId */, RendererMessaging>();
receiveMessage(editorId: string | undefined, rendererId: string, message: unknown): Promise<boolean> {
if (editorId === undefined) {
const sends = [...this.scopedMessaging.values()].map(e => e.receiveMessage?.(rendererId, message));
return Promise.all(sends).then(values => values.some(value => !!value));
}
return this.scopedMessaging.get(editorId)?.receiveMessage?.(rendererId, message) ?? Promise.resolve(false);
}
prepare(rendererId: string): void {
if (this.activations.has(rendererId)) {
return;
}
const queue: RendererMessage[] = [];
this.activations.set(rendererId, queue);
Promise.all(this.willActivateRendererEmitter.fire(rendererId)).then(() => {
for (const message of queue) {
this.postMessageEmitter.fire(message);
}
this.activations.set(rendererId, undefined);
});
}
public getScoped(editorId: string): RendererMessaging {
const existing = this.scopedMessaging.get(editorId);
if (existing) {
return existing;
}
const messaging: RendererMessaging = {
postMessage: (rendererId, message) => this.postMessage(editorId, rendererId, message),
receiveMessage: async (rendererId, message) => {
this.editorWidgetService.getNotebookEditor(editorId)?.postRendererMessage(rendererId, message);
return true;
},
dispose: () => this.scopedMessaging.delete(editorId),
};
this.scopedMessaging.set(editorId, messaging);
return messaging;
}
protected postMessage(editorId: string, rendererId: string, message: unknown): void {
if (!this.activations.has(rendererId)) {
this.prepare(rendererId);
}
const activation = this.activations.get(rendererId);
const toSend = { rendererId, editorId, message };
if (activation === undefined) {
this.postMessageEmitter.fire(toSend);
} else {
activation.push(toSend);
}
}
dispose(): void {
this.postMessageEmitter.dispose();
}
}

View File

@@ -0,0 +1,215 @@
// *****************************************************************************
// 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 { Disposable, DisposableCollection, Emitter, Resource, URI } from '@theia/core';
import { inject, injectable } from '@theia/core/shared/inversify';
import { BinaryBuffer } from '@theia/core/lib/common/buffer';
import { CellKind, NotebookData, TransientOptions } from '../../common';
import { NotebookModel, NotebookModelFactory, NotebookModelProps } from '../view-model/notebook-model';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { NotebookCellModel, NotebookCellModelFactory, NotebookCellModelProps } from '../view-model/notebook-cell-model';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { NotebookMonacoTextModelService } from './notebook-monaco-text-model-service';
import { CellEditOperation } from '../notebook-types';
export const NotebookProvider = Symbol('notebook provider');
export interface NotebookProviderInfo {
readonly notebookType: string,
readonly serializer: NotebookSerializer,
}
export interface NotebookSerializer {
options: TransientOptions;
toNotebook(data: BinaryBuffer): Promise<NotebookData>;
fromNotebook(data: NotebookData): Promise<BinaryBuffer>;
}
export interface NotebookWorkspaceEdit {
edits: {
resource: URI;
edit: CellEditOperation
}[]
}
@injectable()
export class NotebookService implements Disposable {
@inject(FileService)
protected fileService: FileService;
@inject(NotebookModelFactory)
protected notebookModelFactory: (props: NotebookModelProps) => NotebookModel;
@inject(NotebookCellModelFactory)
protected notebookCellModelFactory: (props: NotebookCellModelProps) => NotebookCellModel;
@inject(NotebookMonacoTextModelService)
protected textModelService: NotebookMonacoTextModelService;
protected willUseNotebookSerializerEmitter = new Emitter<string>();
readonly onWillUseNotebookSerializer = this.willUseNotebookSerializerEmitter.event;
protected readonly disposables = new DisposableCollection();
protected readonly notebookProviders = new Map<string, NotebookProviderInfo>();
protected readonly notebookModels = new Map<string, NotebookModel>();
protected readonly didRegisterNotebookSerializerEmitter = new Emitter<string>();
readonly onDidRegisterNotebookSerializer = this.didRegisterNotebookSerializerEmitter.event;
protected readonly didRemoveViewTypeEmitter = new Emitter<string>();
readonly onDidRemoveViewType = this.didRemoveViewTypeEmitter.event;
protected readonly willOpenNotebookTypeEmitter = new Emitter<string>();
readonly onWillOpenNotebook = this.willOpenNotebookTypeEmitter.event;
protected readonly didAddNotebookDocumentEmitter = new Emitter<NotebookModel>();
readonly onDidAddNotebookDocument = this.didAddNotebookDocumentEmitter.event;
protected readonly didRemoveNotebookDocumentEmitter = new Emitter<NotebookModel>();
readonly onDidRemoveNotebookDocument = this.didRemoveNotebookDocumentEmitter.event;
dispose(): void {
this.disposables.dispose();
}
protected readonly ready = new Deferred();
/**
* Marks the notebook service as ready. From this point on, the service will start dispatching the `onNotebookSerializer` event.
*/
markReady(): void {
this.ready.resolve();
}
registerNotebookSerializer(viewType: string, serializer: NotebookSerializer): Disposable {
if (this.notebookProviders.has(viewType)) {
throw new Error(`notebook provider for viewtype '${viewType}' already exists`);
}
this.notebookProviders.set(viewType, { notebookType: viewType, serializer });
this.didRegisterNotebookSerializerEmitter.fire(viewType);
return Disposable.create(() => {
this.notebookProviders.delete(viewType);
this.didRemoveViewTypeEmitter.fire(viewType);
});
}
async createNotebookModel(data: NotebookData, viewType: string, resource: Resource): Promise<NotebookModel> {
const dataProvider = await this.getNotebookDataProvider(viewType);
const serializer = dataProvider.serializer;
const model = this.notebookModelFactory({ data, resource, viewType, serializer });
this.notebookModels.set(resource.uri.toString(), model);
// Resolve cell text models right after creating the notebook model
// This ensures that all text models are available in the plugin host
await this.textModelService.createTextModelsForNotebook(model);
this.didAddNotebookDocumentEmitter.fire(model);
model.onDidDispose(() => {
this.notebookModels.delete(resource.uri.toString());
this.didRemoveNotebookDocumentEmitter.fire(model);
});
return model;
}
async getNotebookDataProvider(viewType: string): Promise<NotebookProviderInfo> {
try {
return await this.waitForNotebookProvider(viewType);
} catch {
throw new Error(`No provider registered for view type: '${viewType}'`);
}
}
/**
* When the application starts up, notebook providers from plugins are not registered yet.
* It takes a few seconds for the plugin host to start so that notebook data providers can be registered.
* This methods waits until the notebook provider is registered.
*/
protected waitForNotebookProvider(type: string): Promise<NotebookProviderInfo> {
const existing = this.notebookProviders.get(type);
if (existing) {
return Promise.resolve(existing);
}
const deferred = new Deferred<NotebookProviderInfo>();
// 20 seconds of timeout
const timeoutDuration = 20_000;
// Must declare these variables where they can be captured by the closure
let disposable: Disposable;
// eslint-disable-next-line
let timeout: ReturnType<typeof setTimeout>;
// eslint-disable-next-line
disposable = this.onDidRegisterNotebookSerializer(viewType => {
if (viewType === type) {
clearTimeout(timeout);
disposable.dispose();
const newProvider = this.notebookProviders.get(type);
if (!newProvider) {
deferred.reject(new Error(`Notebook provider for type ${type} is invalid`));
} else {
deferred.resolve(newProvider);
}
}
});
timeout = setTimeout(() => {
clearTimeout(timeout);
disposable.dispose();
deferred.reject(new Error(`Timed out while waiting for notebook serializer for type ${type} to be registered`));
}, timeoutDuration);
this.ready.promise.then(() => {
this.willUseNotebookSerializerEmitter.fire(type);
});
return deferred.promise;
}
getNotebookEditorModel(uri: URI): NotebookModel | undefined {
return this.notebookModels.get(uri.toString());
}
getNotebookModels(): Iterable<NotebookModel> {
return this.notebookModels.values();
}
async willOpenNotebook(type: string): Promise<void> {
return this.willOpenNotebookTypeEmitter.sequence(async listener => listener(type));
}
listNotebookDocuments(): NotebookModel[] {
return [...this.notebookModels.values()];
}
applyWorkspaceEdit(workspaceEdit: NotebookWorkspaceEdit): boolean {
try {
workspaceEdit.edits.forEach(edit => {
const notebook = this.getNotebookEditorModel(edit.resource);
notebook?.applyEdits([edit.edit], true);
});
return true;
} catch (e) {
console.error(e);
return false;
}
}
getCodeCellLanguage(model: NotebookModel): string {
const firstCodeCell = model.cells.find(cellModel => cellModel.cellKind === CellKind.Code);
const cellLanguage = firstCodeCell?.language ?? 'plaintext';
return cellLanguage;
}
}

View File

@@ -0,0 +1,551 @@
/********************************************************************************
* 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
********************************************************************************/
:root {
--theia-notebook-markdown-size: 17px;
--theia-notebook-cell-editor-margin-right: 10px;
}
.theia-notebook-cell-list {
position: absolute;
top: 0;
width: 100%;
overflow-y: auto;
list-style: none;
padding-left: 0px;
background-color: var(--theia-notebook-editorBackground);
z-index: 0;
pointer-events: none;
}
.theia-notebook-cell-output-webview {
padding: 5px 0px;
margin: 0px 15px 0px 50px;
width: calc(100% - 60px);
position: absolute;
z-index: 0;
}
.theia-notebook-cell {
display: flex;
margin: 10px 0px;
}
.theia-notebook-cell:focus {
outline: none;
}
.theia-notebook-cell.draggable {
cursor: grab;
}
.theia-notebook-cell:hover .theia-notebook-cell-marker {
visibility: visible;
}
.theia-notebook-cell-marker {
background-color: var(--theia-notebook-inactiveFocusedCellBorder);
width: 3px;
margin: 0px 8px 0px 4px;
border-radius: 4px;
visibility: hidden;
}
.theia-notebook-cell-marker-selected {
visibility: visible;
background-color: var(--theia-notebook-focusedCellBorder);
}
.theia-notebook-cell-marker:hover {
width: 5px;
margin: 0px 6px 0px 4px;
}
.theia-notebook-cell-content {
flex: 1;
/* needs this set width because of monaco. 56px is sidebar + gap to sidebar */
width: calc(100% - 56px);
}
/* Rendered Markdown Content */
.theia-notebook-markdown-content {
pointer-events: all;
padding: 8px 16px 8px 0px;
font-size: var(--theia-notebook-markdown-size);
}
.theia-notebook-markdown-content > * {
font-weight: 400;
}
.theia-notebook-markdown-content > *:first-child {
margin-top: 0;
padding-top: 0;
}
.theia-notebook-markdown-content > *:last-child {
margin-bottom: 0;
padding-bottom: 0;
}
.theia-notebook-markdown-sidebar {
width: 35px;
}
/* Markdown cell edit mode */
.theia-notebook-cell-content:has(
.theia-notebook-markdown-editor-container > .theia-notebook-cell-editor
) {
pointer-events: all;
margin-right: var(--theia-notebook-cell-editor-margin-right);
outline: 1px solid var(--theia-notebook-cellBorderColor);
}
/* Markdown cell edit mode focused */
.theia-notebook-cell.focused
.theia-notebook-cell-content:has(
.theia-notebook-markdown-editor-container > .theia-notebook-cell-editor
) {
outline-color: var(--theia-notebook-focusedEditorBorder);
}
.theia-notebook-empty-markdown {
opacity: 0.6;
}
.theia-notebook-cell-editor {
padding: 10px 10px 0 10px;
}
.theia-notebook-cell-editor .monaco-editor {
outline: none;
}
.theia-notebook-cell-editor-container {
pointer-events: all;
width: calc(100% - 46px);
flex: 1;
outline: 1px solid var(--theia-notebook-cellBorderColor);
margin: 0px 16px 0px 10px;
}
/* Only mark an editor cell focused if the editor has focus */
.theia-notebook-cell-editor-container:has(.monaco-editor.focused) {
outline-color: var(--theia-notebook-focusedEditorBorder);
}
.notebook-cell-status {
display: flex;
flex-direction: row;
font-size: 12px;
height: 16px;
}
.notebook-cell-status-left {
display: flex;
flex-direction: row;
flex-grow: 1;
}
.notebook-cell-language-label {
padding: 0 5px;
}
.notebook-cell-language-label:hover {
cursor: pointer;
background-color: var(--theia-toolbar-hoverBackground);
}
.notebook-cell-status-item {
margin: 0 3px;
padding: 0 3px;
display: flex;
align-items: center;
}
.theia-notebook-cell-toolbar {
pointer-events: all;
border: 1px solid var(--theia-notebook-cellToolbarSeparator);
display: flex;
position: absolute;
margin: -20px 0 0 66px;
padding: 2px;
background-color: var(--theia-editor-background);
}
.theia-notebook-cell-sidebar-toolbar {
display: flex;
flex-direction: column;
padding: 2px;
flex-grow: 1;
}
.theia-notebook-cell-sidebar {
pointer-events: all;
display: flex;
}
.theia-notebook-cell-sidebar-actions {
display: flex;
flex-direction: column;
}
.theia-notebook-code-cell-execution-order {
display: block;
font-family: var(--monaco-monospace-font);
font-size: 10px;
opacity: 0.7;
text-align: center;
white-space: pre;
padding: 5px 0;
}
.theia-notebook-cell-toolbar-item {
height: 18px;
width: 18px;
}
.theia-notebook-cell-toolbar-item:hover {
background-color: var(--theia-toolbar-hoverBackground);
}
.theia-notebook-cell-toolbar-item:active {
background-color: var(--theia-toolbar-active);
}
.theia-notebook-cell-divider {
pointer-events: all;
height: 25px;
width: 100%;
}
.theia-notebook-cell-with-sidebar {
display: flex;
flex-direction: row;
}
.theia-notebook-main-container {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.theia-notebook-main-container:focus {
outline: none;
}
.theia-notebook-main-container .theia-notebook-main-loading-indicator {
/* `progress-animation` is defined in `packages/core/src/browser/style/progress-bar.css` */
animation: progress-animation 1.8s 0s infinite
cubic-bezier(0.645, 0.045, 0.355, 1);
background-color: var(--theia-progressBar-background);
height: 2px;
}
.error-message {
justify-content: center;
margin: 0 50px;
text-align: center;
}
.error-message > span {
color: var(--theia-errorForeground);
font-size: 40px !important;
}
.theia-notebook-viewport {
display: flex;
overflow: hidden;
height: 100%;
}
.theia-notebook-scroll-container {
flex: 1;
overflow: hidden;
position: relative;
}
.theia-notebook-main-toolbar {
background: var(--theia-editor-background);
display: flex;
flex-direction: row;
z-index: 1;
/*needed to be on rendered on top of monaco editors*/
}
.theia-notebook-main-toolbar-item {
height: 22px;
display: flex;
align-items: center;
margin: 0 4px;
padding: 2px;
text-align: center;
color: var(--theia-foreground) !important;
cursor: pointer;
}
.theia-notebook-main-toolbar-item.theia-mod-disabled:hover {
background-color: transparent;
cursor: default;
}
.theia-notebook-main-toolbar-item-text {
padding: 0 4px;
white-space: nowrap;
}
.theia-notebook-toolbar-separator {
width: 1px;
background-color: var(--theia-notebook-cellToolbarSeparator);
margin: 0 4px;
}
.theia-notebook-add-cell-buttons {
justify-content: center;
display: flex;
}
.theia-notebook-add-cell-button {
border: 1px solid var(--theia-notebook-cellToolbarSeparator);
background-color: var(--theia-editor-background);
color: var(--theia-foreground);
display: flex;
height: 24px;
margin: 0 8px;
padding: 2px 4px;
}
.theia-notebook-add-cell-button:hover {
background-color: var(--theia-toolbar-hoverBackground);
}
.theia-notebook-add-cell-button:active {
background-color: var(--theia-toolbar-active);
}
.theia-notebook-add-cell-button > * {
vertical-align: middle;
}
.theia-notebook-add-cell-button-icon::before {
font: normal normal normal 14px/1 codicon;
}
.theia-notebook-add-cell-button-text {
margin: 1px 0 0 4px;
}
.theia-notebook-cell-drop-indicator {
height: 2px;
background-color: var(--theia-notebook-focusedCellBorder);
width: 100%;
}
.theia-notebook-collapsed-output-container {
width: 0;
overflow: visible;
}
.theia-notebook-collapsed-output {
text-wrap: nowrap;
padding: 4px 8px;
color: var(--theia-foreground);
margin-left: 30px;
font-size: 14px;
line-height: 22px;
opacity: 0.7;
}
.theia-notebook-drag-ghost-image {
position: absolute;
top: -99999px;
left: -99999px;
max-height: 500px;
min-height: 100px;
background-color: var(--theia-editor-background);
}
/* Notebook Find Widget */
.theia-notebook-overlay {
position: absolute;
z-index: 100;
right: 18px;
}
.theia-notebook-find-widget {
/* position: absolute;
z-index: 35;
height: 33px;
overflow: hidden; */
line-height: 19px;
transition: transform 200ms linear;
display: flex;
flex-direction: row;
padding: 0 4px;
box-sizing: border-box;
box-shadow: 0 0 8px 2px var(--theia-widget-shadow);
background-color: var(--theia-editorWidget-background);
color: var(--theia-editorWidget-foreground);
border-left: 1px solid var(--theia-widget-border);
border-right: 1px solid var(--theia-widget-border);
border-bottom: 1px solid var(--theia-widget-border);
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
.theia-notebook-find-widget.hidden {
display: none;
transform: translateY(calc(-100% - 10px));
}
.theia-notebook-find-widget.search-mode > * > *:nth-child(2) {
display: none;
}
.theia-notebook-find-widget-expand {
display: flex;
flex-direction: row;
align-items: center;
cursor: pointer;
border-radius: 0;
margin-right: 4px;
}
.theia-notebook-find-widget-expand:focus {
outline: 1px solid var(--theia-focusBorder);
}
.theia-notebook-find-widget-expand:hover {
background-color: var(--theia-toolbar-hoverBackground);
}
.theia-notebook-find-widget-buttons-first {
margin-bottom: 4px;
height: 26px;
display: flex;
flex-direction: row;
align-items: center;
}
.theia-notebook-find-widget-buttons-first > div,
.theia-notebook-find-widget-buttons-second > div {
margin-right: 4px;
}
.theia-notebook-find-widget-buttons-second {
height: 26px;
display: flex;
flex-direction: row;
align-items: center;
}
.theia-notebook-find-widget-inputs {
margin-top: 4px;
display: flex;
flex-direction: column;
}
.theia-notebook-find-widget-buttons {
margin-top: 4px;
margin-left: 4px;
display: flex;
flex-direction: column;
}
.theia-notebook-find-widget-matches-count {
width: 72px;
box-sizing: border-box;
overflow: hidden;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
.theia-notebook-find-widget-input-wrapper {
display: flex;
align-items: center;
background: var(--theia-input-background);
border-style: solid;
border-width: var(--theia-border-width);
border-color: var(--theia-input-background);
border-radius: 2px;
margin-bottom: 4px;
}
.theia-notebook-find-widget-input-wrapper:focus-within {
border-color: var(--theia-focusBorder);
}
.theia-notebook-find-widget-input-wrapper .option.enabled {
color: var(--theia-inputOption-activeForeground);
outline: 1px solid var(--theia-inputOption-activeBorder);
background-color: var(--theia-inputOption-activeBackground);
}
.theia-notebook-find-widget-input-wrapper .option {
margin: 2px;
}
.theia-notebook-find-widget-input-wrapper
.theia-notebook-find-widget-input:focus {
border: none;
outline: none;
}
.theia-notebook-find-widget-input-wrapper .theia-notebook-find-widget-input {
background: none;
border: none;
}
.theia-notebook-find-widget-replace {
margin-bottom: 4px;
}
.theia-notebook-find-widget-buttons .disabled {
opacity: 0.5;
}
mark.theia-find-match {
color: var(--theia-editor-findMatchHighlightForeground);
background-color: var(--theia-editor-findMatchHighlightBackground);
}
mark.theia-find-match.theia-find-match-selected {
color: var(--theia-editor-findMatchForeground);
background-color: var(--theia-editor-findMatchBackground);
}
.cell-status-bar-item {
align-items: center;
display: flex;
height: 16px;
margin: 0 3px;
overflow: hidden;
padding: 0 3px;
text-overflow: clip;
white-space: pre;
}
.cell-status-item-has-command {
cursor: pointer;
}
.cell-status-item-has-command:hover {
background-color: var(--theia-toolbar-hoverBackground);
}

View File

@@ -0,0 +1,469 @@
// *****************************************************************************
// 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, DisposableCollection, Emitter, Event, URI } from '@theia/core';
import { inject, injectable, interfaces, postConstruct } from '@theia/core/shared/inversify';
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
import { type MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import {
CellKind, NotebookCellCollapseState, NotebookCellInternalMetadata,
NotebookCellMetadata, CellOutput, CellData, CellOutputItem
} from '../../common';
import { NotebookCellOutputsSplice } from '../notebook-types';
import { NotebookMonacoTextModelService } from '../service/notebook-monaco-text-model-service';
import { NotebookCellOutputModel } from './notebook-cell-output-model';
import { PreferenceService } from '@theia/core/lib/common';
import { NotebookPreferences } from '../../common/notebook-preferences';
import { LanguageService } from '@theia/core/lib/browser/language-service';
import { NotebookEditorFindMatch, NotebookEditorFindMatchOptions } from '../view/notebook-find-widget';
import { Range } from '@theia/core/shared/vscode-languageserver-protocol';
export const NotebookCellModelFactory = Symbol('NotebookModelFactory');
export type NotebookCellModelFactory = (props: NotebookCellModelProps) => NotebookCellModel;
export function createNotebookCellModelContainer(parent: interfaces.Container, props: NotebookCellModelProps): interfaces.Container {
const child = parent.createChild();
child.bind(NotebookCellModelProps).toConstantValue(props);
child.bind(NotebookCellModel).toSelf();
return child;
}
export interface CellInternalMetadataChangedEvent {
readonly lastRunSuccessChanged?: boolean;
}
export interface NotebookCell {
readonly uri: URI;
handle: number;
language: string;
cellKind: CellKind;
outputs: CellOutput[];
metadata: NotebookCellMetadata;
internalMetadata: NotebookCellInternalMetadata;
text: string;
/**
* The selection of the cell. Zero-based line/character coordinates.
*/
selection: Range | undefined;
onDidChangeOutputs?: Event<NotebookCellOutputsSplice>;
onDidChangeOutputItems?: Event<CellOutput>;
onDidChangeLanguage: Event<string>;
onDidChangeMetadata: Event<void>;
onDidChangeInternalMetadata: Event<CellInternalMetadataChangedEvent>;
}
const NotebookCellModelProps = Symbol('NotebookModelProps');
export interface NotebookCellModelProps {
readonly uri: URI,
readonly handle: number,
source: string,
language: string,
readonly cellKind: CellKind,
outputs: CellOutput[],
metadata?: NotebookCellMetadata | undefined,
internalMetadata?: NotebookCellInternalMetadata | undefined,
readonly collapseState?: NotebookCellCollapseState | undefined,
}
@injectable()
export class NotebookCellModel implements NotebookCell, Disposable {
protected readonly onDidChangeOutputsEmitter = new Emitter<NotebookCellOutputsSplice>();
readonly onDidChangeOutputs = this.onDidChangeOutputsEmitter.event;
protected readonly onDidChangeOutputItemsEmitter = new Emitter<CellOutput>();
readonly onDidChangeOutputItems = this.onDidChangeOutputItemsEmitter.event;
protected readonly onDidChangeContentEmitter = new Emitter<'content' | 'language' | 'mime'>();
readonly onDidChangeContent = this.onDidChangeContentEmitter.event;
protected readonly onDidChangeMetadataEmitter = new Emitter<void>();
readonly onDidChangeMetadata = this.onDidChangeMetadataEmitter.event;
protected readonly onDidChangeInternalMetadataEmitter = new Emitter<CellInternalMetadataChangedEvent>();
readonly onDidChangeInternalMetadata = this.onDidChangeInternalMetadataEmitter.event;
protected readonly onDidChangeLanguageEmitter = new Emitter<string>();
readonly onDidChangeLanguage = this.onDidChangeLanguageEmitter.event;
protected readonly onDidChangeEditorOptionsEmitter = new Emitter<MonacoEditor.IOptions>();
readonly onDidChangeEditorOptions = this.onDidChangeEditorOptionsEmitter.event;
protected readonly outputVisibilityChangeEmitter = new Emitter<boolean>();
readonly onDidChangeOutputVisibility = this.outputVisibilityChangeEmitter.event;
protected readonly onDidFindMatchesEmitter = new Emitter<NotebookCodeEditorFindMatch[]>();
readonly onDidFindMatches: Event<NotebookCodeEditorFindMatch[]> = this.onDidFindMatchesEmitter.event;
protected readonly onDidSelectFindMatchEmitter = new Emitter<NotebookCodeEditorFindMatch>();
readonly onDidSelectFindMatch: Event<NotebookCodeEditorFindMatch> = this.onDidSelectFindMatchEmitter.event;
protected onDidRequestCenterEditorEmitter = new Emitter<void>();
readonly onDidRequestCenterEditor = this.onDidRequestCenterEditorEmitter.event;
protected onDidCellHeightChangeEmitter = new Emitter<number>();
readonly onDidCellHeightChange = this.onDidCellHeightChangeEmitter.event;
@inject(NotebookCellModelProps)
protected readonly props: NotebookCellModelProps;
@inject(NotebookMonacoTextModelService)
protected readonly textModelService: NotebookMonacoTextModelService;
@inject(LanguageService)
protected readonly languageService: LanguageService;
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
get outputs(): NotebookCellOutputModel[] {
return this._outputs;
}
protected _outputs: NotebookCellOutputModel[];
get metadata(): NotebookCellMetadata {
return this._metadata;
}
set metadata(newMetadata: NotebookCellMetadata) {
this._metadata = newMetadata;
this.onDidChangeMetadataEmitter.fire();
}
protected _metadata: NotebookCellMetadata;
toDispose = new DisposableCollection();
protected _internalMetadata: NotebookCellInternalMetadata;
get internalMetadata(): NotebookCellInternalMetadata {
return this._internalMetadata;
}
set internalMetadata(newInternalMetadata: NotebookCellInternalMetadata) {
const lastRunSuccessChanged = this._internalMetadata.lastRunSuccess !== newInternalMetadata.lastRunSuccess;
newInternalMetadata = {
...newInternalMetadata,
...{ runStartTimeAdjustment: computeRunStartTimeAdjustment(this._internalMetadata, newInternalMetadata) }
};
this._internalMetadata = newInternalMetadata;
this.onDidChangeInternalMetadataEmitter.fire({ lastRunSuccessChanged });
}
protected textModel?: MonacoEditorModel;
get text(): string {
return this.textModel && !this.textModel.isDisposed() ? this.textModel.getText() : this.source;
}
get isTextModelWritable(): boolean {
return !this.textModel || !this.textModel.readOnly;
}
get source(): string {
return this.props.source;
}
set source(source: string) {
this.props.source = source;
this.textModel?.textEditorModel.setValue(source);
}
get language(): string {
return this.props.language;
}
set language(newLanguage: string) {
if (this.language === newLanguage) {
return;
}
if (this.textModel) {
this.textModel.setLanguageId(newLanguage);
}
this.props.language = newLanguage;
this.onDidChangeLanguageEmitter.fire(newLanguage);
this.onDidChangeContentEmitter.fire('language');
}
get languageName(): string {
return this.languageService.getLanguage(this.language)?.name ?? this.language;
}
get uri(): URI {
return this.props.uri;
}
get handle(): number {
return this.props.handle;
}
get cellKind(): CellKind {
return this.props.cellKind;
}
protected _editorOptions: MonacoEditor.IOptions = {};
get editorOptions(): Readonly<MonacoEditor.IOptions> {
return this._editorOptions;
}
set editorOptions(options: MonacoEditor.IOptions) {
this._editorOptions = options;
this.onDidChangeEditorOptionsEmitter.fire(options);
}
protected _outputVisible: boolean = true;
get outputVisible(): boolean {
return this._outputVisible;
}
set outputVisible(visible: boolean) {
if (this._outputVisible !== visible) {
this._outputVisible = visible;
this.outputVisibilityChangeEmitter.fire(visible);
}
}
protected _selection: Range | undefined = undefined;
get selection(): Range | undefined {
return this._selection;
}
set selection(selection: Range | undefined) {
this._selection = selection;
}
protected _cellheight: number = 0;
get cellHeight(): number {
return this._cellheight;
}
set cellHeight(height: number) {
if (height !== this._cellheight) {
this.onDidCellHeightChangeEmitter.fire(height);
this._cellheight = height;
}
}
@postConstruct()
protected init(): void {
this._outputs = this.props.outputs.map(op => new NotebookCellOutputModel(op));
this._metadata = this.props.metadata ?? {};
this._internalMetadata = this.props.internalMetadata ?? {};
this.editorOptions = {
lineNumbers: this.preferenceService.get(NotebookPreferences.NOTEBOOK_LINE_NUMBERS)
};
this.toDispose.push(this.preferenceService.onPreferenceChanged(e => {
if (e.preferenceName === NotebookPreferences.NOTEBOOK_LINE_NUMBERS) {
this.editorOptions = {
...this.editorOptions,
lineNumbers: this.preferenceService.get(NotebookPreferences.NOTEBOOK_LINE_NUMBERS)
};
}
}));
}
dispose(): void {
this.onDidChangeOutputsEmitter.dispose();
this.onDidChangeOutputItemsEmitter.dispose();
this.onDidChangeContentEmitter.dispose();
this.onDidChangeMetadataEmitter.dispose();
this.onDidChangeInternalMetadataEmitter.dispose();
this.onDidChangeLanguageEmitter.dispose();
this.toDispose.dispose();
}
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);
// update
for (let i = 0; i < commonLen; i++) {
const currentOutput = this.outputs[splice.start + i];
const newOutput = splice.newOutputs[i];
this.replaceOutputData(currentOutput.outputId, newOutput);
}
this.outputs.splice(splice.start + commonLen, splice.deleteCount - commonLen, ...splice.newOutputs.slice(commonLen).map(op => new NotebookCellOutputModel(op)));
this.onDidChangeOutputsEmitter.fire({ start: splice.start + commonLen, deleteCount: splice.deleteCount - commonLen, newOutputs: splice.newOutputs.slice(commonLen) });
} else {
this.outputs.splice(splice.start, splice.deleteCount, ...splice.newOutputs.map(op => new NotebookCellOutputModel(op)));
this.onDidChangeOutputsEmitter.fire(splice);
}
}
replaceOutputData(outputId: string, newOutputData: CellOutput): boolean {
const output = this.outputs.find(out => out.outputId === outputId);
if (!output) {
return false;
}
output.replaceData(newOutputData);
this.onDidChangeOutputItemsEmitter.fire(output);
return true;
}
changeOutputItems(outputId: string, append: boolean, items: CellOutputItem[]): boolean {
const output = this.outputs.find(out => out.outputId === outputId);
if (!output) {
return false;
}
if (append) {
output.appendData(items);
} else {
output.replaceData({ outputId: outputId, outputs: items, metadata: output.metadata });
}
this.onDidChangeOutputItemsEmitter.fire(output);
return true;
}
getData(): CellData {
return {
cellKind: this.cellKind,
language: this.language,
outputs: this.outputs.map(output => output.getData()),
source: this.text,
collapseState: this.props.collapseState,
internalMetadata: this.internalMetadata,
metadata: this.metadata
};
}
async resolveTextModel(): Promise<MonacoEditorModel> {
if (this.textModel) {
return this.textModel;
}
const ref = await this.textModelService.getOrCreateNotebookCellModelReference(this.uri);
this.textModel = ref.object;
this.toDispose.push(ref);
this.toDispose.push(this.textModel.onDidChangeContent(e => {
this.props.source = e.model.getText();
this.onDidChangeContentEmitter.fire('content');
}));
return ref.object;
}
restartOutputRenderer(outputId: string): void {
const output = this.outputs.find(out => out.outputId === outputId);
if (output) {
this.onDidChangeOutputItemsEmitter.fire(output);
}
}
onMarkdownFind: ((options: NotebookEditorFindMatchOptions) => NotebookEditorFindMatch[]) | undefined;
showMatch(selected: NotebookCodeEditorFindMatch): void {
this.onDidSelectFindMatchEmitter.fire(selected);
}
findMatches(options: NotebookEditorFindMatchOptions): NotebookEditorFindMatch[] {
if (this.cellKind === CellKind.Markup && this.onMarkdownFind) {
return this.onMarkdownFind(options) ?? [];
}
if (!this.textModel) {
return [];
}
const matches = options.search ? this.textModel.findMatches({
searchString: options.search,
isRegex: options.regex,
matchCase: options.matchCase,
matchWholeWord: options.wholeWord
}) : [];
const editorFindMatches = matches.map(match => new NotebookCodeEditorFindMatch(this, match.range, this.textModel!));
this.onDidFindMatchesEmitter.fire(editorFindMatches);
return editorFindMatches;
}
replaceAll(matches: NotebookCodeEditorFindMatch[], value: string): void {
const editOperations = matches.map(match => ({
range: {
startColumn: match.range.start.character,
startLineNumber: match.range.start.line,
endColumn: match.range.end.character,
endLineNumber: match.range.end.line
},
text: value
}));
this.textModel?.textEditorModel.pushEditOperations(
// eslint-disable-next-line no-null/no-null
null,
editOperations,
// eslint-disable-next-line no-null/no-null
() => null);
}
}
export interface NotebookCellFindMatches {
matches: NotebookEditorFindMatch[];
selected: NotebookEditorFindMatch;
}
export class NotebookCodeEditorFindMatch implements NotebookEditorFindMatch {
selected = false;
constructor(readonly cell: NotebookCellModel, readonly range: Range, readonly textModel: MonacoEditorModel) {
}
show(): void {
this.cell.showMatch(this);
}
replace(value: string): void {
this.textModel.textEditorModel.pushEditOperations(
// eslint-disable-next-line no-null/no-null
null,
[{
range: {
startColumn: this.range.start.character,
startLineNumber: this.range.start.line,
endColumn: this.range.end.character,
endLineNumber: this.range.end.line
},
text: value
}],
// eslint-disable-next-line no-null/no-null
() => null);
}
}
function computeRunStartTimeAdjustment(oldMetadata: NotebookCellInternalMetadata, newMetadata: NotebookCellInternalMetadata): number | undefined {
if (oldMetadata.runStartTime !== newMetadata.runStartTime && typeof newMetadata.runStartTime === 'number') {
const offset = Date.now() - newMetadata.runStartTime;
return offset < 0 ? Math.abs(offset) : 0;
} else {
return newMetadata.runStartTimeAdjustment;
}
}

View File

@@ -0,0 +1,92 @@
// *****************************************************************************
// 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 { Disposable, Emitter } from '@theia/core';
import { CellOutput, CellOutputItem, isTextStreamMime } from '../../common';
import { compressOutputItemStreams } from '../notebook-output-utils';
export class NotebookCellOutputModel implements Disposable {
private didChangeDataEmitter = new Emitter<void>();
readonly onDidChangeData = this.didChangeDataEmitter.event;
get outputId(): string {
return this.rawOutput.outputId;
}
get outputs(): CellOutputItem[] {
return this.rawOutput.outputs || [];
}
get metadata(): Record<string, unknown> | undefined {
return this.rawOutput.metadata;
}
constructor(protected rawOutput: CellOutput) { }
replaceData(rawData: CellOutput): void {
this.rawOutput = rawData;
this.optimizeOutputItems();
this.didChangeDataEmitter.fire();
}
appendData(items: CellOutputItem[]): void {
this.rawOutput.outputs.push(...items);
this.optimizeOutputItems();
this.didChangeDataEmitter.fire();
}
dispose(): void {
this.didChangeDataEmitter.dispose();
}
getData(): CellOutput {
return {
outputs: this.outputs,
metadata: this.metadata,
outputId: this.outputId
};
}
private optimizeOutputItems(): void {
if (this.outputs.length > 1 && this.outputs.every(item => isTextStreamMime(item.mime))) {
// Look for the mimes in the items, and keep track of their order.
// Merge the streams into one output item, per mime type.
const mimeOutputs = new Map<string, Uint8Array[]>();
const mimeTypes: string[] = [];
this.outputs.forEach(item => {
let items: Uint8Array[];
if (mimeOutputs.has(item.mime)) {
items = mimeOutputs.get(item.mime)!;
} else {
items = [];
mimeOutputs.set(item.mime, items);
mimeTypes.push(item.mime);
}
items.push(item.data.buffer);
});
this.outputs.length = 0;
mimeTypes.forEach(mime => {
const compressionResult = compressOutputItemStreams(mimeOutputs.get(mime)!);
this.outputs.push({
mime,
data: compressionResult.data
});
});
}
}
}

View File

@@ -0,0 +1,532 @@
// *****************************************************************************
// 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 { Disposable, Emitter, Event, QueueableEmitter, Resource, URI } from '@theia/core';
import { Saveable, SaveOptions } from '@theia/core/lib/browser';
import {
CellData, CellEditType, CellUri, NotebookCellInternalMetadata,
NotebookCellMetadata,
NotebookCellsChangeType, NotebookCellTextModelSplice, NotebookData,
NotebookDocumentMetadata,
} from '../../common';
import {
NotebookContentChangedEvent, NotebookModelWillAddRemoveEvent,
CellEditOperation, NullablePartialNotebookCellInternalMetadata,
NullablePartialNotebookCellMetadata
} from '../notebook-types';
import { NotebookSerializer } from '../service/notebook-service';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { NotebookCellModel, NotebookCellModelFactory, NotebookCodeEditorFindMatch } from './notebook-cell-model';
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';
import type { NotebookModelResolverService } from '../service/notebook-model-resolver-service';
import { BinaryBuffer } from '@theia/core/lib/common/buffer';
import { NotebookEditorFindMatch, NotebookEditorFindMatchOptions } from '../view/notebook-find-widget';
export const NotebookModelFactory = Symbol('NotebookModelFactory');
export function createNotebookModelContainer(parent: interfaces.Container, props: NotebookModelProps): interfaces.Container {
const child = parent.createChild();
child.bind(NotebookModelProps).toConstantValue(props);
child.bind(NotebookModel).toSelf();
return child;
}
export const NotebookModelResolverServiceProxy = Symbol('NotebookModelResolverServiceProxy');
const NotebookModelProps = Symbol('NotebookModelProps');
export interface NotebookModelProps {
data: NotebookData;
resource: Resource;
viewType: string;
serializer: NotebookSerializer;
}
@injectable()
export class NotebookModel implements Saveable, Disposable {
protected readonly onDirtyChangedEmitter = new Emitter<void>();
readonly onDirtyChanged = this.onDirtyChangedEmitter.event;
protected readonly onDidSaveNotebookEmitter = new Emitter<void>();
readonly onDidSaveNotebook = this.onDidSaveNotebookEmitter.event;
protected readonly onDidAddOrRemoveCellEmitter = new Emitter<NotebookModelWillAddRemoveEvent>();
readonly onDidAddOrRemoveCell = this.onDidAddOrRemoveCellEmitter.event;
protected readonly onDidChangeContentEmitter = new QueueableEmitter<NotebookContentChangedEvent>();
readonly onDidChangeContent = this.onDidChangeContentEmitter.event;
protected readonly onContentChangedEmitter = new Emitter<void>();
readonly onContentChanged = this.onContentChangedEmitter.event;
protected readonly onDidDisposeEmitter = new Emitter<void>();
readonly onDidDispose = this.onDidDisposeEmitter.event;
get onDidChangeReadOnly(): Event<boolean | MarkdownString> {
return this.props.resource.onDidChangeReadOnly ?? Event.None;
}
@inject(FileService)
protected readonly fileService: FileService;
@inject(UndoRedoService)
protected readonly undoRedoService: UndoRedoService;
@inject(NotebookModelProps)
protected props: NotebookModelProps;
@inject(NotebookCellModelFactory)
protected cellModelFactory: NotebookCellModelFactory;
@inject(NotebookModelResolverServiceProxy)
protected modelResolverService: NotebookModelResolverService;
protected nextHandle: number = 0;
protected _dirty = false;
set dirty(dirty: boolean) {
const oldState = this._dirty;
this._dirty = dirty;
if (oldState !== dirty) {
this.onDirtyChangedEmitter.fire();
}
}
get dirty(): boolean {
return this._dirty;
}
get readOnly(): boolean | MarkdownString {
return this.props.resource.readOnly ?? false;
}
protected _selectedText = '';
set selectedText(value: string) {
this._selectedText = value;
}
get selectedText(): string {
return this._selectedText;
}
protected dirtyCells: NotebookCellModel[] = [];
cells: NotebookCellModel[];
get uri(): URI {
return this.props.resource.uri;
}
get viewType(): string {
return this.props.viewType;
}
metadata: NotebookDocumentMetadata = {};
@postConstruct()
initialize(): void {
this.dirty = false;
this.cells = this.props.data.cells.map((cell, index) => this.cellModelFactory({
uri: CellUri.generate(this.props.resource.uri, index),
handle: index,
source: cell.source,
language: cell.language,
cellKind: cell.cellKind,
outputs: cell.outputs,
metadata: cell.metadata,
internalMetadata: cell.internalMetadata,
collapseState: cell.collapseState
}));
this.addCellOutputListeners(this.cells);
this.metadata = this.props.data.metadata;
this.nextHandle = this.cells.length;
}
dispose(): void {
this.onDirtyChangedEmitter.dispose();
this.onDidSaveNotebookEmitter.dispose();
this.onDidAddOrRemoveCellEmitter.dispose();
this.onDidChangeContentEmitter.dispose();
this.cells.forEach(cell => cell.dispose());
this.onDidDisposeEmitter.fire();
}
async save(options?: SaveOptions): Promise<void> {
this.dirtyCells = [];
this.dirty = false;
const serializedNotebook = await this.serialize();
this.fileService.writeFile(this.uri, serializedNotebook);
this.onDidSaveNotebookEmitter.fire();
}
createSnapshot(): Saveable.Snapshot {
return {
read: () => JSON.stringify(this.getData())
};
}
serialize(): Promise<BinaryBuffer> {
return this.props.serializer.fromNotebook(this.getData());
}
async applySnapshot(snapshot: Saveable.Snapshot): Promise<void> {
const rawData = Saveable.Snapshot.read(snapshot);
if (!rawData) {
throw new Error('could not read notebook snapshot');
}
const data = JSON.parse(rawData) as NotebookData;
this.setData(data);
}
async revert(options?: Saveable.RevertOptions): Promise<void> {
if (!options?.soft) {
// Load the data from the file again
try {
const data = await this.modelResolverService.resolveExistingNotebookData(this.props.resource, this.props.viewType);
this.setData(data, false);
} catch (err) {
console.error('Failed to revert notebook', err);
}
}
this.dirty = false;
}
isDirty(): boolean {
return this.dirty;
}
cellDirtyChanged(cell: NotebookCellModel, dirtyState: boolean): void {
if (dirtyState) {
this.dirtyCells.push(cell);
} else {
this.dirtyCells.splice(this.dirtyCells.indexOf(cell), 1);
}
this.dirty = this.dirtyCells.length > 0;
// Only fire `onContentChangedEmitter` here, because `onDidChangeContentEmitter` is used for model level changes only
// However, this event indicates that the content of a cell has changed
this.onContentChangedEmitter.fire();
}
setData(data: NotebookData, markDirty = true): void {
// Replace all cells in the model
this.dirtyCells = [];
this.replaceCells(0, this.cells.length, data.cells, false, false);
this.metadata = data.metadata;
this.dirty = markDirty;
this.onDidChangeContentEmitter.fire();
}
getData(): NotebookData {
return {
cells: this.cells.map(cell => cell.getData()),
metadata: this.metadata
};
}
undo(): void {
if (!this.readOnly) {
this.undoRedoService.undo(this.uri);
}
}
redo(): void {
if (!this.readOnly) {
this.undoRedoService.redo(this.uri);
}
}
private addCellOutputListeners(cells: NotebookCellModel[]): void {
for (const cell of cells) {
cell.onDidChangeOutputs(() => {
this.dirty = true;
});
}
}
getVisibleCells(): NotebookCellModel[] {
return this.cells;
}
applyEdits(rawEdits: CellEditOperation[], computeUndoRedo: boolean): void {
const editsWithDetails = rawEdits.map((edit, index) => {
let cellIndex: number = -1;
if ('index' in edit) {
cellIndex = edit.index;
} else if ('handle' in edit) {
cellIndex = this.getCellIndexByHandle(edit.handle);
} else if ('outputId' in edit) {
cellIndex = this.cells.findIndex(cell => cell.outputs.some(output => output.outputId === edit.outputId));
}
return {
edit,
cellIndex,
end: edit.editType === CellEditType.Replace ? edit.index + edit.count : cellIndex,
originalIndex: index
};
});
for (const { edit, cellIndex } of editsWithDetails) {
const cell = this.cells[cellIndex];
if (cell) {
this.cellDirtyChanged(cell, true);
}
switch (edit.editType) {
case CellEditType.Replace:
this.replaceCells(edit.index, edit.count, edit.cells, computeUndoRedo, true);
break;
case CellEditType.Output: {
if (edit.append) {
cell.spliceNotebookCellOutputs({ deleteCount: 0, newOutputs: edit.outputs, start: cell.outputs.length });
} else {
// could definitely be more efficient. See vscode __spliceNotebookCellOutputs2
// For now, just replace the whole existing output with the new output
cell.spliceNotebookCellOutputs({ start: 0, deleteCount: edit.deleteCount ?? cell.outputs.length, newOutputs: edit.outputs });
}
this.onDidChangeContentEmitter.queue({ kind: NotebookCellsChangeType.Output, index: cellIndex, outputs: cell.outputs, append: edit.append ?? false });
break;
}
case CellEditType.OutputItems:
cell.changeOutputItems(edit.outputId, !!edit.append, edit.items);
this.onDidChangeContentEmitter.queue({
kind: NotebookCellsChangeType.OutputItem, index: cellIndex, outputItems: edit.items,
outputId: edit.outputId, append: edit.append ?? false
});
break;
case CellEditType.Metadata:
this.changeCellMetadata(this.cells[cellIndex], edit.metadata, false);
break;
case CellEditType.PartialMetadata:
this.changeCellMetadataPartial(this.cells[cellIndex], edit.metadata, false);
break;
case CellEditType.PartialInternalMetadata:
this.changeCellInternalMetadataPartial(this.cells[cellIndex], edit.internalMetadata);
break;
case CellEditType.CellLanguage:
this.changeCellLanguage(this.cells[cellIndex], edit.language, computeUndoRedo);
break;
case CellEditType.DocumentMetadata:
this.updateNotebookMetadata(edit.metadata, false);
break;
case CellEditType.Move:
this.moveCellToIndex(cellIndex, edit.length, edit.newIdx, computeUndoRedo);
break;
}
}
this.fireContentChange();
}
protected fireContentChange(): void {
this.onDidChangeContentEmitter.fire();
this.onContentChangedEmitter.fire();
}
protected replaceCells(start: number, deleteCount: number, newCells: CellData[], computeUndoRedo: boolean, externalEvent: boolean): void {
const cells = newCells.map(cell => {
const handle = this.nextHandle++;
return this.cellModelFactory({
uri: CellUri.generate(this.uri, handle),
handle: handle,
source: cell.source,
language: cell.language,
cellKind: cell.cellKind,
outputs: cell.outputs,
metadata: cell.metadata,
internalMetadata: cell.internalMetadata,
collapseState: cell.collapseState
});
});
this.addCellOutputListeners(cells);
const changes: NotebookCellTextModelSplice<NotebookCellModel>[] = [{
start,
deleteCount,
newItems: cells,
startHandle: this.cells[start]?.handle ?? -1 // -1 in case of new Cells are added at the end.
}];
const deletedCells = this.cells.splice(start, deleteCount, ...cells);
for (const cell of deletedCells) {
cell.dispose();
}
if (computeUndoRedo) {
this.undoRedoService.pushElement(this.uri,
async () => {
this.replaceCells(start, newCells.length, deletedCells.map(cell => cell.getData()), false, false);
this.fireContentChange();
},
async () => {
this.replaceCells(start, deleteCount, newCells, false, false);
this.fireContentChange();
}
);
}
this.onDidAddOrRemoveCellEmitter.fire({ rawEvent: { kind: NotebookCellsChangeType.ModelChange, changes }, newCellIds: cells.map(cell => cell.handle), externalEvent });
this.onDidChangeContentEmitter.queue({ kind: NotebookCellsChangeType.ModelChange, changes });
}
protected changeCellInternalMetadataPartial(cell: NotebookCellModel, internalMetadata: NullablePartialNotebookCellInternalMetadata): void {
const newInternalMetadata: NotebookCellInternalMetadata = {
...cell.internalMetadata
};
let k: keyof NotebookCellInternalMetadata;
// eslint-disable-next-line guard-for-in
for (k in internalMetadata) {
newInternalMetadata[k] = (internalMetadata[k] ?? undefined) as never;
}
cell.internalMetadata = newInternalMetadata;
this.onDidChangeContentEmitter.queue({ kind: NotebookCellsChangeType.ChangeCellInternalMetadata, index: this.cells.indexOf(cell), internalMetadata: newInternalMetadata });
}
protected updateNotebookMetadata(metadata: NotebookDocumentMetadata, computeUndoRedo: boolean): void {
const oldMetadata = this.metadata;
if (computeUndoRedo) {
this.undoRedoService.pushElement(this.uri,
async () => this.updateNotebookMetadata(oldMetadata, false),
async () => this.updateNotebookMetadata(metadata, false)
);
}
this.metadata = metadata;
this.onDidChangeContentEmitter.queue({ kind: NotebookCellsChangeType.ChangeDocumentMetadata, metadata: this.metadata });
}
protected changeCellMetadataPartial(cell: NotebookCellModel, metadata: NullablePartialNotebookCellMetadata, computeUndoRedo: boolean): void {
const newMetadata: NotebookCellMetadata = {
...cell.metadata
};
let k: keyof NullablePartialNotebookCellMetadata;
// eslint-disable-next-line guard-for-in
for (k in metadata) {
const value = metadata[k] ?? undefined;
newMetadata[k] = value as unknown;
}
this.changeCellMetadata(cell, newMetadata, computeUndoRedo);
}
protected changeCellMetadata(cell: NotebookCellModel, metadata: NotebookCellMetadata, computeUndoRedo: boolean): void {
const triggerDirtyChange = this.isCellMetadataChanged(cell.metadata, metadata);
if (triggerDirtyChange) {
if (computeUndoRedo) {
const oldMetadata = cell.metadata;
cell.metadata = metadata;
this.undoRedoService.pushElement(this.uri,
async () => { cell.metadata = oldMetadata; },
async () => { cell.metadata = metadata; }
);
}
}
cell.metadata = metadata;
this.onDidChangeContentEmitter.queue({ kind: NotebookCellsChangeType.ChangeCellMetadata, index: this.cells.indexOf(cell), metadata: cell.metadata });
}
protected changeCellLanguage(cell: NotebookCellModel, languageId: string, computeUndoRedo: boolean): void {
if (cell.language === languageId) {
return;
}
cell.language = languageId;
this.onDidChangeContentEmitter.queue({ kind: NotebookCellsChangeType.ChangeCellLanguage, index: this.cells.indexOf(cell), language: languageId });
}
protected moveCellToIndex(fromIndex: number, length: number, toIndex: number, computeUndoRedo: boolean): boolean {
if (computeUndoRedo) {
this.undoRedoService.pushElement(this.uri,
async () => {
this.moveCellToIndex(toIndex, length, fromIndex, false);
this.fireContentChange();
},
async () => {
this.moveCellToIndex(fromIndex, length, toIndex, false);
this.fireContentChange();
}
);
}
const cells = this.cells.splice(fromIndex, length);
this.cells.splice(toIndex, 0, ...cells);
this.onDidChangeContentEmitter.queue({ kind: NotebookCellsChangeType.Move, index: fromIndex, length, newIdx: toIndex, cells });
return true;
}
getCellIndexByHandle(handle: number): number {
return this.cells.findIndex(c => c.handle === handle);
}
getCellByHandle(handle: number): NotebookCellModel | undefined {
return this.cells.find(c => c.handle === handle);
}
protected isCellMetadataChanged(a: NotebookCellMetadata, b: NotebookCellMetadata): boolean {
const keys = new Set<keyof NotebookCellMetadata>([...Object.keys(a || {}), ...Object.keys(b || {})]);
for (const key of keys) {
if (a[key] !== b[key]) {
return true;
}
}
return false;
}
findMatches(options: NotebookEditorFindMatchOptions): NotebookEditorFindMatch[] {
const matches: NotebookEditorFindMatch[] = [];
for (const cell of this.cells) {
matches.push(...cell.findMatches(options));
}
return matches;
}
replaceAll(matches: NotebookEditorFindMatch[], text: string): void {
const matchMap = new Map<NotebookCellModel, NotebookCodeEditorFindMatch[]>();
for (const match of matches) {
if (match instanceof NotebookCodeEditorFindMatch) {
if (!matchMap.has(match.cell)) {
matchMap.set(match.cell, []);
}
matchMap.get(match.cell)?.push(match);
}
}
for (const [cell, cellMatches] of matchMap) {
cell.replaceAll(cellMatches, text);
}
}
}

View File

@@ -0,0 +1,138 @@
// *****************************************************************************
// Copyright (C) 2025 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 { injectable } from '@theia/core/shared/inversify';
import { NotebookCellModel } from './notebook-cell-model';
import { Disposable, Emitter } from '@theia/core';
import { NotebookModel } from './notebook-model';
export interface SelectedCellChangeEvent {
cell: NotebookCellModel | undefined;
scrollIntoView: boolean;
}
export type CellEditorFocusRequest = number | 'lastLine' | undefined;
/**
* Model containing the editor state/view information of a notebook editor. The actual notebook data can be found in the {@link NotebookModel}.
*/
@injectable()
export class NotebookViewModel implements Disposable {
protected readonly onDidChangeSelectedCellEmitter = new Emitter<SelectedCellChangeEvent>();
readonly onDidChangeSelectedCell = this.onDidChangeSelectedCellEmitter.event;
selectedCell?: NotebookCellModel;
get selectedCellViewModel(): CellViewModel | undefined {
if (this.selectedCell) {
return this.cellViewModels.get(this.selectedCell.handle);
}
}
// Cell handle to CellViewModel mapping
readonly cellViewModels: Map<number, CellViewModel> = new Map();
initDataModel(model: NotebookModel): void {
model.onDidAddOrRemoveCell(e => {
for (const cellId of e.newCellIds || []) {
const cell = model.getCellByHandle(cellId);
if (cell) {
this.cellViewModels.set(cell.handle, new CellViewModel(cell, () => {
this.cellViewModels.delete(cell.handle);
}));
}
}
if (e.newCellIds && e.newCellIds?.length > 0 && e.externalEvent) {
const lastNewCellHandle = e.newCellIds[e.newCellIds.length - 1];
const newSelectedCell = model.getCellByHandle(lastNewCellHandle)!;
this.setSelectedCell(newSelectedCell, true);
this.cellViewModels.get(newSelectedCell.handle)?.requestEdit();
} else if (this.selectedCell && !model.getCellByHandle(this.selectedCell.handle)) {
const newSelectedIndex = e.rawEvent.changes[e.rawEvent.changes.length - 1].start;
const newSelectedCell = model.cells[Math.min(newSelectedIndex, model.cells.length - 1)];
this.setSelectedCell(newSelectedCell, false);
}
});
for (const cell of model.cells) {
this.cellViewModels.set(cell.handle, new CellViewModel(cell, () => {
this.cellViewModels.delete(cell.handle);
}));
}
}
setSelectedCell(cell: NotebookCellModel, scrollIntoView: boolean = true): void {
if (this.selectedCell !== cell) {
this.selectedCell = cell;
this.onDidChangeSelectedCellEmitter.fire({ cell, scrollIntoView });
}
}
dispose(): void {
this.onDidChangeSelectedCellEmitter.dispose();
}
}
export class CellViewModel implements Disposable {
protected readonly onDidRequestCellEditChangeEmitter = new Emitter<boolean>();
readonly onDidRequestCellEditChange = this.onDidRequestCellEditChangeEmitter.event;
protected readonly onWillFocusCellEditorEmitter = new Emitter<CellEditorFocusRequest>();
readonly onWillFocusCellEditor = this.onWillFocusCellEditorEmitter.event;
protected readonly onWillBlurCellEditorEmitter = new Emitter<void>();
readonly onWillBlurCellEditor = this.onWillBlurCellEditorEmitter.event;
protected _editing: boolean = false;
get editing(): boolean {
return this._editing;
}
constructor(protected readonly cell: NotebookCellModel, protected onDispose: () => void) {
cell.toDispose.push(this);
}
requestEdit(): void {
if (this.cell.isTextModelWritable) {
this._editing = true;
this.onDidRequestCellEditChangeEmitter.fire(true);
}
}
requestStopEdit(): void {
this._editing = false;
this.onDidRequestCellEditChangeEmitter.fire(false);
}
requestFocusEditor(focusRequest?: CellEditorFocusRequest): void {
this.requestEdit();
this.onWillFocusCellEditorEmitter.fire(focusRequest);
}
requestBlurEditor(): void {
this.requestStopEdit();
this.onWillBlurCellEditorEmitter.fire();
}
dispose(): void {
this.onDispose();
this.onDidRequestCellEditChangeEmitter.dispose();
}
}

View File

@@ -0,0 +1,312 @@
// *****************************************************************************
// 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 * as React from '@theia/core/shared/react';
import { NotebookModel } from '../view-model/notebook-model';
import { NotebookCellModel, NotebookCodeEditorFindMatch } from '../view-model/notebook-cell-model';
import { SimpleMonacoEditor } from '@theia/monaco/lib/browser/simple-monaco-editor';
import { MonacoEditor, MonacoEditorServices } from '@theia/monaco/lib/browser/monaco-editor';
import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
import { IContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/common/contextkey';
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';
import { EditorExtensionsRegistry } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorExtensions';
import { ModelDecorationOptions } from '@theia/monaco-editor-core/esm/vs/editor/common/model/textModel';
import { IModelDeltaDecoration, OverviewRulerLane, TrackedRangeStickiness } from '@theia/monaco-editor-core/esm/vs/editor/common/model';
import { animationFrame } from '@theia/core/lib/browser';
import { NotebookCellEditorService } from '../service/notebook-cell-editor-service';
import { NotebookViewModel } from '../view-model/notebook-view-model';
interface CellEditorProps {
notebookModel: NotebookModel;
notebookViewModel: NotebookViewModel;
cell: NotebookCellModel;
monacoServices: MonacoEditorServices;
notebookContextManager: NotebookContextManager;
notebookCellEditorService: NotebookCellEditorService;
notebookViewportService?: NotebookViewportService;
fontInfo?: BareFontInfo;
}
const DEFAULT_EDITOR_OPTIONS: MonacoEditor.IOptions = {
...MonacoEditorProvider.inlineOptions,
minHeight: -1,
maxHeight: -1,
scrollbar: {
...MonacoEditorProvider.inlineOptions.scrollbar,
alwaysConsumeMouseWheel: false
},
lineDecorationsWidth: 10,
};
export const CURRENT_FIND_MATCH_DECORATION = ModelDecorationOptions.register({
description: 'current-find-match',
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
zIndex: 13,
className: 'currentFindMatch',
inlineClassName: 'currentFindMatchInline',
showIfCollapsed: true,
overviewRuler: {
color: 'editorOverviewRuler.findMatchForeground',
position: OverviewRulerLane.Center
}
});
export const FIND_MATCH_DECORATION = ModelDecorationOptions.register({
description: 'find-match',
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
zIndex: 10,
className: 'findMatch',
inlineClassName: 'findMatchInline',
showIfCollapsed: true,
overviewRuler: {
color: 'editorOverviewRuler.findMatchForeground',
position: OverviewRulerLane.Center
}
});
export class CellEditor extends React.Component<CellEditorProps, {}> {
protected editor?: SimpleMonacoEditor;
protected toDispose = new DisposableCollection();
protected container?: HTMLDivElement;
protected matches: NotebookCodeEditorFindMatch[] = [];
protected oldMatchDecorations: string[] = [];
protected resizeObserver?: ResizeObserver;
override componentDidMount(): void {
const cellViewModel = this.props.notebookViewModel.cellViewModels.get(this.props.cell.handle);
if (!cellViewModel) {
throw new Error('CellViewModel not found for cell ' + this.props.cell.handle);
}
this.disposeEditor();
this.toDispose.push(cellViewModel.onWillFocusCellEditor(focusRequest => {
this.editor?.getControl().focus();
const lineCount = this.editor?.getControl().getModel()?.getLineCount();
if (focusRequest && lineCount !== undefined) {
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(cellViewModel.onWillBlurCellEditor(() => this.blurEditor()));
this.toDispose.push(this.props.cell.onDidChangeEditorOptions(options => {
this.editor?.getControl().updateOptions(options);
}));
this.toDispose.push(this.props.cell.onDidChangeLanguage(language => {
this.editor?.setLanguage(language);
}));
this.toDispose.push(this.props.cell.onDidFindMatches(matches => {
this.matches = matches;
animationFrame().then(() => this.setMatches());
}));
this.toDispose.push(this.props.cell.onDidSelectFindMatch(match => this.centerEditorInView()));
this.toDispose.push(this.props.notebookViewModel.onDidChangeSelectedCell(e => {
if (e.cell !== this.props.cell && this.editor?.getControl().hasTextFocus()) {
this.blurEditor();
}
}));
if (!this.props.notebookViewportService || (this.container && this.props.notebookViewportService.isElementInViewport(this.container))) {
this.initEditor();
} else {
const disposable = this.props.notebookViewportService?.onDidChangeViewport(() => {
if (!this.editor && this.container && this.props.notebookViewportService!.isElementInViewport(this.container)) {
this.initEditor();
disposable.dispose();
}
});
this.toDispose.push(disposable);
}
this.toDispose.push(this.props.cell.onDidRequestCenterEditor(() => {
this.centerEditorInView();
}));
}
override componentWillUnmount(): void {
if (this.resizeObserver) {
if (this.container) {
this.resizeObserver.unobserve(this.container);
}
this.resizeObserver.disconnect();
this.resizeObserver = undefined;
}
this.disposeEditor();
}
protected disposeEditor(): void {
if (this.editor) {
this.props.notebookCellEditorService.editorDisposed(this.editor.uri);
}
this.toDispose.dispose();
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<void> {
const { cell, notebookModel, monacoServices } = this.props;
if (this.container) {
const editorNode = this.container;
editorNode.style.height = '';
const editorModel = await cell.resolveTextModel();
const uri = cell.uri;
this.editor = new SimpleMonacoEditor(uri,
editorModel,
editorNode,
monacoServices,
{ ...DEFAULT_EDITOR_OPTIONS, ...cell.editorOptions },
[[IContextKeyService, this.props.notebookContextManager.scopedStore]],
{ contributions: EditorExtensionsRegistry.getEditorContributions().filter(c => c.id !== 'editor.contrib.findController') });
this.toDispose.push(this.editor);
this.editor.setLanguage(cell.language);
this.toDispose.push(this.editor.getControl().onDidContentSizeChange(() => {
editorNode.style.height = this.editor!.getControl().getContentHeight() + 7 + 'px';
this.editor!.setSize({ width: -1, height: this.editor!.getControl().getContentHeight() });
}));
this.toDispose.push(this.editor.onDocumentContentChanged(e => {
notebookModel.cellDirtyChanged(cell, true);
}));
this.toDispose.push(this.editor.getControl().onDidFocusEditorText(() => {
this.props.notebookViewModel.setSelectedCell(cell, false);
this.props.notebookCellEditorService.editorFocusChanged(this.editor);
}));
this.toDispose.push(this.editor.getControl().onDidBlurEditorText(() => {
if (this.props.notebookCellEditorService.getActiveCell()?.uri.toString() === this.props.cell.uri.toString()) {
this.props.notebookCellEditorService.editorFocusChanged(undefined);
}
}));
this.toDispose.push(this.editor.getControl().onDidChangeCursorSelection(e => {
const selectedText = this.editor!.getControl().getModel()!.getValueInRange(e.selection);
// TODO handle secondary selections
this.props.cell.selection = {
start: { line: e.selection.startLineNumber - 1, character: e.selection.startColumn - 1 },
end: { line: e.selection.endLineNumber - 1, character: e.selection.endColumn - 1 }
};
this.props.notebookModel.selectedText = selectedText;
}));
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);
}
}));
this.props.notebookCellEditorService.editorCreated(uri, this.editor);
this.setMatches();
if (this.props.notebookViewModel.selectedCell === cell) {
this.editor.getControl().focus();
}
}
}
protected setMatches(): void {
if (!this.editor) {
return;
}
const decorations: IModelDeltaDecoration[] = [];
for (const match of this.matches) {
const decoration = match.selected ? CURRENT_FIND_MATCH_DECORATION : FIND_MATCH_DECORATION;
decorations.push({
range: {
startLineNumber: match.range.start.line,
startColumn: match.range.start.character,
endLineNumber: match.range.end.line,
endColumn: match.range.end.character
},
options: decoration
});
}
this.oldMatchDecorations = this.editor.getControl()
.changeDecorations(accessor => accessor.deltaDecorations(this.oldMatchDecorations, decorations));
}
protected setContainer(component: HTMLDivElement | null): void {
if (this.resizeObserver && this.container) {
this.resizeObserver.unobserve(this.container);
}
this.container = component ?? undefined;
if (this.container) {
if (!this.resizeObserver) {
this.resizeObserver = new ResizeObserver(() => {
this.handleResize();
});
}
this.resizeObserver.observe(this.container);
}
};
protected handleResize = () => {
this.editor?.refresh();
};
protected estimateHeight(): string {
const lineHeight = this.props.fontInfo?.lineHeight ?? 20;
return this.props.cell.text.split(OS.backend.EOL).length * lineHeight + 10 + 7 + 'px';
}
override render(): React.ReactNode {
return <div className='theia-notebook-cell-editor' id={this.props.cell.uri.toString()}
ref={container => this.setContainer(container)} style={{ height: this.editor ? undefined : this.estimateHeight() }}>
</div >;
}
protected blurEditor(): void {
let parent = this.container?.parentElement;
while (parent && !parent.classList.contains('theia-notebook-cell')) {
parent = parent.parentElement;
}
if (parent) {
parent.focus();
}
}
}

View File

@@ -0,0 +1,320 @@
// *****************************************************************************
// 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 * as React from '@theia/core/shared/react';
import { CellEditType, CellKind, NotebookCellsChangeType } from '../../common';
import { NotebookCellModel } from '../view-model/notebook-cell-model';
import { NotebookModel } from '../view-model/notebook-model';
import { NotebookCellToolbarFactory } from './notebook-cell-toolbar-factory';
import { animationFrame, onDomEvent } from '@theia/core/lib/browser';
import { CommandMenu, CommandRegistry, DisposableCollection, MenuModelRegistry, nls } from '@theia/core';
import { NotebookCommands, NotebookMenus } from '../contributions/notebook-actions-contribution';
import { NotebookCellActionContribution } from '../contributions/notebook-cell-actions-contribution';
import { NotebookContextManager } from '../service/notebook-context-manager';
import { NotebookViewModel } from '../view-model/notebook-view-model';
export interface CellRenderer {
render(notebookData: NotebookModel, cell: NotebookCellModel, index: number): React.ReactNode
renderSidebar(notebookModel: NotebookModel, cell: NotebookCellModel): React.ReactNode
renderDragImage(cell: NotebookCellModel): HTMLElement
}
export function observeCellHeight(ref: HTMLDivElement | null, cell: NotebookCellModel): void {
if (ref) {
cell.cellHeight = ref?.getBoundingClientRect().height ?? 0;
new ResizeObserver(entries =>
cell.cellHeight = ref?.getBoundingClientRect().height ?? 0
).observe(ref);
}
}
interface CellListProps {
renderers: Map<CellKind, CellRenderer>;
notebookModel: NotebookModel;
notebookViewModel: NotebookViewModel;
notebookContext: NotebookContextManager;
toolbarRenderer: NotebookCellToolbarFactory;
commandRegistry: CommandRegistry;
menuRegistry: MenuModelRegistry;
}
interface NotebookCellListState {
selectedCell?: NotebookCellModel;
scrollIntoView: boolean;
dragOverIndicator: { cell: NotebookCellModel, position: 'top' | 'bottom' } | undefined;
}
export class NotebookCellListView extends React.Component<CellListProps, NotebookCellListState> {
protected toDispose = new DisposableCollection();
protected static dragGhost: HTMLElement | undefined;
protected cellListRef: React.RefObject<HTMLUListElement> = React.createRef();
constructor(props: CellListProps) {
super(props);
this.state = { selectedCell: props.notebookViewModel.selectedCell, dragOverIndicator: undefined, scrollIntoView: true };
this.toDispose.push(props.notebookModel.onDidAddOrRemoveCell(e => {
if (e.newCellIds && e.newCellIds.length > 0) {
this.setState({
...this.state,
selectedCell: props.notebookViewModel.selectedCell,
scrollIntoView: true
});
} else {
this.setState({
...this.state,
selectedCell: props.notebookViewModel.selectedCell,
scrollIntoView: false
});
}
}));
this.toDispose.push(props.notebookModel.onDidChangeContent(events => {
if (events.some(e => e.kind === NotebookCellsChangeType.Move)) {
// When a cell has been moved, we need to rerender the whole component
this.forceUpdate();
}
}));
this.toDispose.push(props.notebookViewModel.onDidChangeSelectedCell(e => {
this.setState({
...this.state,
selectedCell: e.cell,
scrollIntoView: e.scrollIntoView
});
}));
this.toDispose.push(onDomEvent(document, 'focusin', () => {
animationFrame().then(() => {
if (!this.cellListRef.current) {
return;
}
let hasCellFocus = false;
let hasFocus = false;
if (this.cellListRef.current.contains(document.activeElement)) {
if (this.props.notebookViewModel.selectedCell) {
hasCellFocus = true;
}
hasFocus = true;
}
this.props.notebookContext.changeCellFocus(hasCellFocus);
this.props.notebookContext.changeCellListFocus(hasFocus);
});
}));
}
override componentWillUnmount(): void {
this.toDispose.dispose();
}
override render(): React.ReactNode {
return <ul className='theia-notebook-cell-list' ref={this.cellListRef} onDragStart={e => this.onDragStart(e)}>
{this.props.notebookModel.getVisibleCells()
.map((cell, index) => {
const cellViewModel = this.props.notebookViewModel.cellViewModels.get(cell.handle);
return <React.Fragment key={'cell-' + cell.handle}>
<NotebookCellDivider
menuRegistry={this.props.menuRegistry}
isVisible={() => this.isEnabled()}
onAddNewCell={handler => this.onAddNewCell(handler, index)}
onDrop={e => this.onDrop(e, index)}
onDragOver={e => this.onDragOver(e, cell, 'top')} />
<CellDropIndicator visible={this.shouldRenderDragOverIndicator(cell, 'top')} />
<li className={'theia-notebook-cell' + (this.state.selectedCell === cell ? ' focused' : '') + (this.isEnabled() ? ' draggable' : '')}
onDragEnd={e => {
NotebookCellListView.dragGhost?.remove();
this.setState({ ...this.state, dragOverIndicator: undefined });
}}
onDragOver={e => this.onDragOver(e, cell)}
onDrop={e => this.onDrop(e, index)}
draggable={true}
tabIndex={-1}
data-cell-handle={cell.handle}
ref={ref => {
if (ref && cell === this.state.selectedCell && this.state.scrollIntoView) {
ref.scrollIntoView({ block: 'nearest' });
if (cell.cellKind === CellKind.Markup && !cellViewModel?.editing) {
ref.focus();
}
}
}}
onClick={e => {
this.setState({ ...this.state, selectedCell: cell });
this.props.notebookViewModel.setSelectedCell(cell, false);
}}
>
<div className='theia-notebook-cell-sidebar'>
<div className={'theia-notebook-cell-marker' + (this.state.selectedCell === cell ? ' theia-notebook-cell-marker-selected' : '')}></div>
{this.renderCellSidebar(cell)}
</div>
<div className='theia-notebook-cell-content'>
{this.renderCellContent(cell, index)}
</div>
{this.state.selectedCell === cell &&
this.props.toolbarRenderer.renderCellToolbar(NotebookCellActionContribution.ACTION_MENU, cell, {
contextMenuArgs: () => [cell], commandArgs: () => [this.props.notebookModel, cell]
})
}
</li>
<CellDropIndicator visible={this.shouldRenderDragOverIndicator(cell, 'bottom')} />
</React.Fragment>;
})
}
<NotebookCellDivider
menuRegistry={this.props.menuRegistry}
isVisible={() => this.isEnabled()}
onAddNewCell={handler => this.onAddNewCell(handler, this.props.notebookModel.cells.length)}
onDrop={e => this.onDrop(e, this.props.notebookModel.cells.length - 1)}
onDragOver={e => this.onDragOver(e, this.props.notebookModel.cells[this.props.notebookModel.cells.length - 1], 'bottom')} />
</ul>;
}
renderCellContent(cell: NotebookCellModel, index: number): React.ReactNode {
const renderer = this.props.renderers.get(cell.cellKind);
if (!renderer) {
throw new Error(`No renderer found for cell type ${cell.cellKind}`);
}
return renderer.render(this.props.notebookModel, cell, index);
}
renderCellSidebar(cell: NotebookCellModel): React.ReactNode {
const renderer = this.props.renderers.get(cell.cellKind);
if (!renderer) {
throw new Error(`No renderer found for cell type ${cell.cellKind}`);
}
return renderer.renderSidebar(this.props.notebookModel, cell);
}
protected onDragStart(event: React.DragEvent<HTMLElement>): void {
event.stopPropagation();
if (!this.isEnabled()) {
event.preventDefault();
return;
}
const cellHandle = (event.target as HTMLLIElement).getAttribute('data-cell-handle');
if (!cellHandle) {
throw new Error('Cell handle not found in element for cell drag event');
}
const index = this.props.notebookModel.getCellIndexByHandle(parseInt(cellHandle));
const cell = this.props.notebookModel.cells[index];
NotebookCellListView.dragGhost = document.createElement('div');
NotebookCellListView.dragGhost.classList.add('theia-notebook-drag-ghost-image');
NotebookCellListView.dragGhost.appendChild(this.props.renderers.get(cell.cellKind)?.renderDragImage(cell) ?? document.createElement('div'));
document.body.appendChild(NotebookCellListView.dragGhost);
event.dataTransfer.setDragImage(NotebookCellListView.dragGhost, -10, 0);
event.dataTransfer.setData('text/theia-notebook-cell-index', index.toString());
event.dataTransfer.setData('text/plain', this.props.notebookModel.cells[index].source);
}
protected onDragOver(event: React.DragEvent<HTMLLIElement>, cell: NotebookCellModel, position?: 'top' | 'bottom'): void {
if (!this.isEnabled()) {
return;
}
event.preventDefault();
event.stopPropagation();
// show indicator
this.setState({ ...this.state, dragOverIndicator: { cell, position: position ?? event.nativeEvent.offsetY < event.currentTarget.clientHeight / 2 ? 'top' : 'bottom' } });
}
protected isEnabled(): boolean {
return !Boolean(this.props.notebookModel.readOnly);
}
protected onDrop(event: React.DragEvent<HTMLLIElement>, dropElementIndex: number): void {
if (!this.isEnabled()) {
this.setState({ dragOverIndicator: undefined });
return;
}
const index = parseInt(event.dataTransfer.getData('text/theia-notebook-cell-index'));
const isTargetBelow = index < dropElementIndex;
let newIdx = this.state.dragOverIndicator?.position === 'top' ? dropElementIndex : dropElementIndex + 1;
newIdx = isTargetBelow ? newIdx - 1 : newIdx;
if (index !== undefined && index !== dropElementIndex) {
this.props.notebookModel.applyEdits([{
editType: CellEditType.Move,
length: 1,
index,
newIdx
}], true);
}
this.setState({ ...this.state, dragOverIndicator: undefined });
}
protected onAddNewCell(handler: (...args: unknown[]) => void, index: number): void {
if (this.isEnabled()) {
this.props.commandRegistry.executeCommand(NotebookCommands.CHANGE_SELECTED_CELL.id, index - 1);
handler(
this.props.notebookModel,
index
);
}
}
protected shouldRenderDragOverIndicator(cell: NotebookCellModel, position: 'top' | 'bottom'): boolean {
return this.isEnabled() &&
this.state.dragOverIndicator !== undefined &&
this.state.dragOverIndicator.cell === cell &&
this.state.dragOverIndicator.position === position;
}
}
export interface NotebookCellDividerProps {
isVisible: () => boolean;
onAddNewCell: (createCommand: (...args: unknown[]) => void) => void;
onDrop: (event: React.DragEvent<HTMLLIElement>) => void;
onDragOver: (event: React.DragEvent<HTMLLIElement>) => void;
menuRegistry: MenuModelRegistry;
}
export function NotebookCellDivider({ isVisible, onAddNewCell, onDrop, onDragOver, menuRegistry }: NotebookCellDividerProps): React.JSX.Element {
const [hover, setHover] = React.useState(false);
const menuPath = NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_CELL_ADD_GROUP; // we contribute into this menu, so it will exist
const menuItems: CommandMenu[] = menuRegistry.getMenu(menuPath)!.children.filter(item => CommandMenu.is(item)).map(item => item as CommandMenu);
const renderItem = (item: CommandMenu): React.ReactNode => {
const execute = (...args: unknown[]) => {
if (CommandMenu.is(item)) {
item.run([...menuPath, item.id], ...args);
}
};
return <button
key={item.id}
className='theia-notebook-add-cell-button'
onClick={() => onAddNewCell(execute)}
title={nls.localizeByDefault(`Add ${item.label} Cell`)}
>
<div className={item.icon + ' theia-notebook-add-cell-button-icon'} />
<div className='theia-notebook-add-cell-button-text'>{item.label}</div>
</button>;
};
return <li className='theia-notebook-cell-divider' onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} onDrop={onDrop} onDragOver={onDragOver}>
{hover && isVisible() && <div className='theia-notebook-add-cell-buttons'>
{menuItems.map((item: CommandMenu) => renderItem(item))}
</div>}
</li>;
}
function CellDropIndicator(props: { visible: boolean }): React.JSX.Element {
return <div className='theia-notebook-cell-drop-indicator' style={{ visibility: props.visible ? 'visible' : 'hidden' }} />;
}

View File

@@ -0,0 +1,118 @@
// *****************************************************************************
// 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 * as React from '@theia/core/shared/react';
import { CommandMenu, CommandRegistry, CompoundMenuNode, DisposableCollection, Emitter, Event, MenuModelRegistry, MenuPath, RenderedMenuNode } from '@theia/core';
import { inject, injectable } from '@theia/core/shared/inversify';
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
import { NotebookCellSidebar, NotebookCellToolbar } from './notebook-cell-toolbar';
import { ContextMenuRenderer } from '@theia/core/lib/browser';
import { NotebookCellModel } from '../view-model/notebook-cell-model';
import { NotebookContextManager } from '../service/notebook-context-manager';
export interface NotebookCellToolbarItem {
id: string;
icon?: string;
label?: string;
onClick: (e: React.MouseEvent) => void;
isVisible: () => boolean;
}
export interface toolbarItemOptions {
contextMenuArgs?: () => unknown[];
commandArgs?: () => unknown[];
}
@injectable()
export class NotebookCellToolbarFactory {
@inject(MenuModelRegistry)
protected menuRegistry: MenuModelRegistry;
@inject(ContextKeyService)
protected contextKeyService: ContextKeyService;
@inject(ContextMenuRenderer)
protected readonly contextMenuRenderer: ContextMenuRenderer;
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
@inject(NotebookContextManager)
protected readonly notebookContextManager: NotebookContextManager;
protected readonly onDidChangeContextEmitter = new Emitter<void>;
readonly onDidChangeContext: Event<void> = this.onDidChangeContextEmitter.event;
protected toDisposeOnRender = new DisposableCollection();
renderCellToolbar(menuPath: string[], cell: NotebookCellModel, itemOptions: toolbarItemOptions): React.ReactNode {
return <NotebookCellToolbar getMenuItems={() => this.getMenuItems(menuPath, cell, itemOptions)}
onContextChanged={this.onDidChangeContext} />;
}
renderSidebar(menuPath: string[], cell: NotebookCellModel, itemOptions: toolbarItemOptions): React.ReactNode {
return <NotebookCellSidebar getMenuItems={() => this.getMenuItems(menuPath, cell, itemOptions)}
onContextChanged={this.onDidChangeContext} />;
}
private getMenuItems(menuItemPath: string[], cell: NotebookCellModel, itemOptions: toolbarItemOptions): NotebookCellToolbarItem[] {
this.toDisposeOnRender.dispose();
this.toDisposeOnRender = new DisposableCollection();
const inlineItems: NotebookCellToolbarItem[] = [];
const menu = this.menuRegistry.getMenu(menuItemPath);
this.toDisposeOnRender.push(this.notebookContextManager.scopedStore?.onDidChangeContext(() => {
this.onDidChangeContextEmitter.fire();
}));
if (menu) {
for (const menuNode of menu.children) {
const itemPath = [...menuItemPath, menuNode.id];
if (menuNode.isVisible(itemPath, this.notebookContextManager.getCellContext(cell.handle), this.notebookContextManager.context, itemOptions.commandArgs?.() ?? [])) {
if (RenderedMenuNode.is(menuNode)) {
inlineItems.push(this.createToolbarItem(itemPath, menuNode, itemOptions));
}
}
}
}
return inlineItems;
}
private createToolbarItem(menuPath: MenuPath, menuNode: RenderedMenuNode, itemOptions: toolbarItemOptions): NotebookCellToolbarItem {
return {
id: menuNode.id,
icon: menuNode.icon,
label: menuNode.label,
onClick: e => {
if (CompoundMenuNode.is(menuNode)) {
this.contextMenuRenderer.render(
{
anchor: e.nativeEvent,
menuPath: menuPath,
menu: menuNode,
includeAnchorArg: false,
args: itemOptions.contextMenuArgs?.(),
context: this.notebookContextManager.context || (e.currentTarget as HTMLElement)
});
} else if (CommandMenu.is(menuNode)) {
menuNode.run(menuPath, ...(itemOptions.commandArgs?.() ?? []));
};
},
isVisible: () => true
};
}
}

View File

@@ -0,0 +1,71 @@
// *****************************************************************************
// 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 * as React from '@theia/core/shared/react';
import { ACTION_ITEM } from '@theia/core/lib/browser';
import { NotebookCellToolbarItem } from './notebook-cell-toolbar-factory';
import { DisposableCollection, Event } from '@theia/core';
export interface NotebookCellToolbarProps {
getMenuItems: () => NotebookCellToolbarItem[];
onContextChanged: Event<void>;
}
interface NotebookCellToolbarState {
inlineItems: NotebookCellToolbarItem[];
}
abstract class NotebookCellActionBar extends React.Component<NotebookCellToolbarProps, NotebookCellToolbarState> {
protected toDispose = new DisposableCollection();
constructor(props: NotebookCellToolbarProps) {
super(props);
this.toDispose.push(props.onContextChanged(e => {
const menuItems = this.props.getMenuItems();
this.setState({ inlineItems: menuItems });
}));
this.state = { inlineItems: this.props.getMenuItems() };
}
override componentWillUnmount(): void {
this.toDispose.dispose();
}
protected renderItem(item: NotebookCellToolbarItem): React.ReactNode {
return <div key={item.id} id={item.id} title={item.label} onClick={item.onClick} className={`${item.icon} ${ACTION_ITEM} theia-notebook-cell-toolbar-item`} />;
}
}
export class NotebookCellToolbar extends NotebookCellActionBar {
override render(): React.ReactNode {
return <div className='theia-notebook-cell-toolbar'>
{this.state.inlineItems.filter(e => e.isVisible()).map(item => this.renderItem(item))}
</div>;
}
}
export class NotebookCellSidebar extends NotebookCellActionBar {
override render(): React.ReactNode {
return <div className='theia-notebook-cell-sidebar-toolbar'>
{this.state.inlineItems.filter(e => e.isVisible()).map(item => this.renderItem(item))}
</div>;
}
}

View File

@@ -0,0 +1,411 @@
// *****************************************************************************
// 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 * as React from '@theia/core/shared/react';
import { MonacoEditorServices } from '@theia/monaco/lib/browser/monaco-editor';
import { NotebookRendererRegistry } from '../notebook-renderer-registry';
import { NotebookCellModel } from '../view-model/notebook-cell-model';
import { NotebookModel } from '../view-model/notebook-model';
import { CellEditor } from './notebook-cell-editor';
import { CellRenderer, observeCellHeight } from './notebook-cell-list-view';
import { NotebookCellToolbarFactory } from './notebook-cell-toolbar-factory';
import { NotebookCellActionContribution, NotebookCellCommands } from '../contributions/notebook-cell-actions-contribution';
import { CellExecution, NotebookExecutionStateService } from '../service/notebook-execution-state-service';
import { codicon } from '@theia/core/lib/browser';
import { NotebookCellExecutionState } from '../../common';
import { CancellationToken, CommandRegistry, DisposableCollection, nls } from '@theia/core';
import { NotebookContextManager } from '../service/notebook-context-manager';
import { NotebookViewportService } from './notebook-viewport-service';
import { EditorPreferences } from '@theia/editor/lib/common/editor-preferences';
import { NotebookOptionsService } from '../service/notebook-options';
import { MarkdownRenderer } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer';
import { MarkdownString } from '@theia/monaco-editor-core/esm/vs/base/common/htmlContent';
import { NotebookCellEditorService } from '../service/notebook-cell-editor-service';
import { CellOutputWebview } from '../renderers/cell-output-webview';
import { NotebookCellStatusBarItem, NotebookCellStatusBarItemList, NotebookCellStatusBarService } from '../service/notebook-cell-status-bar-service';
import { LabelParser } from '@theia/core/lib/browser/label-parser';
import { NotebookViewModel } from '../view-model/notebook-view-model';
@injectable()
export class NotebookCodeCellRenderer implements CellRenderer {
@inject(MonacoEditorServices)
protected readonly monacoServices: MonacoEditorServices;
@inject(NotebookRendererRegistry)
protected readonly notebookRendererRegistry: NotebookRendererRegistry;
@inject(NotebookCellToolbarFactory)
protected readonly notebookCellToolbarFactory: NotebookCellToolbarFactory;
@inject(NotebookExecutionStateService)
protected readonly executionStateService: NotebookExecutionStateService;
@inject(NotebookContextManager)
protected readonly notebookContextManager: NotebookContextManager;
@inject(NotebookViewportService)
protected readonly notebookViewportService: NotebookViewportService;
@inject(EditorPreferences)
protected readonly editorPreferences: EditorPreferences;
@inject(NotebookCellEditorService)
protected readonly notebookCellEditorService: NotebookCellEditorService;
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
@inject(NotebookOptionsService)
protected readonly notebookOptionsService: NotebookOptionsService;
@inject(MarkdownRenderer)
protected readonly markdownRenderer: MarkdownRenderer;
@inject(CellOutputWebview)
protected readonly outputWebview: CellOutputWebview;
@inject(NotebookCellStatusBarService)
protected readonly notebookCellStatusBarService: NotebookCellStatusBarService;
@inject(LabelParser)
protected readonly labelParser: LabelParser;
@inject(NotebookViewModel)
protected readonly notebookViewModel: NotebookViewModel;
render(notebookModel: NotebookModel, cell: NotebookCellModel, handle: number): React.ReactNode {
return <div className='theia-notebook-cell-with-sidebar' ref={ref => observeCellHeight(ref, cell)}>
<div className='theia-notebook-cell-editor-container'>
<CellEditor notebookModel={notebookModel} cell={cell}
notebookViewModel={this.notebookViewModel}
monacoServices={this.monacoServices}
notebookContextManager={this.notebookContextManager}
notebookViewportService={this.notebookViewportService}
notebookCellEditorService={this.notebookCellEditorService}
fontInfo={this.notebookOptionsService.editorFontInfo} />
<NotebookCodeCellStatus cell={cell} notebook={notebookModel}
commandRegistry={this.commandRegistry}
executionStateService={this.executionStateService}
cellStatusBarService={this.notebookCellStatusBarService}
labelParser={this.labelParser}
onClick={() => this.notebookViewModel.cellViewModels.get(cell.handle)?.requestFocusEditor()} />
</div >
</div >;
}
renderSidebar(notebookModel: NotebookModel, cell: NotebookCellModel): React.ReactNode {
return <div>
<NotebookCodeCellSidebar cell={cell} notebook={notebookModel} notebookCellToolbarFactory={this.notebookCellToolbarFactory} />
<NotebookCodeCellOutputs cell={cell} notebook={notebookModel} outputWebview={this.outputWebview}
renderSidebar={() =>
this.notebookCellToolbarFactory.renderSidebar(NotebookCellActionContribution.OUTPUT_SIDEBAR_MENU, cell, {
contextMenuArgs: () => [notebookModel, cell, cell.outputs[0]]
})
} />
</div>;
}
renderDragImage(cell: NotebookCellModel): HTMLElement {
const dragImage = document.createElement('div');
dragImage.className = 'theia-notebook-drag-image';
dragImage.style.width = this.notebookContextManager.context?.clientWidth + 'px';
dragImage.style.height = '100px';
dragImage.style.display = 'flex';
const fakeRunButton = document.createElement('span');
fakeRunButton.className = `${codicon('play')} theia-notebook-cell-status-item`;
dragImage.appendChild(fakeRunButton);
const fakeEditor = document.createElement('div');
dragImage.appendChild(fakeEditor);
const lines = cell.source.split('\n').slice(0, 5).join('\n');
const codeSequence = this.getMarkdownCodeSequence(lines);
const firstLine = new MarkdownString(`${codeSequence}${cell.language}\n${lines}\n${codeSequence}`, { supportHtml: true, isTrusted: false });
fakeEditor.appendChild(this.markdownRenderer.render(firstLine).element);
fakeEditor.classList.add('theia-notebook-cell-editor-container');
fakeEditor.style.padding = '10px';
return dragImage;
}
protected getMarkdownCodeSequence(input: string): string {
// We need a minimum of 3 backticks to start a code block.
let longest = 2;
let current = 0;
for (let i = 0; i < input.length; i++) {
const char = input.charAt(i);
if (char === '`') {
current++;
if (current > longest) {
longest = current;
}
} else {
current = 0;
}
}
return Array(longest + 1).fill('`').join('');
}
}
export interface NotebookCodeCellSidebarProps {
cell: NotebookCellModel;
notebook: NotebookModel;
notebookCellToolbarFactory: NotebookCellToolbarFactory
}
export class NotebookCodeCellSidebar extends React.Component<NotebookCodeCellSidebarProps> {
protected toDispose = new DisposableCollection();
constructor(props: NotebookCodeCellSidebarProps) {
super(props);
this.toDispose.push(props.cell.onDidCellHeightChange(() => this.forceUpdate()));
}
override componentWillUnmount(): void {
this.toDispose.dispose();
}
override render(): React.ReactNode {
return <div className='theia-notebook-cell-sidebar-actions' style={{ height: `${this.props.cell.cellHeight}px` }}>
{this.props.notebookCellToolbarFactory.renderSidebar(NotebookCellActionContribution.CODE_CELL_SIDEBAR_MENU, this.props.cell, {
contextMenuArgs: () => [this.props.cell], commandArgs: () => [this.props.notebook, this.props.cell]
})
}
<CodeCellExecutionOrder cell={this.props.cell} />
</div>;
}
}
export interface NotebookCodeCellStatusProps {
notebook: NotebookModel;
cell: NotebookCellModel;
commandRegistry: CommandRegistry;
cellStatusBarService: NotebookCellStatusBarService;
executionStateService?: NotebookExecutionStateService;
labelParser: LabelParser;
onClick: () => void;
}
export interface NotebookCodeCellStatusState {
currentExecution?: CellExecution;
executionTime: number;
}
export class NotebookCodeCellStatus extends React.Component<NotebookCodeCellStatusProps, NotebookCodeCellStatusState> {
protected toDispose = new DisposableCollection();
protected statusBarItems: NotebookCellStatusBarItemList[] = [];
constructor(props: NotebookCodeCellStatusProps) {
super(props);
this.state = {
executionTime: 0
};
let currentInterval: NodeJS.Timeout | undefined;
if (props.executionStateService) {
this.toDispose.push(props.executionStateService.onDidChangeExecution(event => {
if (event.affectsCell(this.props.cell.uri)) {
this.setState({ currentExecution: event.changed, executionTime: 0 });
clearInterval(currentInterval);
if (event.changed?.state === NotebookCellExecutionState.Executing) {
const startTime = Date.now();
// The resolution of the time display is only a single digit after the decimal point.
// Therefore, we only need to update the display every 100ms.
currentInterval = setInterval(() => {
this.setState({
executionTime: Date.now() - startTime
});
}, 100);
}
}
}));
}
this.toDispose.push(props.cell.onDidChangeLanguage(() => {
this.forceUpdate();
}));
this.updateStatusBarItems();
this.props.cellStatusBarService.onDidChangeItems(() => this.updateStatusBarItems());
this.props.notebook.onContentChanged(() => this.updateStatusBarItems());
}
async updateStatusBarItems(): Promise<void> {
this.statusBarItems = await this.props.cellStatusBarService.getStatusBarItemsForCell(
this.props.notebook.uri,
this.props.notebook.cells.indexOf(this.props.cell),
this.props.notebook.viewType,
CancellationToken.None);
this.forceUpdate();
}
override componentWillUnmount(): void {
this.toDispose.dispose();
}
override render(): React.ReactNode {
return <div className='notebook-cell-status' onClick={() => this.props.onClick()}>
<div className='notebook-cell-status-left'>
{this.props.executionStateService && this.renderExecutionState()}
{this.statusBarItems?.length && this.renderStatusBarItems()}
</div>
<div className='notebook-cell-status-right'>
<span className='notebook-cell-language-label' onClick={() => {
this.props.commandRegistry.executeCommand(NotebookCellCommands.CHANGE_CELL_LANGUAGE.id, this.props.notebook, this.props.cell);
}}>{this.props.cell.languageName}</span>
</div>
</div>;
}
protected renderExecutionState(): React.ReactNode {
const state = this.state.currentExecution?.state;
const { lastRunSuccess } = this.props.cell.internalMetadata;
let iconClasses: string | undefined = undefined;
let color: string | undefined = undefined;
if (!state && lastRunSuccess) {
iconClasses = codicon('check');
color = 'green';
} else if (!state && lastRunSuccess === false) {
iconClasses = codicon('error');
color = 'red';
} else if (state === NotebookCellExecutionState.Pending || state === NotebookCellExecutionState.Unconfirmed) {
iconClasses = codicon('clock');
} else if (state === NotebookCellExecutionState.Executing) {
iconClasses = `${codicon('sync')} theia-animation-spin`;
}
return <>
{iconClasses &&
<>
<span className={`${iconClasses} notebook-cell-status-item`} style={{ color }}></span>
<div className='notebook-cell-status-item'>{this.renderTime(this.getExecutionTime())}</div>
</>}
</>;
}
protected getExecutionTime(): number {
const { runStartTime, runEndTime } = this.props.cell.internalMetadata;
const { executionTime } = this.state;
if (runStartTime !== undefined && runEndTime !== undefined) {
return runEndTime - runStartTime;
}
return executionTime;
}
protected renderTime(ms: number): string {
return `${(ms / 1000).toLocaleString(undefined, { maximumFractionDigits: 1, minimumFractionDigits: 1 })}s`;
}
protected renderStatusBarItems(): React.ReactNode {
return <>
{
this.statusBarItems.flatMap((itemList, listIndex) =>
itemList.items.map((item, index) => this.renderStatusBarItem(item, `${listIndex}-${index}`)
)
)
}
</>;
}
protected renderStatusBarItem(item: NotebookCellStatusBarItem, key: string): React.ReactNode {
const content = this.props.labelParser.parse(item.text).map(part => {
if (typeof part === 'string') {
return part;
} else {
return <span key={part.name} className={`codicon codicon-${part.name}`}></span>;
}
});
return <div key={key} className={`cell-status-bar-item ${item.command ? 'cell-status-item-has-command' : ''}`} onClick={async () => {
if (item.command) {
if (typeof item.command === 'string') {
this.props.commandRegistry.executeCommand(item.command);
} else {
this.props.commandRegistry.executeCommand(item.command.id, ...(item.command.arguments ?? []));
}
}
}}>
{content}
</div>;
}
}
interface NotebookCellOutputProps {
cell: NotebookCellModel;
notebook: NotebookModel;
outputWebview: CellOutputWebview;
renderSidebar: () => React.ReactNode;
}
export class NotebookCodeCellOutputs extends React.Component<NotebookCellOutputProps> {
protected toDispose = new DisposableCollection();
protected outputHeight: number = 0;
override async componentDidMount(): Promise<void> {
const { cell } = this.props;
this.toDispose.push(cell.onDidChangeOutputs(() => this.forceUpdate()));
this.toDispose.push(this.props.cell.onDidChangeOutputVisibility(() => this.forceUpdate()));
this.toDispose.push(this.props.outputWebview.onDidRenderOutput(event => {
if (event.cellHandle === this.props.cell.handle) {
this.outputHeight = event.outputHeight;
this.forceUpdate();
}
}));
}
override componentWillUnmount(): void {
this.toDispose.dispose();
}
override render(): React.ReactNode {
if (!this.props.cell.outputs?.length) {
return <></>;
}
if (this.props.cell.outputVisible) {
return <div style={{ minHeight: this.outputHeight }}>
{this.props.renderSidebar()}
</div>;
}
return <div className='theia-notebook-collapsed-output-container'><i className='theia-notebook-collapsed-output'>{nls.localizeByDefault('Outputs are collapsed')}</i></div>;
}
}
interface NotebookCellExecutionOrderProps {
cell: NotebookCellModel;
}
function CodeCellExecutionOrder({ cell }: NotebookCellExecutionOrderProps): React.JSX.Element {
const [executionOrder, setExecutionOrder] = React.useState(cell.internalMetadata.executionOrder ?? ' ');
React.useEffect(() => {
const listener = cell.onDidChangeInternalMetadata(e => {
setExecutionOrder(cell.internalMetadata.executionOrder ?? ' ');
});
return () => listener.dispose();
}, []);
return <span className='theia-notebook-code-cell-execution-order'>{`[${executionOrder}]`}</span>;
}

View File

@@ -0,0 +1,335 @@
// *****************************************************************************
// 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 { nls } from '@theia/core';
import * as React from '@theia/core/shared/react';
import { codicon } from '@theia/core/lib/browser';
import debounce = require('@theia/core/shared/lodash.debounce');
export interface NotebookEditorFindMatch {
selected: boolean;
show(): void;
replace?(value: string): void;
}
export interface NotebookEditorFindMatchOptions {
search: string;
matchCase: boolean;
wholeWord: boolean;
regex: boolean;
activeFilters: string[];
}
export interface NotebookEditorFindFilter {
id: string;
label: string;
active: boolean;
}
export interface NotebookEditorFindOptions {
search?: string;
jumpToMatch?: boolean;
matchCase?: boolean;
wholeWord?: boolean;
regex?: boolean;
modifyIndex?: (matches: NotebookEditorFindMatch[], index: number) => number;
}
export interface NotebookFindWidgetProps {
hidden?: boolean;
filters?: NotebookEditorFindFilter[];
onClose(): void;
onSearch(options: NotebookEditorFindMatchOptions): NotebookEditorFindMatch[];
onReplace(matches: NotebookEditorFindMatch[], value: string): void;
}
export interface NotebookFindWidgetState {
search: string;
replace: string;
expanded: boolean;
matchCase: boolean;
wholeWord: boolean;
regex: boolean;
activeFilters: string[];
currentMatch: number;
matches: NotebookEditorFindMatch[];
}
export class NotebookFindWidget extends React.Component<NotebookFindWidgetProps, NotebookFindWidgetState> {
private searchRef = React.createRef<HTMLInputElement>();
private debounceSearch = debounce(this.search.bind(this), 50);
constructor(props: NotebookFindWidgetProps) {
super(props);
this.state = {
search: '',
replace: '',
currentMatch: 0,
matches: [],
expanded: false,
matchCase: false,
regex: false,
wholeWord: false,
activeFilters: props.filters?.filter(filter => filter.active).map(filter => filter.id) || []
};
}
override render(): React.ReactNode {
const hasMatches = this.hasMatches();
const canReplace = this.canReplace();
const canReplaceAll = this.canReplaceAll();
return (
<div onKeyUp={event => {
if (event.key === 'Escape') {
this.props.onClose();
}
}} className={`theia-notebook-find-widget ${!this.state.expanded ? 'search-mode' : ''} ${this.props.hidden ? 'hidden' : ''}`}>
<div className='theia-notebook-find-widget-expand' title={nls.localizeByDefault('Toggle Replace')} onClick={() => {
this.setState({
expanded: !this.state.expanded
});
}}>
<div className={codicon(`chevron-${this.state.expanded ? 'down' : 'right'}`)}></div>
</div>
<div className='theia-notebook-find-widget-inputs'>
<div className='theia-notebook-find-widget-input-wrapper'>
<input
ref={this.searchRef}
type='text'
className='theia-input theia-notebook-find-widget-input'
placeholder={nls.localizeByDefault('Find')}
value={this.state.search}
onChange={event => {
this.setState({
search: event.target.value
});
this.debounceSearch({});
}}
onKeyDown={event => {
if (event.key === 'Enter') {
if (event.shiftKey) {
this.gotoPreviousMatch();
} else {
this.gotoNextMatch();
}
event.preventDefault();
}
}}
/>
<div
className={`${codicon('case-sensitive', true)} option ${this.state.matchCase ? 'enabled' : ''}`}
title={nls.localizeByDefault('Match Case')}
onClick={() => {
this.search({
matchCase: !this.state.matchCase
});
}}></div>
<div
className={`${codicon('whole-word', true)} option ${this.state.wholeWord ? 'enabled' : ''}`}
title={nls.localizeByDefault('Match Whole Word')}
onClick={() => {
this.search({
wholeWord: !this.state.wholeWord
});
}}></div>
<div
className={`${codicon('regex', true)} option ${this.state.regex ? 'enabled' : ''}`}
title={nls.localizeByDefault('Use Regular Expression')}
onClick={() => {
this.search({
regex: !this.state.regex
});
}}></div>
{/* <div
className={`${codicon('filter', true)} option ${this.state.wholeWord ? 'enabled' : ''}`}
title={nls.localizeByDefault('Find Filters')}></div> */}
</div>
<input
type='text'
className='theia-input theia-notebook-find-widget-replace'
placeholder={nls.localizeByDefault('Replace')}
value={this.state.replace}
onChange={event => {
this.setState({
replace: event.target.value
});
}}
onKeyDown={event => {
if (event.key === 'Enter') {
this.replaceOne();
event.preventDefault();
}
}}
/>
</div>
<div className='theia-notebook-find-widget-buttons'>
<div className='theia-notebook-find-widget-buttons-first'>
<div className='theia-notebook-find-widget-matches-count'>
{this.getMatchesCount()}
</div>
<div
className={`${codicon('arrow-up', hasMatches)} ${hasMatches ? '' : 'disabled'}`}
title={nls.localizeByDefault('Previous Match')}
onClick={() => {
this.gotoPreviousMatch();
}}
></div>
<div
className={`${codicon('arrow-down', hasMatches)} ${hasMatches ? '' : 'disabled'}`}
title={nls.localizeByDefault('Next Match')}
onClick={() => {
this.gotoNextMatch();
}}
></div>
<div
className={codicon('close', true)}
title={nls.localizeByDefault('Close')}
onClick={() => {
this.props.onClose();
}}
></div>
</div>
<div className='theia-notebook-find-widget-buttons-second'>
<div
className={`${codicon('replace', canReplace)} ${canReplace ? '' : 'disabled'}`}
title={nls.localizeByDefault('Replace')}
onClick={() => {
this.replaceOne();
}}
></div>
<div
className={`${codicon('replace-all', canReplaceAll)} ${canReplaceAll ? '' : 'disabled'}`}
title={nls.localizeByDefault('Replace All')}
onClick={() => {
this.replaceAll();
}}
></div>
</div>
</div>
</div>
);
}
private hasMatches(): boolean {
return this.state.matches.length > 0;
}
private canReplace(): boolean {
return Boolean(this.state.matches[this.state.currentMatch]?.replace);
}
private canReplaceAll(): boolean {
return this.state.matches.some(match => Boolean(match.replace));
}
private getMatchesCount(): string {
if (this.hasMatches()) {
return nls.localizeByDefault('{0} of {1}', this.state.currentMatch + 1, this.state.matches.length);
} else {
return nls.localizeByDefault('No results');
}
}
private gotoNextMatch(): void {
this.search({
modifyIndex: (matches, index) => (index + 1) % matches.length,
jumpToMatch: true
});
}
private gotoPreviousMatch(): void {
this.search({
modifyIndex: (matches, index) => (index === 0 ? matches.length : index) - 1,
jumpToMatch: true
});
}
private replaceOne(): void {
const existingMatches = this.state.matches;
const match = existingMatches[this.state.currentMatch];
if (match) {
match.replace?.(this.state.replace);
this.search({
jumpToMatch: true,
modifyIndex: (matches, index) => {
if (matches.length < existingMatches.length) {
return index % matches.length;
} else {
const diff = matches.length - existingMatches.length;
return (index + diff + 1) % matches.length;
}
}
});
}
}
private replaceAll(): void {
this.props.onReplace(this.state.matches, this.state.replace);
this.search({});
}
override componentDidUpdate(prevProps: Readonly<NotebookFindWidgetProps>, prevState: Readonly<NotebookFindWidgetState>): void {
if (!this.props.hidden && prevProps.hidden) {
// Focus the search input when the widget switches from hidden to visible.
this.searchRef.current?.focus();
}
}
focusSearch(content?: string): void {
this.searchRef.current?.focus();
if (content) {
this.search({
search: content,
jumpToMatch: false
});
}
}
search(options: NotebookEditorFindOptions): void {
const matchCase = options.matchCase ?? this.state.matchCase;
const wholeWord = options.wholeWord ?? this.state.wholeWord;
const regex = options.regex ?? this.state.regex;
const search = options.search ?? this.state.search;
const matches = this.props.onSearch({
search,
matchCase,
wholeWord,
regex,
activeFilters: this.state.activeFilters
});
let currentMatch = Math.max(0, Math.min(this.state.currentMatch, matches.length - 1));
if (options.modifyIndex && matches.length > 0) {
currentMatch = options.modifyIndex(matches, currentMatch);
}
const selectedMatch = matches[currentMatch];
if (selectedMatch) {
selectedMatch.selected = true;
if (options.jumpToMatch) {
selectedMatch.show();
}
}
this.setState({
search,
matches,
currentMatch,
matchCase,
wholeWord,
regex
});
}
}

View File

@@ -0,0 +1,209 @@
// *****************************************************************************
// 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 { ArrayUtils, CommandMenu, CommandRegistry, DisposableCollection, Group, GroupImpl, MenuModelRegistry, MenuNode, MenuPath, nls } from '@theia/core';
import * as React from '@theia/core/shared/react';
import { codicon, ContextMenuRenderer } from '@theia/core/lib/browser';
import { NotebookCommands, NotebookMenus } from '../contributions/notebook-actions-contribution';
import { NotebookModel } from '../view-model/notebook-model';
import { NotebookKernelService } from '../service/notebook-kernel-service';
import { inject, injectable } from '@theia/core/shared/inversify';
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
import { NotebookContextManager } from '../service/notebook-context-manager';
export interface NotebookMainToolbarProps {
notebookModel: NotebookModel
menuRegistry: MenuModelRegistry;
notebookKernelService: NotebookKernelService;
commandRegistry: CommandRegistry;
contextKeyService: ContextKeyService;
editorNode: HTMLElement;
notebookContextManager: NotebookContextManager;
contextMenuRenderer: ContextMenuRenderer;
}
@injectable()
export class NotebookMainToolbarRenderer {
@inject(NotebookKernelService) protected readonly notebookKernelService: NotebookKernelService;
@inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry) protected readonly menuRegistry: MenuModelRegistry;
@inject(ContextKeyService) protected readonly contextKeyService: ContextKeyService;
@inject(NotebookContextManager) protected readonly notebookContextManager: NotebookContextManager;
@inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer;
render(notebookModel: NotebookModel, editorNode: HTMLElement): React.ReactNode {
return <NotebookMainToolbar notebookModel={notebookModel}
menuRegistry={this.menuRegistry}
notebookKernelService={this.notebookKernelService}
commandRegistry={this.commandRegistry}
contextKeyService={this.contextKeyService}
editorNode={editorNode}
notebookContextManager={this.notebookContextManager}
contextMenuRenderer={this.contextMenuRenderer} />;
}
}
interface NotebookMainToolbarState {
selectedKernelLabel?: string;
numberOfHiddenItems: number;
}
export class NotebookMainToolbar extends React.Component<NotebookMainToolbarProps, NotebookMainToolbarState> {
// The minimum area between items and kernel select before hiding items in a context menu
static readonly MIN_FREE_AREA = 10;
protected toDispose = new DisposableCollection();
protected nativeSubmenus = [
NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_CELL_ADD_GROUP[NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_CELL_ADD_GROUP.length - 1],
NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_EXECUTION_GROUP[NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_EXECUTION_GROUP.length - 1]];
protected gapElement: HTMLDivElement | undefined;
protected lastGapElementWidth: number = 0;
protected resizeObserver: ResizeObserver = new ResizeObserver(() => this.calculateItemsToHide());
constructor(props: NotebookMainToolbarProps) {
super(props);
this.state = {
selectedKernelLabel: props.notebookKernelService.getSelectedOrSuggestedKernel(props.notebookModel)?.label,
numberOfHiddenItems: 0,
};
this.toDispose.push(props.notebookKernelService.onDidChangeSelectedKernel(event => {
if (props.notebookModel.uri.isEqual(event.notebook)) {
this.setState({ selectedKernelLabel: props.notebookKernelService.getKernel(event.newKernel ?? '')?.label });
}
}));
// in case the selected kernel is added after the notebook is loaded
this.toDispose.push(props.notebookKernelService.onDidAddKernel(() => {
if (!this.state.selectedKernelLabel) {
this.setState({ selectedKernelLabel: props.notebookKernelService.getSelectedOrSuggestedKernel(props.notebookModel)?.label });
}
}));
// TODO maybe we need a mechanism to check for changes in the menu to update this toolbar
const menuItems = this.getMenuItems();
for (const item of menuItems) {
if (item.onDidChange) {
item.onDidChange(() => this.forceUpdate());
}
}
}
override componentWillUnmount(): void {
this.toDispose.dispose();
}
override componentDidUpdate(): void {
this.calculateItemsToHide();
}
override componentDidMount(): void {
this.calculateItemsToHide();
}
protected calculateItemsToHide(): void {
const numberOfMenuItems = this.getMenuItems().length;
if (this.gapElement && this.gapElement.getBoundingClientRect().width < NotebookMainToolbar.MIN_FREE_AREA && this.state.numberOfHiddenItems < numberOfMenuItems) {
this.setState({ ...this.state, numberOfHiddenItems: this.state.numberOfHiddenItems + 1 });
this.lastGapElementWidth = this.gapElement.getBoundingClientRect().width;
} else if (this.gapElement && this.gapElement.getBoundingClientRect().width > this.lastGapElementWidth && this.state.numberOfHiddenItems > 0) {
this.setState({ ...this.state, numberOfHiddenItems: 0 });
this.lastGapElementWidth = this.gapElement.getBoundingClientRect().width;
}
}
protected renderContextMenu(event: MouseEvent, menuItems: readonly MenuNode[]): void {
const hiddenItems = menuItems.slice(menuItems.length - this.calculateNumberOfHiddenItems(menuItems));
const menu = new GroupImpl(NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_HIDDEN_ITEMS_CONTEXT_MENU[0]);
hiddenItems.forEach(item => menu.addNode(item));
this.props.contextMenuRenderer.render({
anchor: event,
menuPath: NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_HIDDEN_ITEMS_CONTEXT_MENU,
menu: menu,
contextKeyService: this.props.contextKeyService,
context: this.props.editorNode,
args: [this.props.notebookModel.uri]
});
}
override render(): React.ReactNode {
const menuItems = this.getMenuItems();
return <div className='theia-notebook-main-toolbar' id='notebook-main-toolbar'>
{menuItems.slice(0, menuItems.length - this.calculateNumberOfHiddenItems(menuItems)).map(item => this.renderMenuItem(NotebookMenus.NOTEBOOK_MAIN_TOOLBAR, item))}
{
this.state.numberOfHiddenItems > 0 &&
<span className={`${codicon('ellipsis')} action-label theia-notebook-main-toolbar-item`} onClick={e => this.renderContextMenu(e.nativeEvent, menuItems)} />
}
<div ref={element => this.gapElementChanged(element)} style={{ flexGrow: 1 }}></div>
<div className='theia-notebook-main-toolbar-item action-label' id={NotebookCommands.SELECT_KERNEL_COMMAND.id}
onClick={() => this.props.commandRegistry.executeCommand(NotebookCommands.SELECT_KERNEL_COMMAND.id, this.props.notebookModel)}>
<span className={codicon('server-environment')} />
<span className=' theia-notebook-main-toolbar-item-text' id='kernel-text'>
{this.state.selectedKernelLabel ?? nls.localizeByDefault('Select Kernel')}
</span>
</div>
</div >;
}
protected gapElementChanged(element: HTMLDivElement | null): void {
if (this.gapElement) {
this.resizeObserver.unobserve(this.gapElement);
}
this.gapElement = element ?? undefined;
if (this.gapElement) {
this.lastGapElementWidth = this.gapElement.getBoundingClientRect().width;
this.resizeObserver.observe(this.gapElement);
}
}
protected renderMenuItem<T>(itemPath: MenuPath, item: MenuNode, submenu?: string): React.ReactNode {
if (Group.is(item)) {
const itemNodes = ArrayUtils.coalesce(item.children?.map(child => this.renderMenuItem([...itemPath, child.id], child, item.id)) ?? []);
return <React.Fragment key={item.id}>
{itemNodes}
{itemNodes && itemNodes.length > 0 && <span key={`${item.id}-separator`} className='theia-notebook-toolbar-separator'></span>}
</React.Fragment>;
} else if (CommandMenu.is(item) && ((this.nativeSubmenus.includes(submenu ?? '')) || item.isVisible(itemPath, this.props.contextKeyService, this.props.editorNode))) {
return <div key={item.id} id={item.id} title={item.label} className={`theia-notebook-main-toolbar-item action-label${this.getAdditionalClasses(itemPath, item)}`}
onClick={() => {
item.run(itemPath, this.props.notebookModel.uri);
}}>
<span className={item.icon} />
<span className='theia-notebook-main-toolbar-item-text'>{item.label}</span>
</div>;
}
return undefined;
}
protected getMenuItems(): readonly MenuNode[] {
return this.props.menuRegistry.getMenu(NotebookMenus.NOTEBOOK_MAIN_TOOLBAR)!.children; // we contribute to this menu, so it exists
}
protected getAdditionalClasses(itemPath: MenuPath, item: CommandMenu): string {
return item.isEnabled(itemPath, this.props.editorNode) ? '' : ' theia-mod-disabled';
}
protected calculateNumberOfHiddenItems(allMenuItems: readonly MenuNode[]): number {
return this.state.numberOfHiddenItems >= allMenuItems.length ?
allMenuItems.length :
this.state.numberOfHiddenItems % allMenuItems.length;
}
}

View File

@@ -0,0 +1,259 @@
// *****************************************************************************
// 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 * as React from '@theia/core/shared/react';
import { MarkdownRenderer } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer';
import { MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering/markdown-string';
import { NotebookModel } from '../view-model/notebook-model';
import { CellRenderer, observeCellHeight } from './notebook-cell-list-view';
import { NotebookCellModel } from '../view-model/notebook-cell-model';
import { CellEditor } from './notebook-cell-editor';
import { inject, injectable } from '@theia/core/shared/inversify';
import { MonacoEditorServices } from '@theia/monaco/lib/browser/monaco-editor';
import { CommandRegistry, nls } from '@theia/core';
import { NotebookContextManager } from '../service/notebook-context-manager';
import { NotebookOptionsService } from '../service/notebook-options';
import { NotebookCodeCellStatus } from './notebook-code-cell-view';
import { NotebookEditorFindMatch, NotebookEditorFindMatchOptions } from './notebook-find-widget';
import * as mark from 'advanced-mark.js';
import { NotebookCellEditorService } from '../service/notebook-cell-editor-service';
import { NotebookCellStatusBarService } from '../service/notebook-cell-status-bar-service';
import { LabelParser } from '@theia/core/lib/browser/label-parser';
import { NotebookViewModel } from '../view-model/notebook-view-model';
@injectable()
export class NotebookMarkdownCellRenderer implements CellRenderer {
@inject(MarkdownRenderer)
private readonly markdownRenderer: MarkdownRenderer;
@inject(MonacoEditorServices)
protected readonly monacoServices: MonacoEditorServices;
@inject(NotebookContextManager)
protected readonly notebookContextManager: NotebookContextManager;
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
@inject(NotebookOptionsService)
protected readonly notebookOptionsService: NotebookOptionsService;
@inject(NotebookCellEditorService)
protected readonly notebookCellEditorService: NotebookCellEditorService;
@inject(NotebookCellStatusBarService)
protected readonly notebookCellStatusBarService: NotebookCellStatusBarService;
@inject(LabelParser)
protected readonly labelParser: LabelParser;
@inject(NotebookViewModel)
protected readonly notebookViewModel: NotebookViewModel;
render(notebookModel: NotebookModel, cell: NotebookCellModel): React.ReactNode {
return <MarkdownCell
markdownRenderer={this.markdownRenderer}
commandRegistry={this.commandRegistry}
monacoServices={this.monacoServices}
notebookOptionsService={this.notebookOptionsService}
cell={cell}
notebookModel={notebookModel}
notebookViewModel={this.notebookViewModel}
notebookContextManager={this.notebookContextManager}
notebookCellEditorService={this.notebookCellEditorService}
notebookCellStatusBarService={this.notebookCellStatusBarService}
labelParser={this.labelParser}
/>;
}
renderSidebar(notebookModel: NotebookModel, cell: NotebookCellModel): React.ReactNode {
return <div className='theia-notebook-markdown-sidebar'></div>;
}
renderDragImage(cell: NotebookCellModel): HTMLElement {
const dragImage = document.createElement('div');
dragImage.style.width = this.notebookContextManager.context?.clientWidth + 'px';
const markdownString = new MarkdownStringImpl(cell.source, { supportHtml: true, isTrusted: true });
const markdownElement = this.markdownRenderer.render(markdownString).element;
dragImage.appendChild(markdownElement);
return dragImage;
}
}
interface MarkdownCellProps {
markdownRenderer: MarkdownRenderer;
monacoServices: MonacoEditorServices;
commandRegistry: CommandRegistry;
cell: NotebookCellModel;
notebookModel: NotebookModel;
notebookViewModel: NotebookViewModel;
notebookContextManager: NotebookContextManager;
notebookOptionsService: NotebookOptionsService;
notebookCellEditorService: NotebookCellEditorService;
notebookCellStatusBarService: NotebookCellStatusBarService;
labelParser: LabelParser;
}
function MarkdownCell({
markdownRenderer, monacoServices, cell, notebookModel, notebookViewModel, notebookContextManager,
notebookOptionsService, commandRegistry, notebookCellEditorService, notebookCellStatusBarService,
labelParser
}: MarkdownCellProps): React.JSX.Element {
const [editMode, setEditMode] = React.useState(notebookViewModel.cellViewModels.get(cell.handle)?.editing || false);
const [, forceUpdate] = React.useReducer(x => x + 1, 0);
let empty = false;
React.useEffect(() => {
if (!editMode) {
const listener = cell.onDidChangeContent(type => {
if (type === 'content') {
forceUpdate();
}
});
return () => listener.dispose();
}
}, [editMode, cell]);
React.useEffect(() => {
const cellViewModel = notebookViewModel.cellViewModels.get(cell.handle);
const listener = cellViewModel?.onDidRequestCellEditChange(cellEdit => setEditMode(cellEdit));
return () => listener?.dispose();
}, [editMode, notebookViewModel, cell]);
React.useEffect(() => {
if (!editMode) {
const instance = new mark(markdownContent);
cell.onMarkdownFind = options => {
instance.unmark();
if (empty) {
return [];
}
return searchInMarkdown(instance, options);
};
return () => {
cell.onMarkdownFind = undefined;
instance.unmark();
};
}
}, [editMode, cell.source]);
let markdownContent: HTMLElement[] = React.useMemo(() => {
const markdownString = new MarkdownStringImpl(cell.source, { supportHtml: true, isTrusted: true });
const rendered = markdownRenderer.render(markdownString).element;
const children: HTMLElement[] = [];
rendered.childNodes.forEach(child => {
if (child instanceof HTMLElement) {
children.push(child);
}
});
return children;
}, [cell.source]);
if (markdownContent.length === 0) {
const italic = document.createElement('i');
italic.className = 'theia-notebook-empty-markdown';
italic.innerText = nls.localizeByDefault('Empty markdown cell, double-click or press enter to edit.');
italic.style.pointerEvents = 'none';
markdownContent = [italic];
empty = true;
}
return editMode ?
(<div className='theia-notebook-markdown-editor-container' key="code" ref={ref => observeCellHeight(ref, cell)}>
<CellEditor notebookModel={notebookModel} cell={cell}
notebookViewModel={notebookViewModel}
monacoServices={monacoServices}
notebookContextManager={notebookContextManager}
notebookCellEditorService={notebookCellEditorService}
fontInfo={notebookOptionsService.editorFontInfo} />
<NotebookCodeCellStatus cell={cell} notebook={notebookModel}
commandRegistry={commandRegistry}
cellStatusBarService={notebookCellStatusBarService}
labelParser={labelParser}
onClick={() => notebookViewModel.cellViewModels.get(cell.handle)?.requestFocusEditor()} />
</div >) :
(<div className='theia-notebook-markdown-content' key="markdown"
onDoubleClick={() => notebookViewModel.cellViewModels.get(cell.handle)?.requestEdit()}
ref={node => {
node?.replaceChildren(...markdownContent);
observeCellHeight(node, cell);
}}
/>);
}
function searchInMarkdown(instance: mark, options: NotebookEditorFindMatchOptions): NotebookEditorFindMatch[] {
const matches: NotebookEditorFindMatch[] = [];
const markOptions: mark.MarkOptions & mark.RegExpOptions = {
className: 'theia-find-match',
diacritics: false,
caseSensitive: options.matchCase,
acrossElements: true,
separateWordSearch: false,
each: node => {
matches.push(new MarkdownEditorFindMatch(node));
}
};
if (options.regex || options.wholeWord) {
let search = options.search;
if (options.wholeWord) {
if (!options.regex) {
search = escapeRegExp(search);
}
search = '\\b' + search + '\\b';
}
instance.markRegExp(new RegExp(search, options.matchCase ? '' : 'i'), markOptions);
} else {
instance.mark(options.search, markOptions);
}
return matches;
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
class MarkdownEditorFindMatch implements NotebookEditorFindMatch {
constructor(readonly node: Node) { }
private _selected = false;
get selected(): boolean {
return this._selected;
}
set selected(selected: boolean) {
this._selected = selected;
const className = 'theia-find-match-selected';
if (this.node instanceof HTMLElement) {
if (selected) {
this.node.classList.add(className);
} else {
this.node.classList.remove(className);
}
}
}
show(): void {
if (this.node instanceof HTMLElement) {
this.node.scrollIntoView({
behavior: 'instant',
block: 'center'
});
}
}
}

View File

@@ -0,0 +1,61 @@
// *****************************************************************************
// 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 { Disposable } from '@theia/core';
import { injectable } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/shared/vscode-languageserver-protocol';
/**
* this service is for managing the viewport and scroll state of a notebook editor.
* its used both for restoring scroll state after reopening an editor and for cell to check if they are in the viewport.
*/
@injectable()
export class NotebookViewportService implements Disposable {
protected onDidChangeViewportEmitter = new Emitter<void>();
readonly onDidChangeViewport = this.onDidChangeViewportEmitter.event;
protected _viewportElement: HTMLDivElement | undefined;
protected resizeObserver?: ResizeObserver;
set viewportElement(element: HTMLDivElement | undefined) {
this._viewportElement = element;
if (element) {
this.onDidChangeViewportEmitter.fire();
this.resizeObserver?.disconnect();
this.resizeObserver = new ResizeObserver(() => this.onDidChangeViewportEmitter.fire());
this.resizeObserver.observe(element);
}
}
isElementInViewport(element: HTMLElement): boolean {
if (this._viewportElement) {
const rect = element.getBoundingClientRect();
const viewRect = this._viewportElement.getBoundingClientRect();
return rect.top < viewRect.top ? rect.bottom > viewRect.top : rect.top < viewRect.bottom;
}
return false;
}
onScroll(e: HTMLDivElement): void {
this.onDidChangeViewportEmitter.fire();
}
dispose(): void {
this.resizeObserver?.disconnect();
}
}

View File

@@ -0,0 +1,18 @@
// *****************************************************************************
// 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
// *****************************************************************************
export * from './notebook-common';
export * from './notebook-range';

View File

@@ -0,0 +1,342 @@
// *****************************************************************************
// 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 { Command, URI, isObject } from '@theia/core';
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering/markdown-string';
import { BinaryBuffer } from '@theia/core/lib/common/buffer';
import { UriComponents } from '@theia/core/lib/common/uri';
export interface NotebookCommand extends Command {
title?: string;
tooltip?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
arguments?: any[];
}
export enum CellKind {
Markup = 1,
Code = 2
}
export interface NotebookCellMetadata {
/**
* custom metadata
*/
[key: string]: unknown;
}
export interface NotebookCellInternalMetadata {
executionId?: string;
executionOrder?: number;
lastRunSuccess?: boolean;
runStartTime?: number;
runStartTimeAdjustment?: number;
runEndTime?: number;
renderDuration?: { [key: string]: number };
}
export type NotebookDocumentMetadata = Record<string, unknown>;
export interface NotebookCellStatusBarItem {
readonly alignment: CellStatusbarAlignment;
readonly priority?: number;
readonly text: string;
// readonly color?: string | ThemeColor;
// readonly backgroundColor?: string | ThemeColor;
readonly tooltip?: string | MarkdownString;
readonly command?: string | Command;
// readonly accessibilityInformation?: IAccessibilityInformation;
readonly opacity?: string;
readonly onlyShowWhenActive?: boolean;
}
export const enum CellStatusbarAlignment {
Left = 1,
Right = 2
}
export type TransientCellMetadata = { readonly [K in keyof NotebookCellMetadata]?: boolean };
export type CellContentMetadata = { readonly [K in keyof NotebookCellMetadata]?: boolean };
export type TransientDocumentMetadata = { readonly [K in keyof NotebookDocumentMetadata]?: boolean };
export interface TransientOptions {
readonly transientOutputs: boolean;
readonly transientCellMetadata: TransientCellMetadata;
readonly transientDocumentMetadata: TransientDocumentMetadata;
}
export interface CellOutputItem {
readonly mime: string;
readonly data: BinaryBuffer;
}
export interface CellOutput {
outputId: string;
outputs: CellOutputItem[];
metadata?: Record<string, unknown>;
}
export interface NotebookCellCollapseState {
inputCollapsed?: boolean;
outputCollapsed?: boolean;
}
export interface CellData {
source: string;
language: string;
cellKind: CellKind;
outputs: CellOutput[];
metadata?: NotebookCellMetadata;
internalMetadata?: NotebookCellInternalMetadata;
collapseState?: NotebookCellCollapseState;
}
export interface NotebookDocumentMetadataEdit {
editType: CellEditType.DocumentMetadata;
metadata: NotebookDocumentMetadata;
}
export interface NotebookData {
readonly cells: CellData[];
readonly metadata: NotebookDocumentMetadata;
}
export interface NotebookContributionData {
extension?: string;
providerDisplayName: string;
displayName: string;
filenamePattern: (string)[];
exclusive: boolean;
}
export interface NotebookCellTextModelSplice<T> {
start: number,
deleteCount: number,
newItems: T[]
/**
* In case of e.g. deletion, the handle of the first cell that was deleted.
* -1 in case of new Cells are added at the end.
*/
startHandle: number,
};
export enum NotebookCellsChangeType {
ModelChange = 1,
Move = 2,
ChangeCellLanguage = 5,
Initialize = 6,
ChangeCellMetadata = 7,
Output = 8,
OutputItem = 9,
ChangeCellContent = 10,
ChangeDocumentMetadata = 11,
ChangeCellInternalMetadata = 12,
// ChangeCellMime = 13,
Unknown = 100
}
export interface NotebookCellsChangeLanguageEvent {
readonly kind: NotebookCellsChangeType.ChangeCellLanguage;
readonly index: number;
readonly language: string;
}
export interface NotebookCellsChangeMetadataEvent {
readonly kind: NotebookCellsChangeType.ChangeCellMetadata;
readonly index: number;
readonly metadata: NotebookCellMetadata;
}
export interface NotebookCellsChangeInternalMetadataEvent {
readonly kind: NotebookCellsChangeType.ChangeCellInternalMetadata;
readonly index: number;
readonly internalMetadata: NotebookCellInternalMetadata;
}
export interface NotebookCellContentChangeEvent {
readonly kind: NotebookCellsChangeType.ChangeCellContent;
readonly index: number;
}
export interface NotebookModelResource {
notebookModelUri: URI;
}
export namespace NotebookModelResource {
export function is(item: unknown): item is NotebookModelResource {
return isObject<NotebookModelResource>(item) && item.notebookModelUri instanceof URI;
}
export function create(uri: URI): NotebookModelResource {
return { notebookModelUri: uri };
}
}
export interface NotebookCellModelResource {
notebookCellModelUri: URI;
}
export namespace NotebookCellModelResource {
export function is(item: unknown): item is NotebookCellModelResource {
return isObject<NotebookCellModelResource>(item) && item.notebookCellModelUri instanceof URI;
}
export function create(uri: URI): NotebookCellModelResource {
return { notebookCellModelUri: uri };
}
}
export enum NotebookCellExecutionState {
Unconfirmed = 1,
Pending = 2,
Executing = 3
}
export enum CellExecutionUpdateType {
Output = 1,
OutputItems = 2,
ExecutionState = 3,
}
export interface CellExecuteOutputEdit {
editType: CellExecutionUpdateType.Output;
cellHandle: number;
append?: boolean;
outputs: CellOutput[];
}
export interface CellExecuteOutputItemEdit {
editType: CellExecutionUpdateType.OutputItems;
append?: boolean;
outputId: string,
items: CellOutputItem[];
}
export interface CellExecutionStateUpdateDto {
editType: CellExecutionUpdateType.ExecutionState;
executionOrder?: number;
runStartTime?: number;
didPause?: boolean;
isPaused?: boolean;
}
export interface CellMetadataEdit {
editType: CellEditType.Metadata;
index: number;
metadata: NotebookCellMetadata;
}
export const enum CellEditType {
Replace = 1,
Output = 2,
Metadata = 3,
CellLanguage = 4,
DocumentMetadata = 5,
Move = 6,
OutputItems = 7,
PartialMetadata = 8,
PartialInternalMetadata = 9,
}
export interface NotebookKernelSourceAction {
readonly label: string;
readonly description?: string;
readonly detail?: string;
readonly command?: string | Command;
readonly documentation?: UriComponents | string;
}
/**
* Whether the provided mime type is a text stream like `stdout`, `stderr`.
*/
export function isTextStreamMime(mimeType: string): boolean {
return ['application/vnd.code.notebook.stdout', 'application/vnd.code.notebook.stderr'].includes(mimeType);
}
export namespace CellUri {
export const cellUriScheme = 'vscode-notebook-cell';
export const outputUriScheme = 'vscode-notebook-cell-output';
const _lengths = ['W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f'];
const _padRegexp = new RegExp(`^[${_lengths.join('')}]+`);
const _radix = 7;
export function generate(notebook: URI, handle: number): URI {
const s = handle.toString(_radix);
const p = s.length < _lengths.length ? _lengths[s.length - 1] : 'z';
const fragment = `${p}${s}s${Buffer.from(BinaryBuffer.fromString(notebook.scheme).buffer).toString('base64')}`;
return notebook.withScheme(cellUriScheme).withFragment(fragment);
}
export function parse(cell: URI): { notebook: URI; handle: number } | undefined {
if (cell.scheme !== cellUriScheme) {
return undefined;
}
const idx = cell.fragment.indexOf('s');
if (idx < 0) {
return undefined;
}
const handle = parseInt(cell.fragment.substring(0, idx).replace(_padRegexp, ''), _radix);
const parsedScheme = Buffer.from(cell.fragment.substring(idx + 1), 'base64').toString();
if (isNaN(handle)) {
return undefined;
}
return {
handle,
notebook: cell.withScheme(parsedScheme).withoutFragment()
};
}
export function generateCellOutputUri(notebook: URI, outputId?: string): URI {
return notebook
.withScheme(outputUriScheme)
.withQuery(`op${outputId ?? ''},${notebook.scheme !== 'file' ? notebook.scheme : ''}`);
};
export function parseCellOutputUri(uri: URI): { notebook: URI; outputId?: string } | undefined {
if (uri.scheme !== outputUriScheme) {
return;
}
const match = /^op([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})?\,(.*)$/i.exec(uri.query);
if (!match) {
return undefined;
}
const outputId = match[1] || undefined;
const scheme = match[2];
return {
outputId,
notebook: uri.withScheme(scheme || 'file').withoutQuery()
};
}
export function generateCellPropertyUri(notebook: URI, handle: number, cellScheme: string): URI {
return CellUri.generate(notebook, handle).withScheme(cellScheme);
}
export function parseCellPropertyUri(uri: URI, propertyScheme: string): { notebook: URI; handle: number } | undefined {
if (uri.scheme !== propertyScheme) {
return undefined;
}
return CellUri.parse(uri.withScheme(cellUriScheme));
}
}

View File

@@ -0,0 +1,92 @@
// *****************************************************************************
// 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { nls } from '@theia/core';
import { interfaces } from '@theia/core/shared/inversify';
import { PreferenceContribution, PreferenceSchema } from '@theia/core/lib/common/preferences/preference-schema';
export namespace NotebookPreferences {
export const NOTEBOOK_LINE_NUMBERS = 'notebook.lineNumbers';
export const OUTPUT_LINE_HEIGHT = 'notebook.output.lineHeight';
export const OUTPUT_FONT_SIZE = 'notebook.output.fontSize';
export const OUTPUT_FONT_FAMILY = 'notebook.output.fontFamily';
export const OUTPUT_SCROLLING = 'notebook.output.scrolling';
export const OUTPUT_WORD_WRAP = 'notebook.output.wordWrap';
export const OUTPUT_LINE_LIMIT = 'notebook.output.textLineLimit';
}
export const notebookPreferenceSchema: PreferenceSchema = {
properties: {
[NotebookPreferences.NOTEBOOK_LINE_NUMBERS]: {
type: 'string',
enum: ['on', 'off'],
default: 'off',
description: nls.localizeByDefault('Controls the display of line numbers in the cell editor.')
},
[NotebookPreferences.OUTPUT_LINE_HEIGHT]: {
// eslint-disable-next-line max-len
markdownDescription: nls.localizeByDefault('Line height of the output text within notebook cells.\n - When set to 0, editor line height is used.\n - Values between 0 and 8 will be used as a multiplier with the font size.\n - Values greater than or equal to 8 will be used as effective values.'),
type: 'number',
default: 0,
tags: ['notebookLayout', 'notebookOutputLayout']
},
[NotebookPreferences.OUTPUT_FONT_SIZE]: {
markdownDescription: nls.localizeByDefault('Font size for the output text within notebook cells. When set to 0, {0} is used.', '`#editor.fontSize#`'),
type: 'number',
default: 0,
tags: ['notebookLayout', 'notebookOutputLayout']
},
[NotebookPreferences.OUTPUT_FONT_FAMILY]: {
markdownDescription: nls.localizeByDefault('The font family of the output text within notebook cells. When set to empty, the {0} is used.', '`#editor.fontFamily#`'),
type: 'string',
tags: ['notebookLayout', 'notebookOutputLayout']
},
[NotebookPreferences.OUTPUT_SCROLLING]: {
markdownDescription: nls.localizeByDefault('Initially render notebook outputs in a scrollable region when longer than the limit.'),
type: 'boolean',
tags: ['notebookLayout', 'notebookOutputLayout'],
default: false
},
[NotebookPreferences.OUTPUT_WORD_WRAP]: {
markdownDescription: nls.localizeByDefault('Controls whether the lines in output should wrap.'),
type: 'boolean',
tags: ['notebookLayout', 'notebookOutputLayout'],
default: false
},
[NotebookPreferences.OUTPUT_LINE_LIMIT]: {
markdownDescription: nls.localizeByDefault(
'Controls how many lines of text are displayed in a text output. If {0} is enabled, this setting is used to determine the scroll height of the output.',
'`#notebook.output.scrolling#`'),
type: 'number',
default: 30,
tags: ['notebookLayout', 'notebookOutputLayout'],
minimum: 1,
},
}
};
export const NotebookPreferenceContribution = Symbol('NotebookPreferenceContribution');
export function bindNotebookPreferences(bind: interfaces.Bind): void {
// We don't need a NotebookPreferenceConfiguration class, so there's no preference proxy to bind
bind(NotebookPreferenceContribution).toConstantValue({ schema: notebookPreferenceSchema });
bind(PreferenceContribution).toService(NotebookPreferenceContribution);
}

View File

@@ -0,0 +1,35 @@
// *****************************************************************************
// 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
// *****************************************************************************
export interface NotebookTypeDescriptor {
readonly type: string;
readonly displayName: string;
readonly priority?: string | undefined;
readonly selector?: readonly NotebookFileSelector[];
}
export interface NotebookFileSelector {
readonly filenamePattern?: string;
readonly excludeFileNamePattern?: string;
}
export interface NotebookRendererDescriptor {
readonly id: string;
readonly displayName: string;
readonly mimeTypes: string[];
readonly entrypoint: string | { readonly extends: string; readonly path: string };
readonly requiresMessaging?: 'always' | 'optional' | 'never'
}

View File

@@ -0,0 +1,30 @@
// *****************************************************************************
// 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
// *****************************************************************************
/**
* [start, end]
*/
export interface CellRange {
/**
* zero based index
*/
start: number;
/**
* zero based index
*/
end: number;
}

View File

@@ -0,0 +1,22 @@
// *****************************************************************************
// Copyright (C) 2025 STMicroelectronics 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 { ContainerModule } from '@theia/core/shared/inversify';
import { bindNotebookPreferences } from '../common/notebook-preferences';
export default new ContainerModule(bind => {
bindNotebookPreferences(bind);
});

View File

@@ -0,0 +1,28 @@
{
"extends": "../../configs/base.tsconfig",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib"
},
"include": [
"src"
],
"references": [
{
"path": "../core"
},
{
"path": "../editor"
},
{
"path": "../filesystem"
},
{
"path": "../monaco"
},
{
"path": "../outline-view"
}
]
}