Skip to content

Commit cf445a5

Browse files
subhash-arabhiAchal1607
authored andcommitted
Improvements in notebook cell execution display
- Corrected execution counter - Introduced mimeTypeHandler and executionSummary and refactored code - Added new commands to localisation english file - Added tests for mimeType and executionSummary of notebook code - New and unexecuted cells will not display success or failure
1 parent c29085e commit cf445a5

File tree

12 files changed

+500
-54
lines changed

12 files changed

+500
-54
lines changed

vscode/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,7 @@
483483
},
484484
{
485485
"command": "jdk.jshell.project",
486-
"title": "Open JShell in context of a Project",
486+
"title": "%jdk.jshell.project%",
487487
"category": "Java"
488488
},
489489
{
@@ -591,7 +591,7 @@
591591
},
592592
{
593593
"command": "jdk.notebook.new",
594-
"title": "Create New Notebook...",
594+
"title": "%jdk.notebook.new%",
595595
"category": "Java",
596596
"icon": "$(new-file)"
597597
}

vscode/package.nls.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
"jdk.workspace.new": "New File from Template...",
77
"jdk.workspace.newproject": "New Project...",
88
"jdk.java.goto.super.implementation": "Go to Super Implementation",
9+
"jdk.jshell.project": "Open JShell in context of a Project",
910
"jdk.open.type": "Open Type...",
1011
"jdk.foundProjects.deleteEntry": "Delete",
1112
"jdk.Edit.org.openide.actions.DeleteAction": "Delete",
1213
"workbench.action.debug.run": "Run Without Debugging",
1314
"workbench.action.debug.start": "Start Debugging",
15+
"jdk.notebook.new": "Create New Notebook...",
1416
"jdk.project.run": "Run Project Without Debugging",
1517
"jdk.project.debug": "Debug Project",
1618
"jdk.project.test": "Test Project",

vscode/src/notebooks/codeCellExecution.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { commands, NotebookCell, NotebookCellExecution, NotebookCellOutput, NotebookController } from "vscode";
22
import { LOGGER } from "../logger";
33
import { NotebookCellExecutionResult } from "../lsp/protocol";
4-
import { createErrorOutputItem, createOutputItem } from "./utils";
4+
import { createErrorOutputItem } from "./utils";
55
import { nbCommands } from "../commands/commands";
66
import { mimeTypes } from "./constants";
7+
import { MimeTypeHandler } from "./mimeTypeHandler";
78

