editor: add basic intellisense

This adds basic intellisense for code completion and function signatures
using the Python `jedi` package running in a Pyodide environment.
This commit is contained in:
David Lechner
2022-06-24 19:15:32 -05:00
parent 9f0e5db307
commit 096eea7077
15 changed files with 693 additions and 15 deletions
+101
View File
@@ -0,0 +1,101 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
import { createAction } from '../actions';
// NB: although we are using the same action creator as we do for redux, these
// actions are not used by redux but rather are to sent between workers.
/**
* Message sent from main to work to request initialization of Pyodide.
*/
export const pythonMessageInit = createAction(() => ({
type: 'python.message.init',
}));
/**
* Message sent from worker to main that indicates {@link pythonMessageInit}
* succeeded.
*/
export const pythonMessageDidInit = createAction(() => ({
type: 'python.message.didInit',
}));
/**
* Message sent from worker to main that indicates {@link pythonMessageInit}
* failed.
*/
export const pythonMessageDidFailToInit = createAction((error: Error) => ({
type: 'python.message.didFailToInit',
error,
}));
/**
* Message sent from main to worker to set the shared interrupt buffer.
*/
export const pythonMessageSetInterruptBuffer = createAction((buffer: Uint8Array) => ({
type: 'python.message.setInterruptBuffer',
buffer,
}));
/**
* Message sent from main to worker to request code completion.
*/
export const pythonMessageComplete = createAction(
(code: string, lineNumber: number, column: number) => ({
type: 'python.message.complete',
code,
lineNumber,
column,
}),
);
/**
* Message sent from worker to main that indicates {@link pythonMessageComplete}
* succeeded.
*/
export const pythonMessageDidComplete = createAction((completionListJson: string) => ({
type: 'python.message.didComplete',
completionListJson,
}));
/**
* Message sent from worker to main that indicates {@link pythonMessageComplete}
* failed.
*/
export const pythonMessageDidFailToComplete = createAction((error: Error) => ({
type: 'python.message.didFailToComplete',
error,
}));
/**
* Message sent from main to worker to request function signature.
*/
export const pythonMessageGetSignature = createAction(
(code: string, lineNumber: number, column: number) => ({
type: 'python.message.getSignature',
code,
lineNumber,
column,
}),
);
/**
* Message sent from worker to main that indicates {@link pythonMessageGetSignature}
* succeeded.
*/
export const pythonMessageDidGetSignature = createAction(
(signatureHelpJson: string) => ({
type: 'python.message.didGetSignature',
signatureHelpJson,
}),
);
/**
* Message sent from worker to main that indicates {@link pythonMessageGetSignature}
* failed.
*/
export const pythonMessageDidFailToGetSignature = createAction((error: Error) => ({
type: 'python.message.didFailToGetSignature',
error,
}));
+115
View File
@@ -0,0 +1,115 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
// This file runs as a web worker.
// NB: We need to be very careful about imports here since many libraries for
// web aren't compatible with web workers!
import type { loadPyodide as loadPyodideFunc } from 'pyodide';
import { ensureError } from '../utils';
import {
pythonMessageComplete,
pythonMessageDidComplete,
pythonMessageDidFailToComplete,
pythonMessageDidFailToGetSignature,
pythonMessageDidFailToInit,
pythonMessageDidGetSignature,
pythonMessageDidInit,
pythonMessageGetSignature,
pythonMessageInit,
pythonMessageSetInterruptBuffer,
} from './python-message';
importScripts('https://cdn.jsdelivr.net/pyodide/v0.20.0/full/pyodide.js');
declare const loadPyodide: typeof loadPyodideFunc;
/**
* Wrapper around {@link ensureError} that also converts KeyboardInterrupt to
* AbortError.
* @param err The value from the catch clause.
* @returns The fixed up error.
*/
function fixUpError(err: unknown): Error {
const error = ensureError(err);
if (
error.constructor.name === 'PythonError' &&
error.message.match(/KeyboardInterrupt/)
) {
return new DOMException('cancelled', 'AbortError');
}
return error;
}
const setUpPythonEnvironment = `
import jedi
import micropip
print('loading pybricks...')
await micropip.install('pybricks-jedi')
print('loaded pybricks.')
import pybricks_jedi
print('preloading...')
pybricks_jedi.initialize()
print('preloading done.')
`;
async function init(): Promise<void> {
console.log('starting Pyodide...');
const pyodide = await loadPyodide();
await pyodide.loadPackage(['micropip', 'jedi']);
await pyodide.runPythonAsync(setUpPythonEnvironment);
const complete = pyodide.runPython('pybricks_jedi.complete');
const getSignatures = pyodide.runPython('pybricks_jedi.get_signatures');
self.addEventListener('message', async (e) => {
if (pythonMessageSetInterruptBuffer.matches(e.data)) {
pyodide.setInterruptBuffer(e.data.buffer);
return;
}
if (pythonMessageComplete.matches(e.data)) {
console.debug('worker received complete message');
try {
const { code, lineNumber, column } = e.data;
const list = complete(code, lineNumber, column);
self.postMessage(pythonMessageDidComplete(list));
} catch (err) {
self.postMessage(pythonMessageDidFailToComplete(fixUpError(err)));
}
return;
}
if (pythonMessageGetSignature.matches(e.data)) {
console.debug('worker received getSignatures message');
try {
const { code, lineNumber, column } = e.data;
const list = getSignatures(code, lineNumber, column);
self.postMessage(pythonMessageDidGetSignature(list));
} catch (err) {
self.postMessage(pythonMessageDidFailToGetSignature(fixUpError(err)));
}
return;
}
});
console.log('Pyodide is ready.');
}
self.addEventListener('message', async (e) => {
if (pythonMessageInit.matches(e.data)) {
try {
await init();
postMessage(pythonMessageDidInit());
} catch (err) {
postMessage(pythonMessageDidFailToInit(ensureError(err)));
}
}
});