Skip to content

Commit

Permalink
Improved Deno.jupyter support on repl kernel
Browse files Browse the repository at this point in the history
  • Loading branch information
redking00 committed Nov 24, 2024
1 parent a6a849a commit 821bdd2
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 25 deletions.
1 change: 1 addition & 0 deletions .vscodeignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ node_modules
client
!client/dist/*.js
!client/package.json
!client/src/notebook-controllers/repl-controller/boot/
!client/lsp-server/notebook-lsp.ts
!client/node_modules/vscode-jsonrpc/
2 changes: 1 addition & 1 deletion client/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export async function activate(
): Promise<void> {

kernelControllerInstance = new KernelController();
replControllerInstance = new REPLController();
replControllerInstance = new REPLController(context);

context.subscriptions.push(vscode.workspace.registerNotebookSerializer('nbts', new NBTSSerializer()));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export class KernelController implements IController {
session.tryClose();
this.sessions.delete(fsPath);
});
}, 1000);
}, 5000);
}

public get output() {
Expand Down
114 changes: 114 additions & 0 deletions client/src/notebook-controllers/repl-controller/boot/boot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { encodeBase64 } from "jsr:@std/encoding/base64";


globalThis.Deno.jupyter = (function () {

const $display = Symbol.for("Jupyter.display");

const _displayFunc = async (obj: unknown, options: Deno.jupyter.DisplayOptions = { raw: true }): Promise<void> => {
let data = (obj as any)[$display] ? (await ((obj as any)[$display])()) : obj;
if (!options.raw) {
data = JSON.stringify(data);
}
console.log(`##DISPLAYDATA#2d522e5a-4a6c-4aae-b20c-91c5189948d9##${JSON.stringify(data)}`);
}

function makeDisplayable(obj: unknown): Deno.jupyter.Displayable {
return { [$display]: () => obj } as any;
}

function createTaggedTemplateDisplayable(mediatype: string) {
return (strings: TemplateStringsArray, ...values: unknown[]) => {
const payload = strings.reduce(
(acc, string, i) =>
acc + string + (values[i] !== undefined ? values[i] : ""),
"",
);
return makeDisplayable({ [mediatype]: payload });
};
}


function isJpg(obj: any) {
// Check if obj is a Uint8Array
if (!(obj instanceof Uint8Array)) {
return false;
}

// JPG files start with the magic bytes FF D8
if (obj.length < 2 || obj[0] !== 0xFF || obj[1] !== 0xD8) {
return false;
}

// JPG files end with the magic bytes FF D9
if (
obj.length < 2 || obj[obj.length - 2] !== 0xFF ||
obj[obj.length - 1] !== 0xD9
) {
return false;
}

return true;
}

function isPng(obj: any) {
// Check if obj is a Uint8Array
if (!(obj instanceof Uint8Array)) {
return false;
}

// PNG files start with a specific 8-byte signature
const pngSignature = [137, 80, 78, 71, 13, 10, 26, 10];

// Check if the array is at least as long as the signature
if (obj.length < pngSignature.length) {
return false;
}

// Check each byte of the signature
for (let i = 0; i < pngSignature.length; i++) {
if (obj[i] !== pngSignature[i]) {
return false;
}
}

return true;
}



const _mdFunc = createTaggedTemplateDisplayable("text/markdown");
const _svgFunc = createTaggedTemplateDisplayable("image/svg+xml");
const _htmlFunc = createTaggedTemplateDisplayable("text/html");
const _imageFunc = (obj: any) => {
if (typeof obj === "string") {
try {
obj = Deno.readFileSync(obj);
} catch {
// pass
}
}

if (isJpg(obj)) {
return makeDisplayable({ "image/jpeg": encodeBase64(obj) });
}

if (isPng(obj)) {
return makeDisplayable({ "image/png": encodeBase64(obj) });
}

throw new TypeError(
"Object is not a valid image or a path to an image. `Deno.jupyter.image` supports displaying JPG or PNG images.",
);
}

return {
$display: $display as any as (typeof Deno.jupyter.$display),
display: _displayFunc,
md: _mdFunc,
svg: _svgFunc,
html: _htmlFunc,
image: _imageFunc
}

})();
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,20 @@ export class REPLController implements IController {
public static readonly label = "DenoNBTS(repl)";
public static readonly id = "deno-nbts-kernel-repl";
public static readonly supportedLanguages = ["typescript"];

private context: vscode.ExtensionContext;
private sessions = new Map<string, REPLSession>();

private onError = (fsPath: string) => this.killSession(fsPath)

constructor() {
constructor(context: vscode.ExtensionContext) {
this.context = context;
setInterval(() => {
const closed: [string, REPLSession][] = [...this.sessions.entries()].filter(([_fsPath, session]) => session.isDocumentClosed());
closed.forEach(([fsPath, session]) => {
session.tryClose();
this.sessions.delete(fsPath);
});
}, 1000);
}, 5000);
}

public get output() {
Expand All @@ -40,7 +41,7 @@ export class REPLController implements IController {
): Promise<void> {
let session = this.sessions.get(doc.uri.fsPath);
if (!session) {
session = new REPLSession(() => this.onError(doc.uri.fsPath), doc, this.output);
session = new REPLSession(this.context, () => this.onError(doc.uri.fsPath), doc, this.output);
this.sessions.set(doc.uri.fsPath, session);
await session.start();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,14 @@ import { UUID } from "@lumino/coreutils";


export class REPLSession implements ISession {
private context: vscode.ExtensionContext;
private currentDocument: vscode.NotebookDocument;
private outputChannel: vscode.OutputChannel;
private stopped: boolean = false;
private started: boolean = false;
private proc: ChildProcess;
private currentExecution?: vscode.NotebookCellExecution;

private static displayScript = 'display: async function (obj,options) { console.log(`##DISPLAYDATA#2d522e5a-4a6c-4aae-b20c-91c5189948d9##${JSON.stringify(obj[Symbol.for("Jupyter.display")]? (await(obj[Symbol.for("Jupyter.display")])()):obj)}`)}';
private static bootScript = `Deno.jupyter = { ${REPLSession.displayScript} };`;

private static lineIsError(line: string): boolean {
return line.startsWith('Uncaught Error: ')
|| line.startsWith('Uncaught TypeError: ')
Expand Down Expand Up @@ -98,17 +96,19 @@ export class REPLSession implements ISession {
}
if (lines.length > 0) {
for (const line of lines) {
const index = line.indexOf('##DISPLAYDATA#2d522e5a-4a6c-4aae-b20c-91c5189948d9##');
if (index === 0) {
const display_data: Record<string, string> = JSON.parse(line.substring(52));
this.currentExecution!.appendOutput([REPLSession.processOutput(display_data)]);
}
else {
this.currentExecution!.appendOutput([
new vscode.NotebookCellOutput([
vscode.NotebookCellOutputItem.stdout(line)
])
]);
if (line.length > 0) {
const index = line.indexOf('##DISPLAYDATA#2d522e5a-4a6c-4aae-b20c-91c5189948d9##');
if (index === 0) {
const display_data: Record<string, string> = JSON.parse(line.substring(52));
this.currentExecution!.appendOutput([REPLSession.processOutput(display_data)]);
}
else {
this.currentExecution!.appendOutput([
new vscode.NotebookCellOutput([
vscode.NotebookCellOutputItem.stdout(line)
])
]);
}
}
}
}
Expand All @@ -126,20 +126,21 @@ export class REPLSession implements ISession {
return result;
}

constructor(onError: () => void, doc: vscode.NotebookDocument, outputChannel: vscode.OutputChannel) {
constructor(context: vscode.ExtensionContext, onError: () => void, doc: vscode.NotebookDocument, outputChannel: vscode.OutputChannel) {
this.context = context;
this.currentDocument = doc;
this.outputChannel = outputChannel;
const cwd = doc.uri.fsPath.split(path.sep).slice(0, -1).join(path.sep) + path.sep;
this.proc = DenoTool.syncLaunch(['repl', '--allow-all'], cwd)!;
const bootScriptPath = path.resolve(this.context.extensionPath, 'client', 'src', 'notebook-controllers', 'repl-controller', 'boot', 'boot.ts');
this.proc = DenoTool.syncLaunch(['repl', `--eval-file=${bootScriptPath}`, '--allow-all'], cwd)!;
this.proc!.on("exit", () => {
if (!this.stopped) onError();
this.outputChannel.appendLine('\n### DENO EXITED');
});
outputChannel.appendLine(JSON.stringify(this.proc));
}

public async start() {
const { lines, errors } = await this.runCode(REPLSession.bootScript);
const { lines, errors } = await this.runCode("'Welcome to Deno repl kernel'");
console.log(lines);
console.log(errors);
this.started = true;
Expand Down
3 changes: 2 additions & 1 deletion client/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@
"src"
],
"exclude": [
"src/notebook-controllers/repl-controller/boot",
"node_modules",
".vscode-test",
"resources",
"build"
]
}
}

4 comments on commit 821bdd2

@janckerchen
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what this mean?

@redking00
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wrote an experimental controller based on the deno repl instead of the deno jupyter kernel, to play a bit with it.
I've also slightly rewritten the main controller and fixed the reference to zeromq in the build process.

@janckerchen
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If successful, we can even do away with Deno.jupyter.display? nice~

@redking00
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about that. The REPL does not automatically evaluate the [Deno.jupyter.$display] method on objects like the kernel does.

Please sign in to comment.