mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 17:45:22 +00:00
+7
-1
@@ -2,6 +2,11 @@
|
||||
|
||||
# Changelog
|
||||
|
||||
## [2.0.0-beta.2]
|
||||
|
||||
### Added
|
||||
- Added basic intellisense to the code editor.
|
||||
|
||||
## [2.0.0-beta.1] - 2022-06-03
|
||||
|
||||
### Added
|
||||
@@ -285,7 +290,8 @@ Prerelease changes are documented at [support#48].
|
||||
|
||||
<!-- links for version headings -->
|
||||
|
||||
[Unreleased]: https://github.com/pybricks/pybricks-code/compare/v2.0.0-beta.1...HEAD
|
||||
[Unreleased]: https://github.com/pybricks/pybricks-code/compare/v2.0.0-beta.2...HEAD
|
||||
[2.0.0-beta.2]: https://github.com/pybricks/pybricks-code/compare/v2.0.0-beta.1...v2.0.0-beta.2
|
||||
[2.0.0-beta.1]: https://github.com/pybricks/pybricks-code/compare/v1.2.0-beta.1...v2.0.0-beta.1
|
||||
[1.2.0-beta.1]: https://github.com/pybricks/pybricks-code/compare/v1.1.0...v1.2.0-beta.1
|
||||
[1.1.0]: https://github.com/pybricks/pybricks-code/compare/v1.1.0-rc.1...v1.1.0
|
||||
|
||||
@@ -20,7 +20,9 @@ module.exports = babelJest.createTransformer({
|
||||
['@babel/plugin-transform-typescript', {
|
||||
allowDeclareFields: true
|
||||
}],
|
||||
'@shopify/react-i18n/babel'],
|
||||
'@shopify/react-i18n/babel',
|
||||
"babel-plugin-transform-import-meta",
|
||||
],
|
||||
presets: [
|
||||
[
|
||||
require.resolve('babel-preset-react-app'),
|
||||
|
||||
@@ -40,6 +40,8 @@ module.exports = function (proxy, allowedHost) {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': '*',
|
||||
'Access-Control-Allow-Headers': '*',
|
||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||
},
|
||||
// Enable gzip compression of generated files.
|
||||
compress: true,
|
||||
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@pybricks/pybricks-code",
|
||||
"version": "2.0.0-beta.1",
|
||||
"version": "2.0.0-beta.2",
|
||||
"license": "MIT",
|
||||
"author": "The Pybricks Authors",
|
||||
"repository": {
|
||||
@@ -38,6 +38,7 @@
|
||||
"babel-loader": "^8.2.3",
|
||||
"babel-plugin-macros": "^3.0.1",
|
||||
"babel-plugin-named-asset-import": "^0.3.8",
|
||||
"babel-plugin-transform-import-meta": "^2.2.0",
|
||||
"babel-preset-react-app": "^10.0.1",
|
||||
"bfj": "^7.0.2",
|
||||
"browser-fs-access": "^0.30.1",
|
||||
@@ -78,6 +79,7 @@
|
||||
"postcss-preset-env": "^7.7.2",
|
||||
"prompts": "^2.4.2",
|
||||
"prop-types": "^15.8.1",
|
||||
"pyodide": "0.20.1-alpha.2",
|
||||
"react": "^16.13.1",
|
||||
"react-app-polyfill": "^3.0.0",
|
||||
"react-aria": "^3.17.0",
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
|
||||
import { createAction } from '../actions';
|
||||
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. */
|
||||
export const editorDidCreate = createAction(() => ({
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
editorDidCreate,
|
||||
editorDidOpenFile,
|
||||
} from './actions';
|
||||
import codeCompletion from './redux/codeCompletion';
|
||||
|
||||
/** Indicates that the code editor is ready for use. */
|
||||
const isReady: Reducer<boolean> = (state = false, action) => {
|
||||
@@ -46,6 +47,7 @@ const openFileUuids: Reducer<readonly UUID[]> = (state = [], action) => {
|
||||
};
|
||||
|
||||
export default combineReducers({
|
||||
codeCompletion,
|
||||
isReady,
|
||||
activeFileUuid,
|
||||
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 {
|
||||
SagaGenerator,
|
||||
call,
|
||||
cancelled,
|
||||
delay,
|
||||
fork,
|
||||
getContext,
|
||||
@@ -26,11 +27,27 @@ import {
|
||||
fileStorageStoreTextFileValue,
|
||||
fileStorageStoreTextFileViewState,
|
||||
} from '../fileStorage/actions';
|
||||
import {
|
||||
pythonMessageComplete,
|
||||
pythonMessageDidComplete,
|
||||
pythonMessageDidFailToComplete,
|
||||
pythonMessageDidFailToGetSignature,
|
||||
pythonMessageDidFailToInit,
|
||||
pythonMessageDidGetSignature,
|
||||
pythonMessageDidInit,
|
||||
pythonMessageGetSignature,
|
||||
pythonMessageInit,
|
||||
pythonMessageSetInterruptBuffer,
|
||||
} from '../pybricksMicropython/python-message';
|
||||
import { RootState } from '../reducers';
|
||||
import { acquireLock, defined, ensureError } from '../utils';
|
||||
import { createCountFunc } from '../utils/iter';
|
||||
import {
|
||||
editorActivateFile,
|
||||
editorCloseFile,
|
||||
editorCompletionDidFailToInit,
|
||||
editorCompletionDidInit,
|
||||
editorCompletionInit,
|
||||
editorDidActivateFile,
|
||||
editorDidCloseFile,
|
||||
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 {
|
||||
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
|
||||
// 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 React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { BleConnectionState } from '../ble/reducers';
|
||||
import { CompletionEngineStatus } from '../editor/redux/codeCompletion';
|
||||
import { useSelector } from '../reducers';
|
||||
import { I18nId, useI18n } from './i18n';
|
||||
|
||||
@@ -15,6 +23,48 @@ const commonPopoverProps: Partial<Popover2Props> = {
|
||||
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 i18n = useI18n();
|
||||
const deviceName = useSelector((s) => s.ble.deviceName);
|
||||
@@ -99,12 +149,17 @@ const StatusBar: React.VFC = (_props) => {
|
||||
|
||||
return (
|
||||
<div className="pb-status-bar" role="status" aria-live="off">
|
||||
{connection === BleConnectionState.Connected && (
|
||||
<>
|
||||
<HubInfoButton />
|
||||
<BatteryIndicator />
|
||||
</>
|
||||
)}
|
||||
<div className="pb-status-bar-group">
|
||||
<CompletionEngineIndicator />
|
||||
</div>
|
||||
<div className="pb-status-bar-group">
|
||||
{connection === BleConnectionState.Connected && (
|
||||
<>
|
||||
<HubInfoButton />
|
||||
<BatteryIndicator />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,11 @@ export function useI18n(): I18n {
|
||||
}
|
||||
|
||||
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',
|
||||
BatteryLow = 'battery.low',
|
||||
BatteryOk = 'battery.ok',
|
||||
|
||||
@@ -15,11 +15,12 @@
|
||||
padding-inline: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
justify-content: space-between;
|
||||
|
||||
// make status bar items right-aligned
|
||||
.pb-status-bar :first-child {
|
||||
margin-left: auto;
|
||||
&-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.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": {
|
||||
"title": "Connected hub",
|
||||
"connectedTo": "Connected to:",
|
||||
|
||||
@@ -1451,7 +1451,7 @@ __metadata:
|
||||
languageName: node
|
||||
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
|
||||
resolution: "@babel/template@npm:7.16.7"
|
||||
dependencies:
|
||||
@@ -2319,6 +2319,7 @@ __metadata:
|
||||
babel-loader: ^8.2.3
|
||||
babel-plugin-macros: ^3.0.1
|
||||
babel-plugin-named-asset-import: ^0.3.8
|
||||
babel-plugin-transform-import-meta: ^2.2.0
|
||||
babel-preset-react-app: ^10.0.1
|
||||
bfj: ^7.0.2
|
||||
browser-fs-access: ^0.30.1
|
||||
@@ -2370,6 +2371,7 @@ __metadata:
|
||||
prettier: ^2.7.1
|
||||
prompts: ^2.4.2
|
||||
prop-types: ^15.8.1
|
||||
pyodide: 0.20.1-alpha.2
|
||||
react: ^16.13.1
|
||||
react-app-polyfill: ^3.0.0
|
||||
react-aria: ^3.17.0
|
||||
@@ -5662,6 +5664,18 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 0.4.24
|
||||
resolution: "babel-plugin-transform-react-remove-prop-types@npm:0.4.24"
|
||||
@@ -5734,6 +5748,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 0.7.0
|
||||
resolution: "base64-arraybuffer-es6@npm:0.7.0"
|
||||
@@ -10785,7 +10806,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-fetch@npm:^2.6.5":
|
||||
"node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.5":
|
||||
version: 2.6.7
|
||||
resolution: "node-fetch@npm:2.6.7"
|
||||
dependencies:
|
||||
@@ -12363,6 +12384,17 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 6.10.3
|
||||
resolution: "qs@npm:6.10.3"
|
||||
@@ -15345,6 +15377,21 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 4.0.0
|
||||
resolution: "xml-name-validator@npm:4.0.0"
|
||||
|
||||
Reference in New Issue
Block a user