89
export class CodeCellExecution {
910
private controller?: NotebookController;
@@ -32,7 +33,7 @@ export class CodeCellExecution {
3233
diagnostics: string[] | undefined,
3334
errorDiagnostics: string[] | undefined,
3435
metadata: any,
35-
executionOrder: number) => {
36+
executionOrder: number | undefined) => {
3637

3738
if (!this.isExecutionStarted) {
3839
this.handleExecutionStart(executionOrder);
@@ -74,7 +75,7 @@ export class CodeCellExecution {
7475
await this.execution!.replaceOutputItems(createErrorOutputItem(updatedData), this.output);
7576
} else {
7677
await this.execution!.replaceOutputItems(
77-
createOutputItem(updatedData, mimeType),
78+
new MimeTypeHandler(mimeType).makeOutputItem(updatedData),
7879
this.output
7980
);
8081
}
@@ -97,7 +98,7 @@ export class CodeCellExecution {
9798
}
9899
}
99100

100-
private handleExecutionStart = async (executionOrder: number) => {
101+
private handleExecutionStart = async (executionOrder: number | undefined) => {
101102
if (this.controller) {
102103
this.execution = this.controller.createNotebookCellExecution(this.cell);
103104
this.isExecutionStarted = true;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright (c) 2024, Oracle and/or its affiliates.
3+
*
4+
* Licensed to the Apache Software Foundation (ASF) under one
5+
* or more contributor license agreements. See the NOTICE file
6+
* distributed with this work for additional information
7+
* regarding copyright ownership. The ASF licenses this file
8+
* to you under the Apache License, Version 2.0 (the
9+
* "License"); you may not use this file except in compliance
10+
* with the License. You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing,
15+
* software distributed under the License is distributed on an
16+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17+
* KIND, either express or implied. See the License for the
18+
* specific language governing permissions and limitations
19+
* under the License.
20+
*/
21+
22+
export interface ExecutionSummaryData {
23+
executionOrder?: number | null;
24+
success?: boolean;
25+
}
26+
27+
export class ExecutionSummary {
28+
constructor(
29+
public executionOrder: number | null = null,
30+
public success: boolean = false
31+
) {}
32+
33+
static fromMetadata(
34+
meta?: ExecutionSummaryData,
35+
fallbackExecCount?: number | null
36+
): ExecutionSummary {
37+
const order =
38+
meta?.executionOrder != null
39+
? meta.executionOrder
40+
: fallbackExecCount ?? null;
41+
const success = meta?.success ?? false;
42+
return new ExecutionSummary(order, success);
43+
}
44+
45+
toJSON(): ExecutionSummaryData {
46+
return {
47+
executionOrder: this.executionOrder,
48+
success: this.success,
49+
};
50+
}
51+
}

vscode/src/notebooks/kernel.ts

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,16 @@
2222
import { globalState } from '../globalState';
2323
import { isNbCommandRegistered } from '../commands/utils';
2424
import { nbCommands } from '../commands/commands';
25-
import { createErrorOutput, createOutputItem } from './utils';
2625
import { NotebookCellExecutionResult } from '../lsp/protocol';
27-
import { NotebookCell, NotebookController, NotebookDocument, Disposable, notebooks, commands, NotebookCellOutput } from 'vscode';
26+
import { NotebookCell, NotebookController, NotebookDocument, Disposable, notebooks, commands, NotebookCellOutput, workspace } from 'vscode';
2827
import { LanguageClient } from 'vscode-languageclient/node';
2928
import { l10n } from '../localiser';
3029
import { ijnbConstants, ipynbConstants, supportLanguages } from './constants';
3130
import { LOGGER } from '../logger';
3231
import { CodeCellExecution } from './codeCellExecution';
33-
import { isError } from '../utils';
32+
import { isError, isString } from '../utils';
33+
import { MimeTypeHandler } from './mimeTypeHandler';
34+
import { createErrorOutput } from './utils';
3435

3536
export class IJNBKernel implements Disposable {
3637
private readonly controllers: NotebookController[] = [];
@@ -86,6 +87,7 @@ export class IJNBKernel implements Disposable {
8687
const cellId = cell.document.uri.toString();
8788
const sourceCode = cell.document.getText();
8889
const codeCellExecution = new CodeCellExecution(controller.id, notebookId, cell);
90+
this.getIncrementedExecutionCounter(notebookId);
8991
try {
9092
this.cellControllerIdMap.set(cellId, codeCellExecution);
9193
const client: LanguageClient = await globalState.getClientPromise().client;
@@ -141,37 +143,43 @@ export class IJNBKernel implements Disposable {
141143

142144
private handleUnkownLanguageTypeExecution = async (notebookId: string, cell: NotebookCell, controller: NotebookController) => {
143145
const exec = controller.createNotebookCellExecution(cell);
144-
exec.executionOrder = this.getExecutionCounterAndIncrement(notebookId);
146+
exec.executionOrder = this.getIncrementedExecutionCounter(notebookId);
145147
exec.start(Date.now());
146148
await exec.replaceOutput(createErrorOutput(new Error(`Doesn't support ${cell.document.languageId} execution`)));
147-
exec.end(true, Date.now());
149+
exec.end(false, Date.now());
148150
}
149151

150-
private getExecutionCounterAndIncrement = (notebookId: string) => {
151-
const next = IJNBKernel.executionCounter.get(notebookId) ?? 1;
152-
IJNBKernel.executionCounter.set(notebookId, next + 1);
152+
private getIncrementedExecutionCounter = (notebookId: string) => {
153+
const next = (IJNBKernel.executionCounter.get(notebookId) ?? 0) + 1;
154+
IJNBKernel.executionCounter.set(notebookId, next);
153155
return next;
154156
}
155157

156158
private getExecutionCounter = (notebookId: string) => {
157-
return IJNBKernel.executionCounter.get(notebookId) ?? 1;
159+
return IJNBKernel.executionCounter.get(notebookId);
158160
}
159161

160162
private handleMarkdownCellExecution = async (notebookId: string, cell: NotebookCell, controller: NotebookController) => {
161163
const exec = controller.createNotebookCellExecution(cell);
162164
const mimeType = 'text/markdown';
163-
exec.executionOrder = this.getExecutionCounterAndIncrement(notebookId);
165+
exec.executionOrder = this.getIncrementedExecutionCounter(notebookId);
164166
try {
165167
exec.start(Date.now());
166168
await exec.replaceOutput([
167-
new NotebookCellOutput([createOutputItem(cell.document.getText(), mimeType)]),
169+
new NotebookCellOutput([new MimeTypeHandler(mimeType).makeOutputItem(cell.document.getText())]),
168170
]);
171+
exec.end(true, Date.now());
169172
} catch (error) {
170173
await exec.replaceOutput(createErrorOutput(error as Error));
171-
} finally {
172-
exec.end(true, Date.now());
174+
exec.end(false, Date.now());
173175
}
174176
}
177+
178+
cleanUpKernel = workspace.onDidCloseNotebookDocument(doc => {
179+
if (doc.notebookType === ijnbConstants.NOTEBOOK_TYPE) {
180+
IJNBKernel.executionCounter.delete(doc.uri.toString());
181+
}
182+
});
175183
}
176184

177185
export const notebookKernel = new IJNBKernel();
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright (c) 2024, Oracle and/or its affiliates.
3+
*
4+
* Licensed to the Apache Software Foundation (ASF) under one
5+
* or more contributor license agreements. See the NOTICE file
6+
* distributed with this work for additional information
7+
* regarding copyright ownership. The ASF licenses this file
8+
* to you under the Apache License, Version 2.0 (the
9+
* "License"); you may not use this file except in compliance
10+
* with the License. You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing,
15+
* software distributed under the License is distributed on an
16+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17+
* KIND, either express or implied. See the License for the
18+
* specific language governing permissions and limitations
19+
* under the License.
20+
*/
21+
22+
import { Buffer } from 'buffer';
23+
import * as vscode from 'vscode';
24+
import { IMimeBundle } from './types';
25+
import { mimeTypes } from './constants';
26+
27+
export type DataOrBytes = string | Uint8Array;
28+
29+
export class MimeTypeHandler {
30+
constructor(public readonly value: string) {}
31+
get isText(): boolean {
32+
return this.value === mimeTypes.TEXT;
33+
}
34+
get isImage(): boolean {
35+
return this.value.startsWith('image/');
36+
}
37+
38+
static toBytes(data: DataOrBytes): Uint8Array {
39+
if (typeof data === 'string') {
40+
return Buffer.from(data, 'base64');
41+
}
42+
return data;
43+
}
44+
45+
static toString(data: DataOrBytes): string {
46+
if (typeof data === 'string') {
47+
return data;
48+
}
49+
return new TextDecoder().decode(data);
50+
}
51+
52+
makeOutputItem(data: DataOrBytes): vscode.NotebookCellOutputItem {
53+
if (this.isImage) {
54+
const bytes = MimeTypeHandler.toBytes(data);
55+
return new vscode.NotebookCellOutputItem(bytes, this.value);
56+
}
57+
const text = MimeTypeHandler.toString(data);
58+
return vscode.NotebookCellOutputItem.text(text, this.value);
59+
}
60+
61+
static itemsFromBundle(bundle: IMimeBundle): vscode.NotebookCellOutputItem[] {
62+
return Object.entries(bundle).flatMap(([mime, data]) => {
63+
const mt = new MimeTypeHandler(mime);
64+
if (mt.isText || mt.isImage) {
65+
const payload = Array.isArray(data) ? data.join('') : data;
66+
return [mt.makeOutputItem(payload)];
67+
}
68+
return [];
69+
});
70+
}
71+
}

vscode/src/notebooks/register.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,5 @@ export const registerNotebookProviders = (context: ExtensionContext) => {
3131
));
3232

3333
context.subscriptions.push(notebookKernel);
34+
context.subscriptions.push(notebookKernel.cleanUpKernel);
3435
}

vscode/src/notebooks/utils.ts

Lines changed: 20 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,11 @@ import {
3131
IMetadata
3232
} from './types';
3333
import { randomUUID } from 'crypto';
34-
import { isError, isString } from '../utils';
34+
import { isString } from '../utils';
3535
import { mimeTypes } from './constants';
36+
import { MimeTypeHandler } from './mimeTypeHandler';
37+
import { ExecutionSummary } from './executionSummary';
38+
3639

3740
export function base64ToUint8Array(base64: string): Uint8Array {
3841
if (typeof Buffer !== 'undefined' && typeof Buffer.from === 'function') {
@@ -56,15 +59,6 @@ export function uint8ArrayToBase64(data: Uint8Array): string {
5659
return btoa(binary);
5760
}
5861

59-
export function createOutputItem(data: string | Uint8Array, mimeType: string): vscode.NotebookCellOutputItem {
60-
if (mimeType.startsWith('image/')) {
61-
const bytes = typeof data === 'string' ? base64ToUint8Array(data) : data;
62-
return new vscode.NotebookCellOutputItem(bytes, mimeType);
63-
}
64-
const text = typeof data === 'string' ? data : new TextDecoder().decode(data);
65-
return vscode.NotebookCellOutputItem.text(text, mimeType);
66-
}
67-
6862
export const createErrorOutput = (err: string | Error) => {
6963
return new vscode.NotebookCellOutput([createErrorOutputItem(err)]);
7064
}
@@ -86,15 +80,16 @@ export function parseCell(cell: ICell): vscode.NotebookCellData {
8680
const cellData = new vscode.NotebookCellData(kind, value, language);
8781
cellData.metadata = { id: cell.id, ...cell.metadata };
8882
if (cell.cell_type === 'code') {
89-
90-
const metaExec = (cell.metadata as IMetadata).executionSummary;
91-
const executionOrder = metaExec?.executionOrder ?? cell.execution_count ?? undefined;
92-
const success = metaExec?.success ?? undefined;
93-
94-
cellData.executionSummary = {
95-
executionOrder,
96-
success,
97-
};
83+
const execSummary = ExecutionSummary.fromMetadata(
84+
(cell.metadata as IMetadata).executionSummary,
85+
cell.execution_count ?? null
86+
);
87+
if (execSummary.executionOrder) {
88+
cellData.executionSummary = {
89+
executionOrder: execSummary.executionOrder ?? undefined,
90+
success: execSummary.success,
91+
};
92+
}
9893

9994
if (Array.isArray(cell.outputs)) {
10095
const outputs = cell.outputs.flatMap((out: IOutput) => {
@@ -119,7 +114,7 @@ export function parseOutput(raw: IOutput): vscode.NotebookCellOutput[] {
119114
case 'stream':
120115
outputs.push(
121116
new vscode.NotebookCellOutput([
122-
vscode.NotebookCellOutputItem.text(Array.isArray(raw.text) ? raw.text.join('') : raw.text),
117+
new MimeTypeHandler(mimeTypes.TEXT).makeOutputItem(Array.isArray(raw.text) ? raw.text.join('') : raw.text),
123118
])
124119
);
125120
break;
@@ -138,19 +133,8 @@ export function parseOutput(raw: IOutput): vscode.NotebookCellOutput[] {
138133

139134
case 'display_data':
140135
case 'execute_result':
141-
const items: vscode.NotebookCellOutputItem[] = [];
142-
const bundle = raw.data || {};
143-
for (const mime in bundle) {
144-
const data = bundle[mime];
145-
if (mime === mimeTypes.TEXT) {
146-
const text = Array.isArray(data) ? data.join('') : String(data);
147-
items.push(vscode.NotebookCellOutputItem.text(text));
148-
} else if ((mime as string).startsWith('image/')) {
149-
const b64 = Array.isArray(data) ? data.join('') : String(data);
150-
const bytes = base64ToUint8Array(b64);
151-
items.push(new vscode.NotebookCellOutputItem(bytes, mime));
152-
}
153-
}
136+
const bundle = raw.data ?? {};
137+
const items = MimeTypeHandler.itemsFromBundle(bundle);
154138
if (items.length) {
155139
outputs.push(new vscode.NotebookCellOutput(items, raw.metadata));
156140
}
@@ -166,9 +150,10 @@ export function serializeCell(cell: vscode.NotebookCellData): ICell {
166150
if (cell.kind === vscode.NotebookCellKind.Code) {
167151
const exec = cell.executionSummary ?? {};
168152
const executionCount = exec.executionOrder ?? null;
169-
const success = exec.success ?? false;
153+
const success = exec.success;
170154

171-
const metadata = { ...baseMeta, executionSummary: { executionOrder: executionCount, success } };
155+
const execSummary = new ExecutionSummary(executionCount, success);
156+
const metadata = executionCount ? { ...baseMeta, executionSummary: execSummary.toJSON() } : {};
172157

173158
const outputs: IOutput[] = (cell.outputs || []).map((output): IOutput => {
174159
const data: IMimeBundle = {};

vscode/src/test/unit/mocks/vscode/mockVscode.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@ import { URI } from './uri';
1919
import { mockWindowNamespace } from './namespaces/window';
2020
import { mockEnvNamespace } from './namespaces/env';
2121
import { mockedEnums } from './vscodeHostedTypes';
22+
import { NotebookCellOutputItem } from './notebookCellOutputItem';
2223

2324
type VSCode = typeof vscode;
2425
const mockedVSCode: Partial<VSCode> = {};
2526

2627
const mockedVscodeClassesAndTypes = () => {
2728
mockedVSCode.Uri = URI as any;
2829
mockedVSCode.ViewColumn = mockedEnums.viewColumn;
30+
mockedVSCode.NotebookCellOutputItem = NotebookCellOutputItem as any;
2931
}
3032

3133
const mockNamespaces = () => {

0 commit comments

Comments
 (0)