deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
10
packages/notebook/.eslintrc.js
Normal file
10
packages/notebook/.eslintrc.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
extends: [
|
||||
'../../configs/build.eslintrc.json'
|
||||
],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: 'tsconfig.json'
|
||||
}
|
||||
};
|
||||
31
packages/notebook/README.md
Normal file
31
packages/notebook/README.md
Normal 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>
|
||||
58
packages/notebook/package.json
Normal file
58
packages/notebook/package.json
Normal 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"
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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'];
|
||||
}
|
||||
@@ -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'];
|
||||
|
||||
}
|
||||
|
||||
@@ -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.'
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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]
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
29
packages/notebook/src/browser/index.ts
Normal file
29
packages/notebook/src/browser/index.ts
Normal 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';
|
||||
52
packages/notebook/src/browser/notebook-cell-open-handler.ts
Normal file
52
packages/notebook/src/browser/notebook-cell-open-handler.ts
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
130
packages/notebook/src/browser/notebook-cell-resource-resolver.ts
Normal file
130
packages/notebook/src/browser/notebook-cell-resource-resolver.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
103
packages/notebook/src/browser/notebook-editor-widget-factory.ts
Normal file
103
packages/notebook/src/browser/notebook-editor-widget-factory.ts
Normal 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';
|
||||
}
|
||||
|
||||
}
|
||||
381
packages/notebook/src/browser/notebook-editor-widget.tsx
Normal file
381
packages/notebook/src/browser/notebook-editor-widget.tsx
Normal 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);
|
||||
}
|
||||
}
|
||||
138
packages/notebook/src/browser/notebook-frontend-module.ts
Normal file
138
packages/notebook/src/browser/notebook-frontend-module.ts
Normal 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);
|
||||
});
|
||||
128
packages/notebook/src/browser/notebook-open-handler.ts
Normal file
128
packages/notebook/src/browser/notebook-open-handler.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
119
packages/notebook/src/browser/notebook-output-utils.ts
Normal file
119
packages/notebook/src/browser/notebook-output-utils.ts
Normal 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;
|
||||
}
|
||||
85
packages/notebook/src/browser/notebook-renderer-registry.ts
Normal file
85
packages/notebook/src/browser/notebook-renderer-registry.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
54
packages/notebook/src/browser/notebook-type-registry.ts
Normal file
54
packages/notebook/src/browser/notebook-type-registry.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
187
packages/notebook/src/browser/notebook-types.ts
Normal file
187
packages/notebook/src/browser/notebook-types.ts
Normal 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[];
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
357
packages/notebook/src/browser/service/notebook-kernel-service.ts
Normal file
357
packages/notebook/src/browser/service/notebook-kernel-service.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
155
packages/notebook/src/browser/service/notebook-options.ts
Normal file
155
packages/notebook/src/browser/service/notebook-options.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
215
packages/notebook/src/browser/service/notebook-service.ts
Normal file
215
packages/notebook/src/browser/service/notebook-service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
551
packages/notebook/src/browser/style/index.css
Normal file
551
packages/notebook/src/browser/style/index.css
Normal 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);
|
||||
}
|
||||
469
packages/notebook/src/browser/view-model/notebook-cell-model.ts
Normal file
469
packages/notebook/src/browser/view-model/notebook-cell-model.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
532
packages/notebook/src/browser/view-model/notebook-model.ts
Normal file
532
packages/notebook/src/browser/view-model/notebook-model.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
138
packages/notebook/src/browser/view-model/notebook-view-model.ts
Normal file
138
packages/notebook/src/browser/view-model/notebook-view-model.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
312
packages/notebook/src/browser/view/notebook-cell-editor.tsx
Normal file
312
packages/notebook/src/browser/view/notebook-cell-editor.tsx
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
320
packages/notebook/src/browser/view/notebook-cell-list-view.tsx
Normal file
320
packages/notebook/src/browser/view/notebook-cell-list-view.tsx
Normal 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' }} />;
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
71
packages/notebook/src/browser/view/notebook-cell-toolbar.tsx
Normal file
71
packages/notebook/src/browser/view/notebook-cell-toolbar.tsx
Normal 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>;
|
||||
}
|
||||
}
|
||||
|
||||
411
packages/notebook/src/browser/view/notebook-code-cell-view.tsx
Normal file
411
packages/notebook/src/browser/view/notebook-code-cell-view.tsx
Normal 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>;
|
||||
}
|
||||
335
packages/notebook/src/browser/view/notebook-find-widget.tsx
Normal file
335
packages/notebook/src/browser/view/notebook-find-widget.tsx
Normal 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
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
209
packages/notebook/src/browser/view/notebook-main-toolbar.tsx
Normal file
209
packages/notebook/src/browser/view/notebook-main-toolbar.tsx
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
18
packages/notebook/src/common/index.ts
Normal file
18
packages/notebook/src/common/index.ts
Normal 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';
|
||||
342
packages/notebook/src/common/notebook-common.ts
Normal file
342
packages/notebook/src/common/notebook-common.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
92
packages/notebook/src/common/notebook-preferences.ts
Normal file
92
packages/notebook/src/common/notebook-preferences.ts
Normal 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);
|
||||
}
|
||||
35
packages/notebook/src/common/notebook-protocol.ts
Normal file
35
packages/notebook/src/common/notebook-protocol.ts
Normal 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'
|
||||
}
|
||||
30
packages/notebook/src/common/notebook-range.ts
Normal file
30
packages/notebook/src/common/notebook-range.ts
Normal 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;
|
||||
}
|
||||
22
packages/notebook/src/node/notebook-backend-module.ts
Normal file
22
packages/notebook/src/node/notebook-backend-module.ts
Normal 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);
|
||||
});
|
||||
28
packages/notebook/tsconfig.json
Normal file
28
packages/notebook/tsconfig.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user