mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 17:45:22 +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:
@@ -2,6 +2,11 @@
|
|||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Added basic intellisense to the code editor.
|
||||||
|
|
||||||
## [2.0.0-beta.1] - 2022-06-03
|
## [2.0.0-beta.1] - 2022-06-03
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ module.exports = babelJest.createTransformer({
|
|||||||
['@babel/plugin-transform-typescript', {
|
['@babel/plugin-transform-typescript', {
|
||||||
allowDeclareFields: true
|
allowDeclareFields: true
|
||||||
}],
|
}],
|
||||||
'@shopify/react-i18n/babel'],
|
'@shopify/react-i18n/babel',
|
||||||
|
"babel-plugin-transform-import-meta",
|
||||||
|
],
|
||||||
presets: [
|
presets: [
|
||||||
[
|
[
|
||||||
require.resolve('babel-preset-react-app'),
|
require.resolve('babel-preset-react-app'),
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ module.exports = function (proxy, allowedHost) {
|
|||||||
'Access-Control-Allow-Origin': '*',
|
'Access-Control-Allow-Origin': '*',
|
||||||
'Access-Control-Allow-Methods': '*',
|
'Access-Control-Allow-Methods': '*',
|
||||||
'Access-Control-Allow-Headers': '*',
|
'Access-Control-Allow-Headers': '*',
|
||||||
|
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||||
|
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||||
},
|
},
|
||||||
// Enable gzip compression of generated files.
|
// Enable gzip compression of generated files.
|
||||||
compress: true,
|
compress: true,
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
"babel-loader": "^8.2.3",
|
"babel-loader": "^8.2.3",
|
||||||
"babel-plugin-macros": "^3.0.1",
|
"babel-plugin-macros": "^3.0.1",
|
||||||
"babel-plugin-named-asset-import": "^0.3.8",
|
"babel-plugin-named-asset-import": "^0.3.8",
|
||||||
|
"babel-plugin-transform-import-meta": "^2.2.0",
|
||||||
"babel-preset-react-app": "^10.0.1",
|
"babel-preset-react-app": "^10.0.1",
|
||||||
"bfj": "^7.0.2",
|
"bfj": "^7.0.2",
|
||||||
"browser-fs-access": "^0.30.1",
|
"browser-fs-access": "^0.30.1",
|
||||||
@@ -78,6 +79,7 @@
|
|||||||
"postcss-preset-env": "^7.7.2",
|
"postcss-preset-env": "^7.7.2",
|
||||||
"prompts": "^2.4.2",
|
"prompts": "^2.4.2",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
|
"pyodide": "0.20.1-alpha.2",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-app-polyfill": "^3.0.0",
|
"react-app-polyfill": "^3.0.0",
|
||||||
"react-aria": "^3.17.0",
|
"react-aria": "^3.17.0",
|
||||||
|
|||||||
@@ -3,6 +3,11 @@
|
|||||||
|
|
||||||
import { createAction } from '../actions';
|
import { createAction } from '../actions';
|
||||||
import { UUID } from '../fileStorage';
|
import { UUID } from '../fileStorage';
|
||||||
|
export {
|
||||||
|
didFailToInit as editorCompletionDidFailToInit,
|
||||||
|
didInit as editorCompletionDidInit,
|
||||||
|
init as editorCompletionInit,
|
||||||
|
} from './redux/codeCompletion';
|
||||||
|
|
||||||
/** Action that indicates that a code editor was created. */
|
/** Action that indicates that a code editor was created. */
|
||||||
export const editorDidCreate = createAction(() => ({
|
export const editorDidCreate = createAction(() => ({
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
editorDidCreate,
|
editorDidCreate,
|
||||||
editorDidOpenFile,
|
editorDidOpenFile,
|
||||||
} from './actions';
|
} from './actions';
|
||||||
|
import codeCompletion from './redux/codeCompletion';
|
||||||
|
|
||||||
/** Indicates that the code editor is ready for use. */
|
/** Indicates that the code editor is ready for use. */
|
||||||
const isReady: Reducer<boolean> = (state = false, action) => {
|
const isReady: Reducer<boolean> = (state = false, action) => {
|
||||||
@@ -46,6 +47,7 @@ const openFileUuids: Reducer<readonly UUID[]> = (state = [], action) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
|
codeCompletion,
|
||||||
isReady,
|
isReady,
|
||||||
activeFileUuid,
|
activeFileUuid,
|
||||||
openFileUuids,
|
openFileUuids,
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2022 The Pybricks Authors
|
||||||
|
|
||||||
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
export enum CompletionEngineStatus {
|
||||||
|
Unknown,
|
||||||
|
Loading,
|
||||||
|
Ready,
|
||||||
|
Failed,
|
||||||
|
}
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
status: CompletionEngineStatus;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialState: State = {
|
||||||
|
status: CompletionEngineStatus.Unknown,
|
||||||
|
};
|
||||||
|
|
||||||
|
const slice = createSlice({
|
||||||
|
name: 'codeCompletion',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
init(state) {
|
||||||
|
state.status = CompletionEngineStatus.Loading;
|
||||||
|
},
|
||||||
|
didInit(state) {
|
||||||
|
state.status = CompletionEngineStatus.Ready;
|
||||||
|
},
|
||||||
|
didFailToInit(state) {
|
||||||
|
state.status = CompletionEngineStatus.Failed;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { init, didInit, didFailToInit } = slice.actions;
|
||||||
|
export default slice.reducer;
|
||||||
@@ -6,6 +6,7 @@ import { EventChannel, buffers, eventChannel } from 'redux-saga';
|
|||||||
import {
|
import {
|
||||||
SagaGenerator,
|
SagaGenerator,
|
||||||
call,
|
call,
|
||||||
|
cancelled,
|
||||||
delay,
|
delay,
|
||||||
fork,
|
fork,
|
||||||
getContext,
|
getContext,
|
||||||
@@ -26,11 +27,27 @@ import {
|
|||||||
fileStorageStoreTextFileValue,
|
fileStorageStoreTextFileValue,
|
||||||
fileStorageStoreTextFileViewState,
|
fileStorageStoreTextFileViewState,
|
||||||
} from '../fileStorage/actions';
|
} from '../fileStorage/actions';
|
||||||
|
import {
|
||||||
|
pythonMessageComplete,
|
||||||
|
pythonMessageDidComplete,
|
||||||
|
pythonMessageDidFailToComplete,
|
||||||
|
pythonMessageDidFailToGetSignature,
|
||||||
|
pythonMessageDidFailToInit,
|
||||||
|
pythonMessageDidGetSignature,
|
||||||
|
pythonMessageDidInit,
|
||||||
|
pythonMessageGetSignature,
|
||||||
|
pythonMessageInit,
|
||||||
|
pythonMessageSetInterruptBuffer,
|
||||||
|
} from '../pybricksMicropython/python-message';
|
||||||
import { RootState } from '../reducers';
|
import { RootState } from '../reducers';
|
||||||
import { acquireLock, defined, ensureError } from '../utils';
|
import { acquireLock, defined, ensureError } from '../utils';
|
||||||
|
import { createCountFunc } from '../utils/iter';
|
||||||
import {
|
import {
|
||||||
editorActivateFile,
|
editorActivateFile,
|
||||||
editorCloseFile,
|
editorCloseFile,
|
||||||
|
editorCompletionDidFailToInit,
|
||||||
|
editorCompletionDidInit,
|
||||||
|
editorCompletionInit,
|
||||||
editorDidActivateFile,
|
editorDidActivateFile,
|
||||||
editorDidCloseFile,
|
editorDidCloseFile,
|
||||||
editorDidCreate,
|
editorDidCreate,
|
||||||
@@ -373,6 +390,278 @@ function* monitorEditors(): Generator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs a web worker with Pyodide so that we can use Jedi for intellisense.
|
||||||
|
*/
|
||||||
|
function* runJedi(): Generator {
|
||||||
|
const defer = new Array<() => void>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.debug('creating code completion worker');
|
||||||
|
|
||||||
|
// start the web worker and set up communication channels
|
||||||
|
|
||||||
|
const worker = new Worker(
|
||||||
|
new URL('../pybricksMicropython/python-worker.ts', import.meta.url),
|
||||||
|
);
|
||||||
|
|
||||||
|
defer.push(() => worker.terminate());
|
||||||
|
|
||||||
|
const messageChannel = eventChannel<MessageEvent>((emit) => {
|
||||||
|
worker.addEventListener('message', emit);
|
||||||
|
|
||||||
|
return () => worker.removeEventListener('message', emit);
|
||||||
|
}, buffers.expanding());
|
||||||
|
|
||||||
|
defer.push(() => messageChannel.close());
|
||||||
|
|
||||||
|
const errorChannel = eventChannel<ErrorEvent>((emit) => {
|
||||||
|
worker.addEventListener('error', emit);
|
||||||
|
|
||||||
|
return () => worker.removeEventListener('error', emit);
|
||||||
|
}, buffers.expanding());
|
||||||
|
|
||||||
|
defer.push(() => errorChannel.close());
|
||||||
|
|
||||||
|
// wait for the Python runtime to start and get in a ready state
|
||||||
|
|
||||||
|
worker.postMessage(pythonMessageInit());
|
||||||
|
yield* put(editorCompletionInit());
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
const { messageEvent, errorEvent } = yield* race({
|
||||||
|
messageEvent: take(messageChannel),
|
||||||
|
errorEvent: take(errorChannel),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (errorEvent) {
|
||||||
|
yield* put(editorCompletionDidFailToInit());
|
||||||
|
throw errorEvent.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
defined(messageEvent);
|
||||||
|
|
||||||
|
if (pythonMessageDidFailToInit.matches(messageEvent.data)) {
|
||||||
|
yield* put(editorCompletionDidFailToInit());
|
||||||
|
throw messageEvent.data.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pythonMessageDidInit.matches(messageEvent.data)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug('code completion engine is ready');
|
||||||
|
yield* put(editorCompletionDidInit());
|
||||||
|
|
||||||
|
// configure interrupts
|
||||||
|
// https://pyodide.org/en/stable/usage/keyboard-interrupts.html
|
||||||
|
|
||||||
|
// HACK: Using WebAssembly.Memory instead of SharedArrayBuffer to avoid exception.
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer/Planned_changes#api_changes
|
||||||
|
const interrupt = new Uint8Array(
|
||||||
|
new WebAssembly.Memory({ initial: 1, maximum: 1, shared: true }).buffer,
|
||||||
|
);
|
||||||
|
|
||||||
|
const setInterrupt = () => {
|
||||||
|
interrupt[0] = 2; //2 === SIGINT
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearInterrupt = () => {
|
||||||
|
interrupt[0] = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements
|
||||||
|
if (crossOriginIsolated) {
|
||||||
|
worker.postMessage(pythonMessageSetInterruptBuffer(interrupt));
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
'required headers missing for SharedArrayBuffer, cancellation will not work',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// register intellisense hooks with editor
|
||||||
|
|
||||||
|
const nextId = createCountFunc();
|
||||||
|
|
||||||
|
const completionItemProviderChan = eventChannel<{
|
||||||
|
model: monaco.editor.ITextModel;
|
||||||
|
position: monaco.Position;
|
||||||
|
context: monaco.languages.CompletionContext;
|
||||||
|
token: monaco.CancellationToken;
|
||||||
|
resolve: (value: monaco.languages.CompletionList | null) => void;
|
||||||
|
}>((emit) => {
|
||||||
|
const subscription = monaco.languages.registerCompletionItemProvider(
|
||||||
|
pybricksMicroPythonId,
|
||||||
|
{
|
||||||
|
triggerCharacters: ['.', ' '],
|
||||||
|
provideCompletionItems(
|
||||||
|
model,
|
||||||
|
position,
|
||||||
|
context,
|
||||||
|
token,
|
||||||
|
): Promise<monaco.languages.CompletionList | null> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
emit({ model, position, context, token, resolve });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => subscription.dispose();
|
||||||
|
}, buffers.expanding());
|
||||||
|
|
||||||
|
defer.push(() => completionItemProviderChan.close());
|
||||||
|
|
||||||
|
const signatureProviderChan = eventChannel<{
|
||||||
|
model: monaco.editor.ITextModel;
|
||||||
|
position: monaco.Position;
|
||||||
|
token: monaco.CancellationToken;
|
||||||
|
context: monaco.languages.SignatureHelpContext;
|
||||||
|
resolve: (value: monaco.languages.SignatureHelpResult | null) => void;
|
||||||
|
}>((emit) => {
|
||||||
|
const subscription = monaco.languages.registerSignatureHelpProvider(
|
||||||
|
pybricksMicroPythonId,
|
||||||
|
{
|
||||||
|
signatureHelpTriggerCharacters: ['('],
|
||||||
|
signatureHelpRetriggerCharacters: [','],
|
||||||
|
provideSignatureHelp(model, position, token, context) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
emit({ model, position, token, context, resolve });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => subscription.dispose();
|
||||||
|
}, buffers.expanding());
|
||||||
|
|
||||||
|
defer.push(() => signatureProviderChan.close());
|
||||||
|
|
||||||
|
// Serialize requests from editor. Due to the way cancellation works, we
|
||||||
|
// can only have one pending message from the web worker at a time.
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
const { complete, getSignature } = yield* race({
|
||||||
|
complete: take(completionItemProviderChan),
|
||||||
|
getSignature: take(signatureProviderChan),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (complete) {
|
||||||
|
// for debugging
|
||||||
|
const id = nextId();
|
||||||
|
|
||||||
|
console.debug(`${id}: requested completion item`);
|
||||||
|
|
||||||
|
const subscription = complete.token.onCancellationRequested(() => {
|
||||||
|
console.debug(`${id}: requested cancelation`);
|
||||||
|
setInterrupt();
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
clearInterrupt();
|
||||||
|
|
||||||
|
worker.postMessage(
|
||||||
|
pythonMessageComplete(
|
||||||
|
complete.model.getValue(),
|
||||||
|
complete.position.lineNumber,
|
||||||
|
complete.position.column,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
const msg = yield* take(messageChannel);
|
||||||
|
|
||||||
|
if (pythonMessageDidFailToComplete.matches(msg.data)) {
|
||||||
|
if (
|
||||||
|
msg.data.error instanceof DOMException &&
|
||||||
|
msg.data.error.name === 'AbortError'
|
||||||
|
) {
|
||||||
|
console.log(`${id} canceled`);
|
||||||
|
} else {
|
||||||
|
console.error(msg.data.error);
|
||||||
|
}
|
||||||
|
complete.resolve(null);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pythonMessageDidComplete.matches(msg.data)) {
|
||||||
|
const list = JSON.parse(msg.data.completionListJson);
|
||||||
|
console.debug(list);
|
||||||
|
complete.resolve({ suggestions: list });
|
||||||
|
console.debug(`${id}: resolved: ${msg.data.type}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
subscription.dispose();
|
||||||
|
}
|
||||||
|
} else if (getSignature) {
|
||||||
|
// for debugging
|
||||||
|
const id = nextId();
|
||||||
|
|
||||||
|
console.debug(`${id}: requested signatures`);
|
||||||
|
|
||||||
|
const subscription = getSignature.token.onCancellationRequested(() => {
|
||||||
|
console.debug(`${id}: requested cancelation`);
|
||||||
|
setInterrupt();
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
clearInterrupt();
|
||||||
|
|
||||||
|
worker.postMessage(
|
||||||
|
pythonMessageGetSignature(
|
||||||
|
getSignature.model.getValue(),
|
||||||
|
getSignature.position.lineNumber,
|
||||||
|
getSignature.position.column,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
const msg = yield* take(messageChannel);
|
||||||
|
|
||||||
|
if (pythonMessageDidFailToGetSignature.matches(msg.data)) {
|
||||||
|
if (
|
||||||
|
msg.data.error instanceof DOMException &&
|
||||||
|
msg.data.error.name === 'AbortError'
|
||||||
|
) {
|
||||||
|
console.log(`${id} canceled`);
|
||||||
|
} else {
|
||||||
|
console.error(msg.data.error);
|
||||||
|
}
|
||||||
|
getSignature.resolve(null);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pythonMessageDidGetSignature.matches(msg.data)) {
|
||||||
|
const signatures = JSON.parse(msg.data.signatureHelpJson);
|
||||||
|
console.debug(signatures);
|
||||||
|
getSignature.resolve({
|
||||||
|
value: signatures,
|
||||||
|
dispose: () => undefined,
|
||||||
|
});
|
||||||
|
console.debug(`${id}: resolved: ${msg.data.type}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
subscription.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const isCancelled = yield* cancelled();
|
||||||
|
|
||||||
|
if (!isCancelled) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
defer.forEach((item) => item());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function* (): Generator {
|
export default function* (): Generator {
|
||||||
yield* fork(monitorEditors);
|
yield* fork(monitorEditors);
|
||||||
|
yield* fork(runJedi);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,10 +1,18 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright (c) 2020-2022 The Pybricks Authors
|
// Copyright (c) 2020-2022 The Pybricks Authors
|
||||||
|
|
||||||
import { Button, Intent, ProgressBar } from '@blueprintjs/core';
|
import {
|
||||||
|
Button,
|
||||||
|
Icon,
|
||||||
|
IconSize,
|
||||||
|
Intent,
|
||||||
|
ProgressBar,
|
||||||
|
Spinner,
|
||||||
|
} from '@blueprintjs/core';
|
||||||
import { Classes as Classes2, Popover2, Popover2Props } from '@blueprintjs/popover2';
|
import { Classes as Classes2, Popover2, Popover2Props } from '@blueprintjs/popover2';
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { BleConnectionState } from '../ble/reducers';
|
import { BleConnectionState } from '../ble/reducers';
|
||||||
|
import { CompletionEngineStatus } from '../editor/redux/codeCompletion';
|
||||||
import { useSelector } from '../reducers';
|
import { useSelector } from '../reducers';
|
||||||
import { I18nId, useI18n } from './i18n';
|
import { I18nId, useI18n } from './i18n';
|
||||||
|
|
||||||
@@ -15,6 +23,48 @@ const commonPopoverProps: Partial<Popover2Props> = {
|
|||||||
placement: 'top',
|
placement: 'top',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CompletionEngineIndicator: React.VoidFunctionComponent = () => {
|
||||||
|
const { status } = useSelector((s) => s.editor.codeCompletion);
|
||||||
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
const icon = useMemo(() => {
|
||||||
|
switch (status) {
|
||||||
|
case CompletionEngineStatus.Loading:
|
||||||
|
return <Spinner size={IconSize.STANDARD} />;
|
||||||
|
case CompletionEngineStatus.Ready:
|
||||||
|
return <Icon icon="tick-circle" />;
|
||||||
|
case CompletionEngineStatus.Failed:
|
||||||
|
return <Icon icon="error" />;
|
||||||
|
default:
|
||||||
|
return <Icon icon="disable" />;
|
||||||
|
}
|
||||||
|
}, [status]);
|
||||||
|
|
||||||
|
const message = useMemo(() => {
|
||||||
|
switch (status) {
|
||||||
|
case CompletionEngineStatus.Loading:
|
||||||
|
return i18n.translate(I18nId.CompletionEngineStatusMessageLoading);
|
||||||
|
case CompletionEngineStatus.Ready:
|
||||||
|
return i18n.translate(I18nId.CompletionEngineStatusMessageReady);
|
||||||
|
case CompletionEngineStatus.Failed:
|
||||||
|
return i18n.translate(I18nId.CompletionEngineStatusMessageFailed);
|
||||||
|
default:
|
||||||
|
return i18n.translate(I18nId.CompletionEngineStatusMessageUnknown);
|
||||||
|
}
|
||||||
|
}, [status, i18n]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover2 {...commonPopoverProps} content={message}>
|
||||||
|
<div
|
||||||
|
aria-label={i18n.translate(I18nId.CompletionEngineStatusLabel)}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
</Popover2>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const HubInfoButton: React.VoidFunctionComponent = () => {
|
const HubInfoButton: React.VoidFunctionComponent = () => {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const deviceName = useSelector((s) => s.ble.deviceName);
|
const deviceName = useSelector((s) => s.ble.deviceName);
|
||||||
@@ -99,6 +149,10 @@ const StatusBar: React.VFC = (_props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pb-status-bar" role="status" aria-live="off">
|
<div className="pb-status-bar" role="status" aria-live="off">
|
||||||
|
<div className="pb-status-bar-group">
|
||||||
|
<CompletionEngineIndicator />
|
||||||
|
</div>
|
||||||
|
<div className="pb-status-bar-group">
|
||||||
{connection === BleConnectionState.Connected && (
|
{connection === BleConnectionState.Connected && (
|
||||||
<>
|
<>
|
||||||
<HubInfoButton />
|
<HubInfoButton />
|
||||||
@@ -106,6 +160,7 @@ const StatusBar: React.VFC = (_props) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ export function useI18n(): I18n {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum I18nId {
|
export enum I18nId {
|
||||||
|
CompletionEngineStatusLabel = 'completionEngineStatus.label',
|
||||||
|
CompletionEngineStatusMessageLoading = 'completionEngineStatus.message.loading',
|
||||||
|
CompletionEngineStatusMessageReady = 'completionEngineStatus.message.ready',
|
||||||
|
CompletionEngineStatusMessageFailed = 'completionEngineStatus.message.failed',
|
||||||
|
CompletionEngineStatusMessageUnknown = 'completionEngineStatus.message.unknown',
|
||||||
BatteryTitle = 'battery.title',
|
BatteryTitle = 'battery.title',
|
||||||
BatteryLow = 'battery.low',
|
BatteryLow = 'battery.low',
|
||||||
BatteryOk = 'battery.ok',
|
BatteryOk = 'battery.ok',
|
||||||
|
|||||||
@@ -15,11 +15,12 @@
|
|||||||
padding-inline: 5px;
|
padding-inline: 5px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
justify-content: space-between;
|
||||||
|
|
||||||
// make status bar items right-aligned
|
&-group {
|
||||||
.pb-status-bar :first-child {
|
display: flex;
|
||||||
margin-left: auto;
|
align-items: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pb-battery-indicator {
|
.pb-battery-indicator {
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
{
|
{
|
||||||
|
"completionEngineStatus": {
|
||||||
|
"label": "Code completion engine status indicator",
|
||||||
|
"message": {
|
||||||
|
"loading": "Code completion engine is starting up and will be ready soon.",
|
||||||
|
"ready": "Code completion engine is ready to use.",
|
||||||
|
"failed": "Code completion engine has stopped working.",
|
||||||
|
"unknown": "Code completion engine has not started yet."
|
||||||
|
}
|
||||||
|
},
|
||||||
"hubInfo": {
|
"hubInfo": {
|
||||||
"title": "Connected hub",
|
"title": "Connected hub",
|
||||||
"connectedTo": "Connected to:",
|
"connectedTo": "Connected to:",
|
||||||
|
|||||||
@@ -1451,7 +1451,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@babel/template@npm:^7.0.0, @babel/template@npm:^7.16.7, @babel/template@npm:^7.3.3":
|
"@babel/template@npm:^7.0.0, @babel/template@npm:^7.16.7, @babel/template@npm:^7.3.3, @babel/template@npm:^7.4.4":
|
||||||
version: 7.16.7
|
version: 7.16.7
|
||||||
resolution: "@babel/template@npm:7.16.7"
|
resolution: "@babel/template@npm:7.16.7"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -2319,6 +2319,7 @@ __metadata:
|
|||||||
babel-loader: ^8.2.3
|
babel-loader: ^8.2.3
|
||||||
babel-plugin-macros: ^3.0.1
|
babel-plugin-macros: ^3.0.1
|
||||||
babel-plugin-named-asset-import: ^0.3.8
|
babel-plugin-named-asset-import: ^0.3.8
|
||||||
|
babel-plugin-transform-import-meta: ^2.2.0
|
||||||
babel-preset-react-app: ^10.0.1
|
babel-preset-react-app: ^10.0.1
|
||||||
bfj: ^7.0.2
|
bfj: ^7.0.2
|
||||||
browser-fs-access: ^0.30.1
|
browser-fs-access: ^0.30.1
|
||||||
@@ -2370,6 +2371,7 @@ __metadata:
|
|||||||
prettier: ^2.7.1
|
prettier: ^2.7.1
|
||||||
prompts: ^2.4.2
|
prompts: ^2.4.2
|
||||||
prop-types: ^15.8.1
|
prop-types: ^15.8.1
|
||||||
|
pyodide: 0.20.1-alpha.2
|
||||||
react: ^16.13.1
|
react: ^16.13.1
|
||||||
react-app-polyfill: ^3.0.0
|
react-app-polyfill: ^3.0.0
|
||||||
react-aria: ^3.17.0
|
react-aria: ^3.17.0
|
||||||
@@ -5662,6 +5664,18 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"babel-plugin-transform-import-meta@npm:^2.2.0":
|
||||||
|
version: 2.2.0
|
||||||
|
resolution: "babel-plugin-transform-import-meta@npm:2.2.0"
|
||||||
|
dependencies:
|
||||||
|
"@babel/template": ^7.4.4
|
||||||
|
tslib: ^2.4.0
|
||||||
|
peerDependencies:
|
||||||
|
"@babel/core": ^7.10.0
|
||||||
|
checksum: 305b0750f957e4df3ce08e0b24359dc55fb789c96477e944d113d259a8292e7bb8d665944e04b5a690c82a8dc23f54b711a85917d80988dffe1e046e1db940ee
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"babel-plugin-transform-react-remove-prop-types@npm:^0.4.24":
|
"babel-plugin-transform-react-remove-prop-types@npm:^0.4.24":
|
||||||
version: 0.4.24
|
version: 0.4.24
|
||||||
resolution: "babel-plugin-transform-react-remove-prop-types@npm:0.4.24"
|
resolution: "babel-plugin-transform-react-remove-prop-types@npm:0.4.24"
|
||||||
@@ -5734,6 +5748,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"base-64@npm:^1.0.0":
|
||||||
|
version: 1.0.0
|
||||||
|
resolution: "base-64@npm:1.0.0"
|
||||||
|
checksum: d10b64a1fc9b2c5a5f39f1ce1e6c9d1c5b249222bbfa3a0604c592d90623caf74419983feadd8a170f27dc0c3389704f72faafa3e645aeb56bfc030c93ff074a
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"base64-arraybuffer-es6@npm:^0.7.0":
|
"base64-arraybuffer-es6@npm:^0.7.0":
|
||||||
version: 0.7.0
|
version: 0.7.0
|
||||||
resolution: "base64-arraybuffer-es6@npm:0.7.0"
|
resolution: "base64-arraybuffer-es6@npm:0.7.0"
|
||||||
@@ -10785,7 +10806,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"node-fetch@npm:^2.6.5":
|
"node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.5":
|
||||||
version: 2.6.7
|
version: 2.6.7
|
||||||
resolution: "node-fetch@npm:2.6.7"
|
resolution: "node-fetch@npm:2.6.7"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -12363,6 +12384,17 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"pyodide@npm:0.20.1-alpha.2":
|
||||||
|
version: 0.20.1-alpha.2
|
||||||
|
resolution: "pyodide@npm:0.20.1-alpha.2"
|
||||||
|
dependencies:
|
||||||
|
base-64: ^1.0.0
|
||||||
|
node-fetch: ^2.6.1
|
||||||
|
ws: ^8.5.0
|
||||||
|
checksum: aeadfd5822660b435561c7f010f1a029b28bfa6080a781a7fbb2fe95b02ca2ea95f234d3c79a5f750c71a6eb3b10872731814805b82be5cbb1a2da2a6386e65f
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"qs@npm:6.10.3":
|
"qs@npm:6.10.3":
|
||||||
version: 6.10.3
|
version: 6.10.3
|
||||||
resolution: "qs@npm:6.10.3"
|
resolution: "qs@npm:6.10.3"
|
||||||
@@ -15345,6 +15377,21 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"ws@npm:^8.5.0":
|
||||||
|
version: 8.8.0
|
||||||
|
resolution: "ws@npm:8.8.0"
|
||||||
|
peerDependencies:
|
||||||
|
bufferutil: ^4.0.1
|
||||||
|
utf-8-validate: ^5.0.2
|
||||||
|
peerDependenciesMeta:
|
||||||
|
bufferutil:
|
||||||
|
optional: true
|
||||||
|
utf-8-validate:
|
||||||
|
optional: true
|
||||||
|
checksum: 6ceed1ca1cb800ef60c7fc8346c7d5d73d73be754228eb958765abf5d714519338efa20ffe674167039486eb3a813aae5a497f8d319e16b4d96216a31df5bd95
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"xml-name-validator@npm:^4.0.0":
|
"xml-name-validator@npm:^4.0.0":
|
||||||
version: 4.0.0
|
version: 4.0.0
|
||||||
resolution: "xml-name-validator@npm:4.0.0"
|
resolution: "xml-name-validator@npm:4.0.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user