Skip to content

Commit a77ba54

Browse files
jonah-idenmsujew
andauthored
Support truncated notebook output commands (#13555)
* made notebook truncated output commands working (open as texteditor, as scrollable element, open settings) Signed-off-by: Jonah Iden <[email protected]> * review changes Signed-off-by: Jonah Iden <[email protected]> * fixed type Co-authored-by: Mark Sujew <[email protected]> --------- Signed-off-by: Jonah Iden <[email protected]> Co-authored-by: Mark Sujew <[email protected]>
1 parent 35a340b commit a77ba54

File tree

7 files changed

+179
-8
lines changed

7 files changed

+179
-8
lines changed

packages/core/src/browser/command-open-handler.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export class CommandOpenHandler implements OpenHandler {
4141
try {
4242
args = JSON.parse(uri.query);
4343
} catch {
44-
// ignore error
44+
args = uri.query;
4545
}
4646
}
4747
if (!Array.isArray(args)) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// *****************************************************************************
2+
// Copyright (C) 2024 TypeFox and others.
3+
//
4+
// This program and the accompanying materials are made available under the
5+
// terms of the Eclipse Public License v. 2.0 which is available at
6+
// http://www.eclipse.org/legal/epl-2.0.
7+
//
8+
// This Source Code may also be made available under the following Secondary
9+
// Licenses when the conditions for such availability set forth in the Eclipse
10+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
11+
// with the GNU Classpath Exception which is available at
12+
// https://www.gnu.org/software/classpath/license.html.
13+
//
14+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15+
// *****************************************************************************
16+
17+
import { Command, CommandContribution, CommandRegistry } from '@theia/core';
18+
import { inject, injectable } from '@theia/core/shared/inversify';
19+
import { NotebookEditorWidgetService } from '../service/notebook-editor-widget-service';
20+
import { CellOutput, CellUri } from '../../common';
21+
import { NotebookCellModel } from '../view-model/notebook-cell-model';
22+
import { EditorManager } from '@theia/editor/lib/browser';
23+
24+
export namespace NotebookOutputCommands {
25+
export const ENABLE_SCROLLING = Command.toDefaultLocalizedCommand({
26+
id: 'cellOutput.enableScrolling',
27+
});
28+
29+
export const OPEN_LARGE_OUTPUT = Command.toDefaultLocalizedCommand({
30+
id: 'workbench.action.openLargeOutput',
31+
label: 'Open Large Output'
32+
});
33+
}
34+
35+
@injectable()
36+
export class NotebookOutputActionContribution implements CommandContribution {
37+
38+
@inject(NotebookEditorWidgetService)
39+
protected readonly notebookEditorService: NotebookEditorWidgetService;
40+
41+
@inject(EditorManager)
42+
protected readonly editorManager: EditorManager;
43+
44+
registerCommands(commands: CommandRegistry): void {
45+
commands.registerCommand(NotebookOutputCommands.ENABLE_SCROLLING, {
46+
execute: outputId => {
47+
const [cell, output] = this.findOutputAndCell(outputId) ?? [];
48+
if (cell && output?.metadata) {
49+
output.metadata['scrollable'] = true;
50+
cell.restartOutputRenderer(output.outputId);
51+
}
52+
}
53+
});
54+
55+
commands.registerCommand(NotebookOutputCommands.OPEN_LARGE_OUTPUT, {
56+
execute: outputId => {
57+
const [cell, output] = this.findOutputAndCell(outputId) ?? [];
58+
if (cell && output) {
59+
this.editorManager.open(CellUri.generateCellOutputUri(CellUri.parse(cell.uri)!.notebook, output.outputId));
60+
}
61+
}
62+
});
63+
}
64+
65+
protected findOutputAndCell(output: string): [NotebookCellModel, CellOutput] | undefined {
66+
const model = this.notebookEditorService.focusedEditor?.model;
67+
if (!model) {
68+
return undefined;
69+
}
70+
71+
const outputId = output.slice(0, output.lastIndexOf('-'));
72+
73+
for (const cell of model.cells) {
74+
for (const outputModel of cell.outputs) {
75+
if (outputModel.outputId === outputId) {
76+
return [cell, outputModel];
77+
}
78+
}
79+
}
80+
}
81+
82+
}

packages/notebook/src/browser/notebook-cell-resource-resolver.ts

+39-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export class NotebookCellResourceResolver implements ResourceResolver {
6565
protected readonly notebookService: NotebookService;
6666

6767
async resolve(uri: URI): Promise<Resource> {
68-
if (uri.scheme !== CellUri.scheme) {
68+
if (uri.scheme !== CellUri.cellUriScheme) {
6969
throw new Error(`Cannot resolve cell uri with scheme '${uri.scheme}'`);
7070
}
7171

@@ -90,3 +90,41 @@ export class NotebookCellResourceResolver implements ResourceResolver {
9090
}
9191

9292
}
93+
94+
@injectable()
95+
export class NotebookOutputResourceResolver implements ResourceResolver {
96+
97+
@inject(NotebookService)
98+
protected readonly notebookService: NotebookService;
99+
100+
async resolve(uri: URI): Promise<Resource> {
101+
if (uri.scheme !== CellUri.outputUriScheme) {
102+
throw new Error(`Cannot resolve output uri with scheme '${uri.scheme}'`);
103+
}
104+
105+
const parsedUri = CellUri.parseCellOutputUri(uri);
106+
if (!parsedUri) {
107+
throw new Error(`Cannot parse uri '${uri.toString()}'`);
108+
}
109+
110+
const notebookModel = this.notebookService.getNotebookEditorModel(parsedUri.notebook);
111+
112+
if (!notebookModel) {
113+
throw new Error(`No notebook found for uri '${parsedUri.notebook}'`);
114+
}
115+
116+
const ouputModel = notebookModel.cells.flatMap(cell => cell.outputs).find(output => output.outputId === parsedUri.outputId);
117+
118+
if (!ouputModel) {
119+
throw new Error(`No output found with id '${parsedUri.outputId}' in '${parsedUri.notebook}'`);
120+
}
121+
122+
return {
123+
uri: uri,
124+
dispose: () => { },
125+
readContents: async () => ouputModel.outputs[0].data.toString(),
126+
readOnly: true,
127+
};
128+
}
129+
130+
}

packages/notebook/src/browser/notebook-frontend-module.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { NotebookTypeRegistry } from './notebook-type-registry';
2424
import { NotebookRendererRegistry } from './notebook-renderer-registry';
2525
import { NotebookService } from './service/notebook-service';
2626
import { NotebookEditorWidgetFactory } from './notebook-editor-widget-factory';
27-
import { NotebookCellResourceResolver } from './notebook-cell-resource-resolver';
27+
import { NotebookCellResourceResolver, NotebookOutputResourceResolver } from './notebook-cell-resource-resolver';
2828
import { NotebookModelResolverService } from './service/notebook-model-resolver-service';
2929
import { NotebookCellActionContribution } from './contributions/notebook-cell-actions-contribution';
3030
import { NotebookCellToolbarFactory } from './view/notebook-cell-toolbar-factory';
@@ -41,6 +41,7 @@ import { NotebookEditorWidgetService } from './service/notebook-editor-widget-se
4141
import { NotebookRendererMessagingService } from './service/notebook-renderer-messaging-service';
4242
import { NotebookColorContribution } from './contributions/notebook-color-contribution';
4343
import { NotebookMonacoTextModelService } from './service/notebook-monaco-text-model-service';
44+
import { NotebookOutputActionContribution } from './contributions/notebook-output-action-contribution';
4445

4546
export default new ContainerModule((bind, unbind, isBound, rebind) => {
4647
bind(NotebookColorContribution).toSelf().inSingletonScope();
@@ -67,6 +68,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
6768
bind(NotebookCellResourceResolver).toSelf().inSingletonScope();
6869
bind(ResourceResolver).toService(NotebookCellResourceResolver);
6970
bind(NotebookModelResolverService).toSelf().inSingletonScope();
71+
bind(NotebookOutputResourceResolver).toSelf().inSingletonScope();
72+
bind(ResourceResolver).toService(NotebookOutputResourceResolver);
7073

7174
bind(NotebookCellActionContribution).toSelf().inSingletonScope();
7275
bind(MenuContribution).toService(NotebookCellActionContribution);
@@ -78,6 +81,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
7881
bind(MenuContribution).toService(NotebookActionsContribution);
7982
bind(KeybindingContribution).toService(NotebookActionsContribution);
8083

84+
bind(NotebookOutputActionContribution).toSelf().inSingletonScope();
85+
bind(CommandContribution).toService(NotebookOutputActionContribution);
86+
8187
bind(NotebookEditorWidgetContainerFactory).toFactory(ctx => (props: NotebookEditorProps) =>
8288
createNotebookEditorWidgetContainer(ctx.container, props).get(NotebookEditorWidget)
8389
);

packages/notebook/src/browser/view-model/notebook-cell-model.ts

+7
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,13 @@ export class NotebookCellModel implements NotebookCell, Disposable {
298298
});
299299
return ref.object;
300300
}
301+
302+
restartOutputRenderer(outputId: string): void {
303+
const output = this.outputs.find(out => out.outputId === outputId);
304+
if (output) {
305+
this.onDidChangeOutputItemsEmitter.fire(output);
306+
}
307+
}
301308
}
302309

303310
function computeRunStartTimeAdjustment(oldMetadata: NotebookCellInternalMetadata, newMetadata: NotebookCellInternalMetadata): number | undefined {

packages/notebook/src/common/notebook-common.ts

+29-4
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,8 @@ export function isTextStreamMime(mimeType: string): boolean {
261261

262262
export namespace CellUri {
263263

264-
export const scheme = 'vscode-notebook-cell';
264+
export const cellUriScheme = 'vscode-notebook-cell';
265+
export const outputUriScheme = 'vscode-notebook-cell-output';
265266

266267
const _lengths = ['W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f'];
267268
const _padRegexp = new RegExp(`^[${_lengths.join('')}]+`);
@@ -273,11 +274,11 @@ export namespace CellUri {
273274
const p = s.length < _lengths.length ? _lengths[s.length - 1] : 'z';
274275

275276
const fragment = `${p}${s}s${Buffer.from(BinaryBuffer.fromString(notebook.scheme).buffer).toString('base64')} `;
276-
return notebook.withScheme(scheme).withFragment(fragment);
277+
return notebook.withScheme(cellUriScheme).withFragment(fragment);
277278
}
278279

279280
export function parse(cell: URI): { notebook: URI; handle: number } | undefined {
280-
if (cell.scheme !== scheme) {
281+
if (cell.scheme !== cellUriScheme) {
281282
return undefined;
282283
}
283284

@@ -298,6 +299,30 @@ export namespace CellUri {
298299
};
299300
}
300301

302+
export function generateCellOutputUri(notebook: URI, outputId?: string): URI {
303+
return notebook
304+
.withScheme(outputUriScheme)
305+
.withQuery(`op${outputId ?? ''},${notebook.scheme !== 'file' ? notebook.scheme : ''}`);
306+
};
307+
308+
export function parseCellOutputUri(uri: URI): { notebook: URI; outputId?: string } | undefined {
309+
if (uri.scheme !== outputUriScheme) {
310+
return;
311+
}
312+
313+
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);
314+
if (!match) {
315+
return undefined;
316+
}
317+
318+
const outputId = match[1] || undefined;
319+
const scheme = match[2];
320+
return {
321+
outputId,
322+
notebook: uri.withScheme(scheme || 'file').withoutQuery()
323+
};
324+
}
325+
301326
export function generateCellPropertyUri(notebook: URI, handle: number, cellScheme: string): URI {
302327
return CellUri.generate(notebook, handle).withScheme(cellScheme);
303328
}
@@ -307,6 +332,6 @@ export namespace CellUri {
307332
return undefined;
308333
}
309334

310-
return CellUri.parse(uri.withScheme(scheme));
335+
return CellUri.parse(uri.withScheme(cellUriScheme));
311336
}
312337
}

packages/plugin-ext/src/main/browser/notebooks/renderers/cell-output-webview.tsx

+14-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,20 @@ export class CellOutputWebviewImpl implements CellOutputWebview, Disposable {
117117
}
118118

119119
this.webviewWidget = await this.widgetManager.getOrCreateWidget(WebviewWidget.FACTORY_ID, { id: this.id });
120-
this.webviewWidget.setContentOptions({ allowScripts: true });
120+
this.webviewWidget.setContentOptions({
121+
allowScripts: true,
122+
// eslint-disable-next-line max-len
123+
// list taken from https://github.com/microsoft/vscode/blob/a27099233b956dddc2536d4a0d714ab36266d897/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts#L762-L774
124+
enableCommandUris: [
125+
'github-issues.authNow',
126+
'workbench.extensions.search',
127+
'workbench.action.openSettings',
128+
'_notebook.selectKernel',
129+
'jupyter.viewOutput',
130+
'workbench.action.openLargeOutput',
131+
'cellOutput.enableScrolling',
132+
]
133+
});
121134
this.webviewWidget.setHTML(await this.createWebviewContent());
122135

123136
this.webviewWidget.onMessage((message: FromWebviewMessage) => {

0 commit comments

Comments
 (0)