mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-14 10:35:11 +00:00
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:
@@ -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,
|
||||
}));
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user