Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Language service for VS Code and Playground #429

Merged
merged 46 commits into from
Jun 28, 2023
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
276864f
WASM wrapper for language service
minestarks Jun 15, 2023
221cc9b
Merge branch 'main' of https://github.com/microsoft/qsharp into mines…
minestarks Jun 15, 2023
7d6875e
Merge branch 'main' of https://github.com/microsoft/qsharp into mines…
minestarks Jun 15, 2023
9b3a504
Merge branch 'main' of https://github.com/microsoft/qsharp into mines…
minestarks Jun 15, 2023
a7420ec
Move worker stuff into compiler subfolder
minestarks Jun 15, 2023
9a2a2da
Move all the compiler related files to a subfolder
minestarks Jun 16, 2023
a55ce99
Add QSharpLanguageService to npm package
minestarks Jun 16, 2023
f4d2b00
Add language service web worker
minestarks Jun 16, 2023
fdddf90
tests passing
minestarks Jun 22, 2023
64c3201
refactor event stuff
minestarks Jun 22, 2023
62752b0
Clean up requests too
minestarks Jun 22, 2023
c1971cf
cleaned up almost everything
minestarks Jun 22, 2023
e296151
bit more cleanup
minestarks Jun 22, 2023
f8e5866
Really clean it up
minestarks Jun 23, 2023
3b4ba81
Add language service
minestarks Jun 23, 2023
5314af0
Merge branch 'main' of https://github.com/microsoft/qsharp into mines…
minestarks Jun 23, 2023
93c3a71
update .eslintrc
minestarks Jun 23, 2023
144f290
revert package.json
minestarks Jun 23, 2023
b7e5e23
Fix npm entrypoint
minestarks Jun 23, 2023
b49bea6
Update README.md
minestarks Jun 23, 2023
0345a2a
Remove TODO and run prettier
minestarks Jun 23, 2023
1b03fa3
Add dispose()
minestarks Jun 23, 2023
dad7af8
Merge branch 'main' into minestarks/language-service-webworker
minestarks Jun 23, 2023
f904b55
Merge branch 'main' of https://github.com/microsoft/qsharp into mines…
minestarks Jun 23, 2023
4fc0d06
Revert accidental change
minestarks Jun 23, 2023
3569eb9
Remove TODO
minestarks Jun 24, 2023
bec69c3
Remove code samples from README.md
minestarks Jun 24, 2023
e4ffe62
Language Service for VS Code and Playground
minestarks Jun 24, 2023
cd609d0
Remove checkCode()
minestarks Jun 24, 2023
0f3f2f1
Remove ICompiler.getCompletions()
minestarks Jun 24, 2023
91449fd
Map severity properly
minestarks Jun 27, 2023
d1424d5
Register Monaco providers at startup
minestarks Jun 27, 2023
f43d4b4
Change where we call updateDocument()
minestarks Jun 27, 2023
57804fc
Merge branch 'main' of https://github.com/microsoft/qsharp into mines…
minestarks Jun 27, 2023
60b7a73
Revert unnecessary change in updateHir()
minestarks Jun 27, 2023
e0abd5c
Unsubscribe from events at disposal
minestarks Jun 27, 2023
1aab0e8
Revert language-configuration.json
minestarks Jun 27, 2023
d695d57
Remove TODO
minestarks Jun 27, 2023
12d3e0d
Revert accidental revert
minestarks Jun 27, 2023
e3816a5
Better vs code logging
minestarks Jun 27, 2023
c5d2e46
Bring back checkCode() for compatibility
minestarks Jun 28, 2023
225c6eb
Fix playground mistakes
minestarks Jun 28, 2023
cbc3a03
Add license headers
minestarks Jun 28, 2023
4c7caf9
Change logging level
minestarks Jun 28, 2023
535366c
Fix QscEventTarget initialization
minestarks Jun 28, 2023
cac517a
Move monaco provider registrations to proper init
minestarks Jun 28, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion npm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"node": "./dist/main.js",
"default": "./dist/browser.js"
},
"./worker": "./dist/compiler/worker-browser.js"
"./compiler-worker": "./dist/compiler/worker-browser.js",
"./language-service-worker": "./dist/language-service/worker-browser.js"
},
"scripts": {
"build": "npm run generate && npm run build:tsc",
Expand Down
1 change: 1 addition & 0 deletions npm/src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,4 @@ export { default as samples } from "./samples.generated.js";
export { type VSDiagnostic } from "./vsdiagnostic.js";
export { log, type LogLevel };
export type { ICompilerWorker };
export type { ILanguageServiceWorker, ILanguageService };
29 changes: 2 additions & 27 deletions npm/src/compiler/compiler.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import type { IDiagnostic, ICompletionList } from "../../lib/node/qsc_wasm.cjs";
import { log } from "../log.js";
import { VSDiagnostic } from "../vsdiagnostic.js";
import { IServiceProxy, ServiceState } from "../worker-proxy.js";
import { eventStringToMsg } from "./common.js";
import { mapDiagnostics, VSDiagnostic } from "../vsdiagnostic.js";
import { IQscEventTarget, QscEvents, makeEvent } from "./events.js";
import { IServiceProxy, ServiceState } from "../worker-proxy.js";

// The wasm types generated for the node.js bundle are just the exported APIs,
// so use those as the set used by the shared compiler
Expand All @@ -15,9 +14,7 @@ type Wasm = typeof import("../../lib/node/qsc_wasm.cjs");
// These need to be async/promise results for when communicating across a WebWorker, however
// for running the compiler in the same thread the result will be synchronous (a resolved promise).
export interface ICompiler {
checkCode(code: string): Promise<VSDiagnostic[]>;
minestarks marked this conversation as resolved.
Show resolved Hide resolved
getHir(code: string): Promise<string>;
getCompletions(): Promise<ICompletionList>;
run(
code: string,
expr: string,
Expand Down Expand Up @@ -63,32 +60,10 @@ export class Compiler implements ICompiler {
globalThis.qscGitHash = this.wasm.git_hash();
}

async checkCode(code: string): Promise<VSDiagnostic[]> {
// Temporary implementation until we have the language
// service notifications properly wired up to the editor.
let diags: IDiagnostic[] = [];
const languageService = new this.wasm.LanguageService(
(uri: string, version: number, errors: IDiagnostic[]) => {
diags = errors;
}
);
languageService.update_document("code", 1, code);
return mapDiagnostics(diags, code);
}

async getHir(code: string): Promise<string> {
return this.wasm.get_hir(code);
}

async getCompletions(): Promise<ICompletionList> {
// Temporary implementation until we have the language
// service properly wired up to the editor.
// eslint-disable-next-line @typescript-eslint/no-empty-function
const languageService = new this.wasm.LanguageService(() => {});
languageService.update_document("code", 1, "");
return languageService.get_completions("code", 1);
}

async run(
code: string,
expr: string,
Expand Down
2 changes: 0 additions & 2 deletions npm/src/compiler/worker-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ import { ICompiler } from "./compiler.js";
import { QscEventData } from "./events.js";

const requests: MethodMap<ICompiler> = {
checkCode: "request",
getHir: "request",
getCompletions: "request",
run: "requestWithProgress",
runKata: "requestWithProgress",
};
Expand Down
90 changes: 4 additions & 86 deletions npm/test/basics.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,6 @@ import samples from "../dist/samples.generated.js";

log.setLogLevel("warn");

/**
*
* @param {string} code
* @param {string} expr
* @param {boolean} useWorker
* @returns {Promise<import("../dist/common.js").ShotResult>}
*/
export function runSingleShot(code, expr, useWorker) {
return new Promise((resolve, reject) => {
const resultsHandler = new QscEventTarget(true);
Expand Down Expand Up @@ -66,38 +59,6 @@ namespace Test {
assert(result.result === "Zero");
});

test("one syntax error", async () => {
billti marked this conversation as resolved.
Show resolved Hide resolved
const compiler = getCompiler();

const diags = await compiler.checkCode("namespace Foo []");
assert.equal(diags.length, 1);
assert.equal(diags[0].start_pos, 14);
assert.equal(diags[0].end_pos, 15);
});

test("error with newlines", async () => {
const compiler = getCompiler();

const diags = await compiler.checkCode(
"namespace input { operation Foo(a) : Unit {} }"
);
assert.equal(diags.length, 1);
assert.equal(diags[0].start_pos, 32);
assert.equal(diags[0].end_pos, 33);
assert.equal(
diags[0].message,
"type error: missing type in item signature\n\nhelp: types cannot be inferred for global declarations"
);
});

test("completions include CNOT", async () => {
const compiler = getCompiler();

let results = await compiler.getCompletions();
let cnot = results.items.find((x) => x.label === "CNOT");
assert.ok(cnot);
});

test("dump and message output", async () => {
let code = `namespace Test {
function Answer() : Int {
Expand All @@ -117,27 +78,6 @@ test("dump and message output", async () => {
assert(result.events[1].message == "hello, qsharp");
});

test("type error", async () => {
let code = `namespace Sample {
operation main() : Result[] {
use q1 = Qubit();
Ry(q1);
let m1 = M(q1);
return [m1];
}
}`;
const compiler = getCompiler();
let result = await compiler.checkCode(code);

assert.equal(result.length, 1);
assert.equal(result[0].start_pos, 99);
assert.equal(result[0].end_pos, 105);
assert.equal(
result[0].message,
"type error: expected (Double, Qubit), found Qubit"
);
});

test("kata success", async () => {
const evtTarget = new QscEventTarget(true);
const compiler = getCompiler();
Expand Down Expand Up @@ -208,28 +148,6 @@ namespace Kata {
assert.equal(results[0].result.message, "Error: syntax error");
});

test("worker check", async () => {
let code = `namespace Sample {
operation main() : Result[] {
use q1 = Qubit();
Ry(q1);
let m1 = M(q1);
return [m1];
}
}`;
const compiler = getCompilerWorker();
let result = await compiler.checkCode(code);
compiler.terminate();

assert.equal(result.length, 1);
assert.equal(result[0].start_pos, 99);
assert.equal(result[0].end_pos, 105);
assert.equal(
result[0].message,
"type error: expected (Double, Qubit), found Qubit"
);
});

test("worker 100 shots", async () => {
let code = `namespace Test {
function Answer() : Int {
Expand Down Expand Up @@ -316,7 +234,7 @@ test("cancel worker", () => {
compiler.run(code, "", 10, resultsHandler).catch((err) => {
cancelledArray.push(err);
});
compiler.checkCode(code).catch((err) => {
compiler.getHir(code).catch((err) => {
cancelledArray.push(err);
});

Expand All @@ -327,11 +245,11 @@ test("cancel worker", () => {

// Start a new compiler and ensure that works fine
const compiler2 = getCompilerWorker();
const result = await compiler2.checkCode(code);
const result = await compiler2.getHir(code);
compiler2.terminate();

// New 'check' result is good
assert(Array.isArray(result) && result.length === 0);
// getHir should have worked
assert(typeof result === "string" && result.length > 0);

// Old requests were cancelled
assert(cancelledArray.length === 2);
Expand Down
6 changes: 5 additions & 1 deletion playground/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ const outdir = join(thisDir, "public/libs");

/** @type {import("esbuild").BuildOptions} */
const buildOptions = {
entryPoints: [join(thisDir, "src/main.tsx"), join(thisDir, "src/worker.ts")],
entryPoints: [
join(thisDir, "src/main.tsx"),
join(thisDir, "src/compiler-worker.ts"),
join(thisDir, "src/language-service-worker.ts"),
],
outdir,
bundle: true,
target: ["es2020", "chrome64", "edge79", "firefox62", "safari11.1"],
Expand Down
3 changes: 3 additions & 0 deletions playground/src/compiler-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { messageHandler } from "qsharp/compiler-worker";

self.onmessage = messageHandler;
63 changes: 51 additions & 12 deletions playground/src/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useEffect, useRef, useState } from "preact/hooks";
import {
CompilerState,
ICompilerWorker,
ILanguageServiceWorker,
QscEventTarget,
VSDiagnostic,
log,
Expand All @@ -24,10 +25,23 @@ function VSDiagsToMarkers(
srcModel: monaco.editor.ITextModel
): monaco.editor.IMarkerData[] {
return errors.map((err) => {
let severity = monaco.MarkerSeverity.Error;
switch (err.severity) {
case "error":
severity = monaco.MarkerSeverity.Error;
break;
case "warning":
severity = monaco.MarkerSeverity.Warning;
break;
case "info":
severity = monaco.MarkerSeverity.Info;
break;
}

const startPos = srcModel.getPositionAt(err.start_pos);
const endPos = srcModel.getPositionAt(err.end_pos);
const marker: monaco.editor.IMarkerData = {
severity: monaco.MarkerSeverity.Error,
severity,
message: err.message,
startLineNumber: startPos.lineNumber,
startColumn: startPos.column,
Expand All @@ -52,13 +66,14 @@ export function Editor(props: {
showShots: boolean;
setHir: (hir: string) => void;
activeTab: ActiveTab;
languageService: ILanguageServiceWorker;
}) {
const editor = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
const errMarks = useRef<ErrCollection>({ checkDiags: [], shotDiags: [] });
const editorDiv = useRef<HTMLDivElement>(null);

// Maintain a ref to the latest check function, as it closes over a bunch of stuff
const checkRef = useRef(async () => {
// Maintain a ref to the latest getHir function, as it closes over a bunch of stuff
const hirRef = useRef(async () => {
return;
});

Expand All @@ -69,10 +84,15 @@ export function Editor(props: {
);
const [hasCheckErrors, setHasCheckErrors] = useState(false);

function markErrors() {
function markErrors(version?: number) {
const model = editor.current?.getModel();
if (!model) return;

if (version != null && version !== model.getVersionId()) {
// Diagnostics event received for an outdated model
return;
}

const errs = [
...errMarks.current.checkDiags,
...errMarks.current.shotDiags,
Expand All @@ -88,16 +108,14 @@ export function Editor(props: {
setErrors(errList);
}

checkRef.current = async function onCheck() {
hirRef.current = async function updateHir() {
// This should get called on initial load and on every document update.
const code = editor.current?.getValue();
if (code == null) return;
const diags = await props.compiler.checkCode(code);
if (code == null) throw new Error("Why is code null?");

if (props.activeTab === "hir-tab") {
props.setHir(await props.compiler.getHir(code));
}
errMarks.current.checkDiags = diags;
markErrors();
setHasCheckErrors(diags.length > 0);
};

async function onRun() {
Expand Down Expand Up @@ -132,7 +150,14 @@ export function Editor(props: {
editor.current = newEditor;
const srcModel = monaco.editor.createModel(props.code, "qsharp");
newEditor.setModel(srcModel);
srcModel.onDidChangeContent(() => checkRef.current());
srcModel.onDidChangeContent(() => hirRef.current());
srcModel.onDidChangeContent(async () => {
await props.languageService.updateDocument(
minestarks marked this conversation as resolved.
Show resolved Hide resolved
srcModel.uri.toString(),
srcModel.getVersionId(),
srcModel.getValue()
);
});

function onResize() {
newEditor.layout();
Expand All @@ -147,6 +172,20 @@ export function Editor(props: {
};
}, []);

useEffect(() => {
props.languageService.addEventListener("diagnostics", (evt) => {
const diagnostics = evt.detail.diagnostics;
errMarks.current.checkDiags = diagnostics;
markErrors(evt.detail.version);
setHasCheckErrors(diagnostics.length > 0);
});
}, [props.languageService]);

useEffect(() => {
minestarks marked this conversation as resolved.
Show resolved Hide resolved
// Whenever the active tab changes, run check again.
hirRef.current();
}, [props.activeTab]);

useEffect(() => {
const theEditor = editor.current;
if (!theEditor) return;
Expand All @@ -164,7 +203,7 @@ export function Editor(props: {

useEffect(() => {
// Whenever the active tab changes, run check again.
checkRef.current();
hirRef.current();
}, [props.activeTab]);

// On reset, reload the initial code
Expand Down
10 changes: 9 additions & 1 deletion playground/src/kata.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@
// Licensed under the MIT License.

import { useEffect, useRef } from "preact/hooks";
import { CompilerState, ICompilerWorker, Kata, QscEventTarget } from "qsharp";
import {
CompilerState,
ICompilerWorker,
ILanguageServiceWorker,
Kata,
QscEventTarget,
} from "qsharp";
import { Editor } from "./editor.js";
import { OutputTabs } from "./tabs.js";

Expand All @@ -11,6 +17,7 @@ export function Kata(props: {
compiler: ICompilerWorker;
compilerState: CompilerState;
onRestartCompiler: () => void;
languageService: ILanguageServiceWorker;
}) {
const kataContent = useRef<HTMLDivElement>(null);
const itemContent = useRef<(HTMLDivElement | null)[]>([]);
Expand Down Expand Up @@ -81,6 +88,7 @@ export function Kata(props: {
key={item.id}
setHir={() => ({})}
activeTab="results-tab"
languageService={props.languageService}
></Editor>
<OutputTabs
key={item.id + "-results"}
Expand Down
Loading