Skip to content

Commit 107edfa

Browse files
Add diagnostics and autocomplete. (#251)
Adds support for the Pyright language server based on a fork with browser support. This PR uses a build from https://github.com/microbit-matt-hillsdon/pyright/tree/background-worker as of 03d67527beb5c8e9ede2e9baa7bab83ed481237f. Key limitations: - We might consider some more student focussed error messages for common scenarios. - We might like to add hover, signature help, documentation for completions and other features that require more language server integration. See #240, #239
1 parent cf9c70b commit 107edfa

28 files changed

+923
-146
lines changed

.vscode/settings.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"python.formatting.provider": "black"
3+
}

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ This software is under the MIT open source license.
8686

8787
Binaries for MicroPython are included for micro:bit V1 ([license](https://github.com/bbcmicrobit/micropython/blob/master/LICENSE)) and micro:bit V2 ([license](https://github.com/microbit-foundation/micropython-microbit-v2/blob/master/LICENSE)). Both are MIT licensed.
8888

89+
Python diagnostics and autocomplete use a fork of Microsoft's Pyright type checker which has been [modified by us](public/workers/PYRIGHT_README.txt) to run as a Web Worker. Pyright is © Microsoft Corporation and [used under an MIT license](public/workers/PYRIGHT_LICENSE.txt).
90+
8991
We use dependencies via the NPM registry as specified by the package.json file under common Open Source licenses.
9092

9193
Full details of each package can be found by running `license-checker`:

package-lock.json

+19
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
"react-dom": "^17.0.2",
4040
"react-icons": "^4.2.0",
4141
"react-intl": "^5.20.7",
42+
"vscode-jsonrpc": "^6.0.0",
43+
"vscode-languageserver-protocol": "^3.16.0",
4244
"web-vitals": "^1.1.1",
4345
"xterm": "^4.13.0",
4446
"xterm-addon-fit": "^0.5.0"

public/workers/PYRIGHT_LICENSE.txt

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
MIT License
2+
3+
Pyright - A static type checker for the Python language
4+
Copyright (c) Microsoft Corporation. All rights reserved.
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
SOFTWARE

public/workers/PYRIGHT_README.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Pyright as a Web Worker.
2+
3+
Source: https://github.com/microbit-matt-hillsdon/pyright

public/workers/pyright-0fa21f4869dd61b44452.worker.js

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/workers/pyright-0fa21f4869dd61b44452.worker.js.map

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/App.test.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
* (c) 2021, Micro:bit Educational Foundation and contributors
33
*
44
* SPDX-License-Identifier: MIT
5+
*
6+
* @jest-environment ./src/testing/custom-browser-env
57
*/
68
import { render } from "@testing-library/react";
79
import App from "./App";

src/App.tsx

+15-6
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import { MockDeviceConnection } from "./device/mock";
1616
import { FileSystem } from "./fs/fs";
1717
import { FileSystemContext } from "./fs/fs-hooks";
1818
import { fetchMicroPython } from "./fs/micropython";
19+
import { trackFsChanges } from "./language-server/client-fs";
20+
import { LanguageServerClientContext } from "./language-server/language-server-hooks";
21+
import { pyright } from "./language-server/pyright";
1922
import { LoggingContext } from "./logging/logging-hooks";
2023
import TranslationProvider from "./messages/TranslationProvider";
2124
import ProjectDropTarget from "./project/ProjectDropTarget";
@@ -39,7 +42,11 @@ const logging = deployment.logging;
3942
const device = isMockDeviceMode()
4043
? new MockDeviceConnection()
4144
: new MicrobitWebUSBConnection({ logging });
45+
46+
const client = pyright();
4247
const fs = new FileSystem(logging, fetchMicroPython);
48+
client?.initialize().then(() => trackFsChanges(client, fs));
49+
4350
// If this fails then we retry on access.
4451
fs.initializeInBackground();
4552

@@ -69,12 +76,14 @@ const App = () => {
6976
<DialogProvider>
7077
<DeviceContext.Provider value={device}>
7178
<FileSystemContext.Provider value={fs}>
72-
<BeforeUnloadDirtyCheck />
73-
<SelectionContext>
74-
<ProjectDropTarget>
75-
<Workbench />
76-
</ProjectDropTarget>
77-
</SelectionContext>
79+
<LanguageServerClientContext.Provider value={client}>
80+
<BeforeUnloadDirtyCheck />
81+
<SelectionContext>
82+
<ProjectDropTarget>
83+
<Workbench />
84+
</ProjectDropTarget>
85+
</SelectionContext>
86+
</LanguageServerClientContext.Provider>
7887
</FileSystemContext.Provider>
7988
</DeviceContext.Provider>
8089
</DialogProvider>

src/device/mock.ts

-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ export class MockDeviceConnection
4040

4141
async connect(): Promise<ConnectionStatus> {
4242
this.setStatus(ConnectionStatus.CONNECTED);
43-
console.log("Adding listener");
4443
document.addEventListener("mockSerialWrite", this.mockSerialListener);
4544
return this.status;
4645
}

src/editor/EditorContainer.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const EditorContainer = ({ selection }: EditorContainerProps) => {
2222
return typeof defaultValue === "undefined" ? null : (
2323
<Editor
2424
defaultValue={defaultValue}
25-
location={selection.location}
25+
selection={selection}
2626
onChange={onFileChange}
2727
fontSize={settings.fontSize}
2828
codeStructureSettings={codeStructureSettings(settings)}

src/editor/codemirror/CodeMirror.tsx

+11-4
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ import { EditorSelection, EditorState } from "@codemirror/state";
77
import { EditorView } from "@codemirror/view";
88
import { useEffect, useMemo, useRef } from "react";
99
import { useIntl } from "react-intl";
10-
import { FileLocation } from "../../workbench/use-selection";
10+
import { createUri } from "../../language-server/client";
11+
import { useLanguageServerClient } from "../../language-server/language-server-hooks";
12+
import { WorkbenchSelection } from "../../workbench/use-selection";
1113
import "./CodeMirror.css";
1214
import { editorConfig, themeExtensionsCompartment } from "./config";
15+
import { languageServer } from "./language-server/view";
1316
import {
1417
codeStructure,
1518
CodeStructureSettings,
@@ -22,7 +25,7 @@ interface CodeMirrorProps {
2225
defaultValue: string;
2326
onChange: (doc: string) => void;
2427

25-
location: FileLocation;
28+
selection: WorkbenchSelection;
2629
fontSize: number;
2730
codeStructureSettings: CodeStructureSettings;
2831
}
@@ -39,12 +42,14 @@ const CodeMirror = ({
3942
defaultValue,
4043
className,
4144
onChange,
42-
location,
45+
selection,
4346
fontSize,
4447
codeStructureSettings,
4548
}: CodeMirrorProps) => {
49+
const uri = createUri(selection.file);
4650
const elementRef = useRef<HTMLDivElement | null>(null);
4751
const viewRef = useRef<EditorView | null>(null);
52+
const client = useLanguageServerClient();
4853
const intl = useIntl();
4954

5055
// Group the option props together to keep configuration updates simple.
@@ -69,6 +74,7 @@ const CodeMirror = ({
6974
extensions: [
7075
notify,
7176
editorConfig,
77+
client ? languageServer(client, uri) : [],
7278
// Extensions we enable/disable based on props.
7379
structureHighlightingCompartment.of(
7480
codeStructure(options.codeStructureSettings)
@@ -83,7 +89,7 @@ const CodeMirror = ({
8389

8490
viewRef.current = view;
8591
}
86-
}, [options, defaultValue, onChange]);
92+
}, [options, defaultValue, onChange, client, uri]);
8793
useEffect(() => {
8894
// Do this separately as we don't want to destroy the view whenever options needed for initialization change.
8995
return () => {
@@ -107,6 +113,7 @@ const CodeMirror = ({
107113
});
108114
}, [options]);
109115

116+
const { location } = selection;
110117
useEffect(() => {
111118
// When the identity of location changes then the user has navigated.
112119
if (location.line) {

src/editor/codemirror/completion.ts

-115
This file was deleted.

src/editor/codemirror/config.ts

+15-19
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,26 @@
33
*
44
* SPDX-License-Identifier: MIT
55
*/
6+
import { completionKeymap } from "@codemirror/autocomplete";
7+
import { closeBrackets, closeBracketsKeymap } from "@codemirror/closebrackets";
8+
import { defaultKeymap, indentLess, indentMore } from "@codemirror/commands";
9+
import { commentKeymap } from "@codemirror/comment";
10+
import { highlightActiveLineGutter, lineNumbers } from "@codemirror/gutter";
11+
import { defaultHighlightStyle } from "@codemirror/highlight";
12+
import { history, historyKeymap } from "@codemirror/history";
13+
import { python } from "@codemirror/lang-python";
14+
import { indentOnInput, indentUnit } from "@codemirror/language";
15+
import { lintKeymap } from "@codemirror/lint";
16+
import { bracketMatching } from "@codemirror/matchbrackets";
17+
import { Compartment, EditorState, Extension, Prec } from "@codemirror/state";
618
import {
7-
keymap,
8-
highlightSpecialChars,
919
drawSelection,
20+
EditorView,
1021
highlightActiveLine,
22+
highlightSpecialChars,
1123
KeyBinding,
24+
keymap,
1225
} from "@codemirror/view";
13-
import { Extension, EditorState, Prec, Compartment } from "@codemirror/state";
14-
import { history, historyKeymap } from "@codemirror/history";
15-
import { indentOnInput, indentUnit } from "@codemirror/language";
16-
import { highlightActiveLineGutter, lineNumbers } from "@codemirror/gutter";
17-
import { defaultKeymap, indentLess, indentMore } from "@codemirror/commands";
18-
import { bracketMatching } from "@codemirror/matchbrackets";
19-
import { closeBrackets, closeBracketsKeymap } from "@codemirror/closebrackets";
20-
import { autocompletion, completionKeymap } from "@codemirror/autocomplete";
21-
import { commentKeymap } from "@codemirror/comment";
22-
import { defaultHighlightStyle } from "@codemirror/highlight";
23-
import { lintKeymap } from "@codemirror/lint";
24-
import { EditorView } from "@codemirror/view";
25-
import { python } from "@codemirror/lang-python";
26-
import { completion } from "./completion";
2726
import highlightStyle from "./highlightStyle";
2827

2928
const customTabBinding: KeyBinding = {
@@ -53,9 +52,6 @@ export const editorConfig: Extension = [
5352
bracketMatching(),
5453
closeBrackets(),
5554
highlightStyle(),
56-
autocompletion({
57-
override: completion,
58-
}),
5955
highlightActiveLine(),
6056
highlightActiveLineGutter(),
6157

0 commit comments

Comments
 (0)