diff --git a/packages/core/src/browser/command-open-handler.ts b/packages/core/src/browser/command-open-handler.ts index f3e7323b04c07..2692ae5347765 100644 --- a/packages/core/src/browser/command-open-handler.ts +++ b/packages/core/src/browser/command-open-handler.ts @@ -41,7 +41,7 @@ export class CommandOpenHandler implements OpenHandler { try { args = JSON.parse(uri.query); } catch { - // ignore error + args = uri.query; } } if (!Array.isArray(args)) { diff --git a/packages/notebook/src/browser/contributions/notebook-output-action-contribution.ts b/packages/notebook/src/browser/contributions/notebook-output-action-contribution.ts new file mode 100644 index 0000000000000..c7756fe507f43 --- /dev/null +++ b/packages/notebook/src/browser/contributions/notebook-output-action-contribution.ts @@ -0,0 +1,82 @@ +// ***************************************************************************** +// 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.toDefaultLocalizedCommand({ + id: 'cellOutput.enableScrolling', + }); + + export const OPEN_LARGE_OUTPUT = Command.toDefaultLocalizedCommand({ + id: 'workbench.action.openLargeOutput', + label: 'Open Large Output' + }); +} + +@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]; + } + } + } + } + +} diff --git a/packages/notebook/src/browser/notebook-cell-resource-resolver.ts b/packages/notebook/src/browser/notebook-cell-resource-resolver.ts index 1d6af928cc9b8..bfe3b3e606917 100644 --- a/packages/notebook/src/browser/notebook-cell-resource-resolver.ts +++ b/packages/notebook/src/browser/notebook-cell-resource-resolver.ts @@ -65,7 +65,7 @@ export class NotebookCellResourceResolver implements ResourceResolver { protected readonly notebookService: NotebookService; async resolve(uri: URI): Promise { - if (uri.scheme !== CellUri.scheme) { + if (uri.scheme !== CellUri.cellUriScheme) { throw new Error(`Cannot resolve cell uri with scheme '${uri.scheme}'`); } @@ -90,3 +90,41 @@ export class NotebookCellResourceResolver implements ResourceResolver { } } + +@injectable() +export class NotebookOutputResourceResolver implements ResourceResolver { + + @inject(NotebookService) + protected readonly notebookService: NotebookService; + + async resolve(uri: URI): Promise { + 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, + }; + } + +} diff --git a/packages/notebook/src/browser/notebook-frontend-module.ts b/packages/notebook/src/browser/notebook-frontend-module.ts index f74e129578d67..8f5a3c50c8e85 100644 --- a/packages/notebook/src/browser/notebook-frontend-module.ts +++ b/packages/notebook/src/browser/notebook-frontend-module.ts @@ -24,7 +24,7 @@ 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 } from './notebook-cell-resource-resolver'; +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 { NotebookCellToolbarFactory } from './view/notebook-cell-toolbar-factory'; @@ -41,6 +41,7 @@ import { NotebookEditorWidgetService } from './service/notebook-editor-widget-se import { NotebookRendererMessagingService } from './service/notebook-renderer-messaging-service'; import { NotebookColorContribution } from './contributions/notebook-color-contribution'; import { NotebookMonacoTextModelService } from './service/notebook-monaco-text-model-service'; +import { NotebookOutputActionContribution } from './contributions/notebook-output-action-contribution'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(NotebookColorContribution).toSelf().inSingletonScope(); @@ -67,6 +68,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(NotebookCellResourceResolver).toSelf().inSingletonScope(); bind(ResourceResolver).toService(NotebookCellResourceResolver); bind(NotebookModelResolverService).toSelf().inSingletonScope(); + bind(NotebookOutputResourceResolver).toSelf().inSingletonScope(); + bind(ResourceResolver).toService(NotebookOutputResourceResolver); bind(NotebookCellActionContribution).toSelf().inSingletonScope(); bind(MenuContribution).toService(NotebookCellActionContribution); @@ -78,6 +81,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { 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) ); diff --git a/packages/notebook/src/browser/view-model/notebook-cell-model.ts b/packages/notebook/src/browser/view-model/notebook-cell-model.ts index 1f20849de19ff..eb2ad2a126ed6 100644 --- a/packages/notebook/src/browser/view-model/notebook-cell-model.ts +++ b/packages/notebook/src/browser/view-model/notebook-cell-model.ts @@ -298,6 +298,13 @@ export class NotebookCellModel implements NotebookCell, Disposable { }); return ref.object; } + + restartOutputRenderer(outputId: string): void { + const output = this.outputs.find(out => out.outputId === outputId); + if (output) { + this.onDidChangeOutputItemsEmitter.fire(output); + } + } } function computeRunStartTimeAdjustment(oldMetadata: NotebookCellInternalMetadata, newMetadata: NotebookCellInternalMetadata): number | undefined { diff --git a/packages/notebook/src/common/notebook-common.ts b/packages/notebook/src/common/notebook-common.ts index c7db67d8f9b67..bf7355035c796 100644 --- a/packages/notebook/src/common/notebook-common.ts +++ b/packages/notebook/src/common/notebook-common.ts @@ -261,7 +261,8 @@ export function isTextStreamMime(mimeType: string): boolean { export namespace CellUri { - export const scheme = 'vscode-notebook-cell'; + 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('')}]+`); @@ -273,11 +274,11 @@ export namespace CellUri { 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(scheme).withFragment(fragment); + return notebook.withScheme(cellUriScheme).withFragment(fragment); } export function parse(cell: URI): { notebook: URI; handle: number } | undefined { - if (cell.scheme !== scheme) { + if (cell.scheme !== cellUriScheme) { return undefined; } @@ -298,6 +299,30 @@ export namespace CellUri { }; } + 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); } @@ -307,6 +332,6 @@ export namespace CellUri { return undefined; } - return CellUri.parse(uri.withScheme(scheme)); + return CellUri.parse(uri.withScheme(cellUriScheme)); } } diff --git a/packages/plugin-ext/src/main/browser/notebooks/renderers/cell-output-webview.tsx b/packages/plugin-ext/src/main/browser/notebooks/renderers/cell-output-webview.tsx index 0d4c48e4f8a87..2e8b91a5a0c24 100644 --- a/packages/plugin-ext/src/main/browser/notebooks/renderers/cell-output-webview.tsx +++ b/packages/plugin-ext/src/main/browser/notebooks/renderers/cell-output-webview.tsx @@ -117,7 +117,20 @@ export class CellOutputWebviewImpl implements CellOutputWebview, Disposable { } this.webviewWidget = await this.widgetManager.getOrCreateWidget(WebviewWidget.FACTORY_ID, { id: this.id }); - this.webviewWidget.setContentOptions({ allowScripts: true }); + this.webviewWidget.setContentOptions({ + allowScripts: true, + // eslint-disable-next-line max-len + // list taken from https://github.com/microsoft/vscode/blob/a27099233b956dddc2536d4a0d714ab36266d897/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts#L762-L774 + enableCommandUris: [ + 'github-issues.authNow', + 'workbench.extensions.search', + 'workbench.action.openSettings', + '_notebook.selectKernel', + 'jupyter.viewOutput', + 'workbench.action.openLargeOutput', + 'cellOutput.enableScrolling', + ] + }); this.webviewWidget.setHTML(await this.createWebviewContent()); this.webviewWidget.onMessage((message: FromWebviewMessage) => {