microsoft/qdk

Public

mirrored fromhttps://github.com/microsoft/qdkAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
9831093db0098b3a3e55cbadf3929222d7dd4805

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

source/playground/src/editor.tsx

481lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4/// <reference types="../../../node_modules/monaco-editor/monaco.d.ts"/>
5
6import { useEffect, useRef, useState } from "preact/hooks";
7import {
8 CompilerState,
9 ICompilerWorker,
10 ILanguageServiceWorker,
11 QscEventTarget,
12 VSDiagnostic,
13 log,
14 ProgramConfig,
15 LanguageServiceDiagnosticEvent,
16 getTargetProfileFromEntryPoint,
17} from "qsharp-lang";
18import { Exercise, getExerciseSources } from "qsharp-lang/katas-md";
19import { codeToCompressedBase64, lsRangeToMonacoRange } from "./utils.js";
20import { ActiveTab } from "./main.js";
21
22import type { KataSection } from "qsharp-lang/katas";
23
24type ErrCollection = {
25 checkDiags: VSDiagnostic[];
26 shotDiags: VSDiagnostic[];
27};
28
29function VSDiagsToMarkers(errors: VSDiagnostic[]): monaco.editor.IMarkerData[] {
30 return errors.map((err) => {
31 let severity = monaco.MarkerSeverity.Error;
32 switch (err.severity) {
33 case "error":
34 severity = monaco.MarkerSeverity.Error;
35 break;
36 case "warning":
37 severity = monaco.MarkerSeverity.Warning;
38 break;
39 case "info":
40 severity = monaco.MarkerSeverity.Info;
41 break;
42 }
43
44 const marker: monaco.editor.IMarkerData = {
45 ...lsRangeToMonacoRange(err.range),
46 severity,
47 message: err.message,
48 relatedInformation: err.related?.map((r) => {
49 const range = lsRangeToMonacoRange(r.location.span);
50 return {
51 resource: monaco.Uri.parse(r.location.source),
52 message: r.message,
53 ...range,
54 };
55 }),
56 };
57
58 if (err.uri && err.code) {
59 marker.code = {
60 value: err.code,
61 target: monaco.Uri.parse(err.uri),
62 };
63 } else if (err.code) {
64 marker.code = err.code;
65 }
66
67 return marker;
68 });
69}
70
71export function Editor(props: {
72 code: string;
73 compiler: ICompilerWorker;
74 compiler_worker_factory: () => ICompilerWorker;
75 compilerState: CompilerState;
76 defaultShots: number;
77 evtTarget: QscEventTarget;
78 kataSection?: KataSection;
79 onRestartCompiler: () => void;
80 shotError?: VSDiagnostic;
81 showExpr: boolean;
82 showShots: boolean;
83 setAst: (ast: string) => void;
84 setHir: (hir: string) => void;
85 setRir: (rir: string[]) => void;
86 setQir: (qir: string) => void;
87 activeTab: ActiveTab;
88 languageService: ILanguageServiceWorker;
89}) {
90 const editor = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
91 const errMarks = useRef<ErrCollection>({ checkDiags: [], shotDiags: [] });
92 const editorDiv = useRef<HTMLDivElement>(null);
93
94 // Maintain a ref to the latest getAst/getHir functions, as it closes over a bunch of stuff
95 const irRef = useRef(async () => {
96 return;
97 });
98 const [shotCount, setShotCount] = useState(props.defaultShots);
99 const [runExpr, setRunExpr] = useState("");
100 const [errors, setErrors] = useState<
101 { location: string; severity: monaco.MarkerSeverity; msg: string[] }[]
102 >([]);
103 const [hasCheckErrors, setHasCheckErrors] = useState(false);
104
105 function markErrors() {
106 const model = editor.current?.getModel();
107 if (!model) return;
108
109 const errs = [
110 ...errMarks.current.checkDiags,
111 ...errMarks.current.shotDiags,
112 ];
113
114 const markers = VSDiagsToMarkers(errs);
115 monaco.editor.setModelMarkers(model, "qsharp", markers);
116
117 const errList = markers.map((err) => ({
118 location: `main.qs@(${err.startLineNumber},${err.startColumn})`,
119 severity: err.severity,
120 msg: err.message.split("\n\n"),
121 }));
122 setErrors(errList);
123 }
124
125 irRef.current = async function updateIr() {
126 const code = editor.current?.getValue();
127 if (code == null) return;
128
129 const config = {
130 sources: [["code", code]] as [string, string][],
131 languageFeatures: [],
132 profile:
133 (await getTargetProfileFromEntryPoint("main.qs", code)) ||
134 "adaptive_rif", // Default to adaptive_rif for qir and rir generation
135 };
136
137 if (props.activeTab === "ast-tab") {
138 props.setAst(await props.compiler.getAst(code, config.languageFeatures));
139 }
140 if (props.activeTab === "hir-tab") {
141 props.setHir(await props.compiler.getHir(code, config.languageFeatures));
142 }
143 const codeGenTimeout = 1000; // ms
144 if (props.activeTab === "qir-tab" || props.activeTab === "rir-tab") {
145 let timedOut = false;
146 const compiler = props.compiler_worker_factory();
147 const compilerTimeout = setTimeout(() => {
148 log.info("Compiler timeout. Terminating worker.");
149 timedOut = true;
150 compiler.terminate();
151 }, codeGenTimeout);
152 try {
153 if (props.activeTab === "rir-tab") {
154 const ir = await compiler.getRir(config);
155 clearTimeout(compilerTimeout);
156 props.setRir(ir);
157 } else {
158 const ir = await compiler.getQir(config);
159 clearTimeout(compilerTimeout);
160 props.setQir(ir);
161 }
162 } catch (e: any) {
163 if (timedOut) {
164 if (props.activeTab === "rir-tab") {
165 props.setRir(["timed out", "timed out"]);
166 } else {
167 props.setQir("timed out");
168 }
169 } else {
170 if (props.activeTab === "rir-tab") {
171 props.setRir([e.toString(), e.toString()]);
172 } else {
173 props.setQir(e.toString());
174 }
175 }
176 } finally {
177 compiler.terminate();
178 }
179 }
180 };
181
182 async function onRun() {
183 const code = editor.current?.getValue();
184 if (code == null) return;
185 props.evtTarget.clearResults();
186 const config = {
187 sources: [["code", code]],
188 languageFeatures: [],
189 profile: await getTargetProfileFromEntryPoint("main.qs", code),
190 } as ProgramConfig;
191
192 try {
193 if (props.kataSection?.type === "exercise") {
194 // This is for a kata exercise. Provide the sources that implement the solution verification.
195 const sources = await getExerciseSources(props.kataSection as Exercise);
196 // check uses the unrestricted profile and doesn't do code gen,
197 // so we just pass the sources
198 await props.compiler.checkExerciseSolution(
199 code,
200 sources,
201 props.evtTarget,
202 );
203 } else {
204 performance.mark("compiler-run-start");
205 await props.compiler.run(config, runExpr, shotCount, props.evtTarget);
206 const runTimer = performance.measure(
207 "compiler-run",
208 "compiler-run-start",
209 );
210 log.logTelemetry({
211 id: "compiler-run",
212 data: {
213 duration: runTimer.duration,
214 codeSize: code.length,
215 shotCount,
216 },
217 });
218 }
219 } catch (err) {
220 // This could fail for several reasons, e.g. the run being cancelled.
221 if (err === "terminated") {
222 log.info("Run was terminated");
223 } else {
224 log.error("Run failed with error: %o", err);
225 }
226 }
227 }
228
229 useEffect(() => {
230 if (!editorDiv.current) return;
231 const newEditor = monaco.editor.create(editorDiv.current, {
232 minimap: { enabled: false },
233 lineNumbersMinChars: 3,
234 automaticLayout: true,
235 });
236
237 editor.current = newEditor;
238 const srcModel =
239 monaco.editor.getModel(
240 monaco.Uri.parse(props.kataSection?.id ?? "main.qs"),
241 ) ??
242 monaco.editor.createModel(
243 "",
244 "qsharp",
245 monaco.Uri.parse(props.kataSection?.id ?? "main.qs"),
246 );
247 srcModel.setValue(props.code);
248 newEditor.setModel(srcModel);
249 srcModel.onDidChangeContent(() => irRef.current());
250
251 // TODO: If the language service ever changes, this callback
252 // will be invalid as it captures the *original* props.languageService
253 // and not the updated one. Not a problem currently since the language
254 // service is never updated, but not correct either.
255 srcModel.onDidChangeContent(async () => {
256 // Reset the shot errors whenever the document changes.
257 // The markers will be refreshed by the onDiagnostics callback
258 // when the language service finishes checking the document.
259 errMarks.current.shotDiags = [];
260
261 performance.mark("update-document-start");
262 await props.languageService.updateDocument(
263 srcModel.uri.toString(),
264 srcModel.getVersionId(),
265 srcModel.getValue(),
266 );
267 const measure = performance.measure(
268 "update-document",
269 "update-document-start",
270 );
271 log.info(`updateDocument took ${measure.duration}ms`);
272 });
273
274 function onResize() {
275 newEditor.layout();
276 }
277
278 // If the browser window resizes, tell the editor to update it's layout
279 window.addEventListener("resize", onResize);
280 return () => {
281 log.info("Disposing a monaco editor");
282 window.removeEventListener("resize", onResize);
283 props.languageService.closeDocument(srcModel.uri.toString());
284 newEditor.dispose();
285 };
286 }, []);
287
288 useEffect(() => {
289 props.languageService.updateConfiguration({
290 packageType: props.kataSection ? "lib" : "exe",
291 lints: props.kataSection
292 ? []
293 : [{ lint: "needlessOperation", level: "warn" }],
294 });
295
296 function onDiagnostics(evt: LanguageServiceDiagnosticEvent) {
297 const diagnostics = evt.detail.diagnostics;
298 errMarks.current.checkDiags = diagnostics;
299 markErrors();
300 setHasCheckErrors(
301 diagnostics.filter((d) => d.severity === "error").length > 0,
302 );
303 }
304
305 props.languageService.addEventListener("diagnostics", onDiagnostics);
306
307 return () => {
308 log.info("Removing diagnostics listener");
309 props.languageService.removeEventListener("diagnostics", onDiagnostics);
310 };
311 }, [props.languageService, props.kataSection]);
312
313 useEffect(() => {
314 const theEditor = editor.current;
315 if (!theEditor) return;
316
317 theEditor.getModel()?.setValue(props.code);
318 theEditor.revealLineNearTop(1);
319 setShotCount(props.defaultShots);
320 setRunExpr("");
321 }, [props.code, props.defaultShots]);
322
323 useEffect(() => {
324 errMarks.current.shotDiags = props.shotError ? [props.shotError] : [];
325 markErrors();
326 }, [props.shotError]);
327
328 useEffect(() => {
329 // Whenever the active tab changes, run check again.
330 irRef.current();
331 }, [props.activeTab]);
332
333 // On reset, reload the initial code
334 function onReset() {
335 const theEditor = editor.current;
336 if (!theEditor) return;
337 theEditor.getModel()?.setValue(props.code || "");
338 setShotCount(props.defaultShots);
339 setRunExpr("");
340 }
341
342 async function onGetLink(ev: MouseEvent) {
343 const code = editor.current?.getModel()?.getValue();
344 if (!code) return;
345
346 let messageText = "Unable to create the link";
347 try {
348 const encodedCode = await codeToCompressedBase64(code);
349 const escapedCode = encodeURIComponent(encodedCode);
350 // Update or add the current URL parameter 'code'
351 const newURL = new URL(window.location.href);
352 newURL.searchParams.set("code", escapedCode);
353
354 // Copy link to clipboard and update url without reloading the page
355 navigator.clipboard.writeText(newURL.toString());
356
357 window.history.pushState({}, "", newURL.toString());
358 messageText = "Link was copied to the clipboard";
359 } finally {
360 const popup = document.getElementById("popup") as HTMLDivElement;
361 popup.style.display = "block";
362 popup.innerText = messageText;
363 popup.style.left = `${ev.clientX - 120}px`;
364 popup.style.top = `${ev.clientY - 40}px`;
365
366 setTimeout(() => {
367 popup.style.display = "none";
368 }, 2000);
369 }
370 }
371
372 function shotCountChanged(e: Event) {
373 const target = e.target as HTMLInputElement;
374 setShotCount(parseInt(target.value) || 1);
375 }
376
377 function runExprChanged(e: Event) {
378 const target = e.target as HTMLInputElement;
379 setRunExpr(target.value);
380 }
381
382 return (
383 <div class="editor-column">
384 <div style="display: flex; justify-content: space-between; align-items: center;">
385 <div class="file-name">main.qs</div>
386 <div class="icon-row">
387 <svg
388 onClick={onGetLink}
389 width="24px"
390 height="24px"
391 viewBox="0 0 24 24"
392 fill="none"
393 >
394 <title>Get a link to this code</title>
395 <path
396 d="M14 12C14 14.2091 12.2091 16 10 16H6C3.79086 16 2 14.2091 2 12C2 9.79086 3.79086 8 6 8H8M10 12C10 9.79086 11.7909 8 14 8H18C20.2091 8 22 9.79086 22 12C22 14.2091 20.2091 16 18 16H16"
397 stroke="#000000"
398 stroke-width="2"
399 stroke-linecap="round"
400 stroke-linejoin="round"
401 />
402 </svg>
403 <svg
404 onClick={onReset}
405 width="24px"
406 height="24px"
407 viewBox="0 0 24 24"
408 fill="none"
409 >
410 <title>Reset code to initial state</title>
411 <path
412 d="M4,13 C4,17.4183 7.58172,21 12,21 C16.4183,21 20,17.4183 20,13 C20,8.58172 16.4183,5 12,5 C10.4407,5 8.98566,5.44609 7.75543,6.21762"
413 stroke="#0C0310"
414 stroke-width="2"
415 stroke-linecap="round"
416 ></path>
417 <path
418 d="M9.2384,1.89795 L7.49856,5.83917 C7.27552,6.34441 7.50429,6.9348 8.00954,7.15784 L11.9508,8.89768"
419 stroke="#0C0310"
420 stroke-width="2"
421 stroke-linecap="round"
422 ></path>
423 </svg>
424 </div>
425 </div>
426 <div class="code-editor" ref={editorDiv}></div>
427 <div class="button-row">
428 {props.showExpr ? (
429 <>
430 <span>Start</span>
431 <input
432 style="width: 160px"
433 value={runExpr}
434 onChange={runExprChanged}
435 />
436 </>
437 ) : null}
438 {props.showShots ? (
439 <>
440 <span>Shots</span>
441 <input
442 style="width: 88px;"
443 type="number"
444 value={shotCount || 100}
445 max="1000"
446 min="1"
447 onChange={shotCountChanged}
448 />
449 </>
450 ) : null}
451 <button
452 class="main-button"
453 onClick={onRun}
454 disabled={hasCheckErrors || props.compilerState === "busy"}
455 >
456 Run
457 </button>
458 <button
459 class="main-button"
460 onClick={props.onRestartCompiler}
461 disabled={props.compilerState === "idle"}
462 >
463 Cancel
464 </button>
465 </div>
466 <div class="diag-list">
467 {errors.map((err) => (
468 <div
469 className={`diag-row ${err.severity === monaco.MarkerSeverity.Error ? "error-row" : "warning-row"}`}
470 >
471 <span>{err.location}: </span>
472 <span>{err.msg[0]}</span>
473 {err.msg.length > 1 ? (
474 <div class="diag-help">{err.msg[1]}</div>
475 ) : null}
476 </div>
477 ))}
478 </div>
479 </div>
480 );
481}
482