microsoft/qdk

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.18.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

source/playground/src/editor.tsx

527lines · 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 TargetProfile,
16 LanguageServiceDiagnosticEvent,
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
71// get the language service profile from the URL
72// default to unrestricted if not specified
73export function getProfile(): TargetProfile {
74 return (new URLSearchParams(window.location.search).get("profile") ??
75 "unrestricted") as TargetProfile;
76}
77
78export function Editor(props: {
79 code: string;
80 compiler: ICompilerWorker;
81 compiler_worker_factory: () => ICompilerWorker;
82 compilerState: CompilerState;
83 defaultShots: number;
84 evtTarget: QscEventTarget;
85 kataSection?: KataSection;
86 onRestartCompiler: () => void;
87 shotError?: VSDiagnostic;
88 showExpr: boolean;
89 showShots: boolean;
90 profile: TargetProfile;
91 setAst: (ast: string) => void;
92 setHir: (hir: string) => void;
93 setRir: (rir: string[]) => void;
94 setQir: (qir: string) => void;
95 activeTab: ActiveTab;
96 languageService: ILanguageServiceWorker;
97}) {
98 const editor = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
99 const errMarks = useRef<ErrCollection>({ checkDiags: [], shotDiags: [] });
100 const editorDiv = useRef<HTMLDivElement>(null);
101
102 // Maintain a ref to the latest getAst/getHir functions, as it closes over a bunch of stuff
103 const irRef = useRef(async () => {
104 return;
105 });
106 const [profile, setProfile] = useState(props.profile);
107 const [shotCount, setShotCount] = useState(props.defaultShots);
108 const [runExpr, setRunExpr] = useState("");
109 const [errors, setErrors] = useState<
110 { location: string; severity: monaco.MarkerSeverity; msg: string[] }[]
111 >([]);
112 const [hasCheckErrors, setHasCheckErrors] = useState(false);
113
114 function markErrors() {
115 const model = editor.current?.getModel();
116 if (!model) return;
117
118 const errs = [
119 ...errMarks.current.checkDiags,
120 ...errMarks.current.shotDiags,
121 ];
122
123 const markers = VSDiagsToMarkers(errs);
124 monaco.editor.setModelMarkers(model, "qsharp", markers);
125
126 const errList = markers.map((err) => ({
127 location: `main.qs@(${err.startLineNumber},${err.startColumn})`,
128 severity: err.severity,
129 msg: err.message.split("\n\n"),
130 }));
131 setErrors(errList);
132 }
133
134 irRef.current = async function updateIr() {
135 const code = editor.current?.getValue();
136 if (code == null) return;
137
138 const config = {
139 sources: [["code", code]] as [string, string][],
140 languageFeatures: [],
141 profile: profile,
142 };
143
144 if (props.activeTab === "ast-tab") {
145 props.setAst(
146 await props.compiler.getAst(
147 code,
148 config.languageFeatures,
149 config.profile,
150 ),
151 );
152 }
153 if (props.activeTab === "hir-tab") {
154 props.setHir(
155 await props.compiler.getHir(
156 code,
157 config.languageFeatures,
158 config.profile,
159 ),
160 );
161 }
162 const codeGenTimeout = 1000; // ms
163 if (props.activeTab === "qir-tab" || props.activeTab === "rir-tab") {
164 let timedOut = false;
165 const compiler = props.compiler_worker_factory();
166 const compilerTimeout = setTimeout(() => {
167 log.info("Compiler timeout. Terminating worker.");
168 timedOut = true;
169 compiler.terminate();
170 }, codeGenTimeout);
171 try {
172 if (props.activeTab === "rir-tab") {
173 const ir = await compiler.getRir(config);
174 clearTimeout(compilerTimeout);
175 props.setRir(ir);
176 } else {
177 const ir = await compiler.getQir(config);
178 clearTimeout(compilerTimeout);
179 props.setQir(ir);
180 }
181 } catch (e: any) {
182 if (timedOut) {
183 if (props.activeTab === "rir-tab") {
184 props.setRir(["timed out", "timed out"]);
185 } else {
186 props.setQir("timed out");
187 }
188 } else {
189 if (props.activeTab === "rir-tab") {
190 props.setRir([e.toString(), e.toString()]);
191 } else {
192 props.setQir(e.toString());
193 }
194 }
195 } finally {
196 compiler.terminate();
197 }
198 }
199 };
200
201 async function onRun() {
202 const code = editor.current?.getValue();
203 if (code == null) return;
204 props.evtTarget.clearResults();
205 const config = {
206 sources: [["code", code]],
207 languageFeatures: [],
208 profile: profile,
209 } as ProgramConfig;
210
211 try {
212 if (props.kataSection?.type === "exercise") {
213 // This is for a kata exercise. Provide the sources that implement the solution verification.
214 const sources = await getExerciseSources(props.kataSection as Exercise);
215 // check uses the unrestricted profile and doesn't do code gen,
216 // so we just pass the sources
217 await props.compiler.checkExerciseSolution(
218 code,
219 sources,
220 props.evtTarget,
221 );
222 } else {
223 performance.mark("compiler-run-start");
224 await props.compiler.run(config, runExpr, shotCount, props.evtTarget);
225 const runTimer = performance.measure(
226 "compiler-run",
227 "compiler-run-start",
228 );
229 log.logTelemetry({
230 id: "compiler-run",
231 data: {
232 duration: runTimer.duration,
233 codeSize: code.length,
234 shotCount,
235 },
236 });
237 }
238 } catch (err) {
239 // This could fail for several reasons, e.g. the run being cancelled.
240 if (err === "terminated") {
241 log.info("Run was terminated");
242 } else {
243 log.error("Run failed with error: %o", err);
244 }
245 }
246 }
247
248 useEffect(() => {
249 if (!editorDiv.current) return;
250 const newEditor = monaco.editor.create(editorDiv.current, {
251 minimap: { enabled: false },
252 lineNumbersMinChars: 3,
253 automaticLayout: true,
254 });
255
256 editor.current = newEditor;
257 const srcModel =
258 monaco.editor.getModel(
259 monaco.Uri.parse(props.kataSection?.id ?? "main.qs"),
260 ) ??
261 monaco.editor.createModel(
262 "",
263 "qsharp",
264 monaco.Uri.parse(props.kataSection?.id ?? "main.qs"),
265 );
266 srcModel.setValue(props.code);
267 newEditor.setModel(srcModel);
268 srcModel.onDidChangeContent(() => irRef.current());
269
270 // TODO: If the language service ever changes, this callback
271 // will be invalid as it captures the *original* props.languageService
272 // and not the updated one. Not a problem currently since the language
273 // service is never updated, but not correct either.
274 srcModel.onDidChangeContent(async () => {
275 // Reset the shot errors whenever the document changes.
276 // The markers will be refreshed by the onDiagnostics callback
277 // when the language service finishes checking the document.
278 errMarks.current.shotDiags = [];
279
280 performance.mark("update-document-start");
281 await props.languageService.updateDocument(
282 srcModel.uri.toString(),
283 srcModel.getVersionId(),
284 srcModel.getValue(),
285 );
286 const measure = performance.measure(
287 "update-document",
288 "update-document-start",
289 );
290 log.info(`updateDocument took ${measure.duration}ms`);
291 });
292
293 function onResize() {
294 newEditor.layout();
295 }
296
297 // If the browser window resizes, tell the editor to update it's layout
298 window.addEventListener("resize", onResize);
299 return () => {
300 log.info("Disposing a monaco editor");
301 window.removeEventListener("resize", onResize);
302 props.languageService.closeDocument(srcModel.uri.toString());
303 newEditor.dispose();
304 };
305 }, []);
306
307 useEffect(() => {
308 props.languageService.updateConfiguration({
309 targetProfile: profile,
310 packageType: props.kataSection ? "lib" : "exe",
311 lints: props.kataSection
312 ? []
313 : [{ lint: "needlessOperation", level: "warn" }],
314 });
315
316 function onDiagnostics(evt: LanguageServiceDiagnosticEvent) {
317 const diagnostics = evt.detail.diagnostics;
318 errMarks.current.checkDiags = diagnostics;
319 markErrors();
320 setHasCheckErrors(
321 diagnostics.filter((d) => d.severity === "error").length > 0,
322 );
323 }
324
325 props.languageService.addEventListener("diagnostics", onDiagnostics);
326
327 return () => {
328 log.info("Removing diagnostics listener");
329 props.languageService.removeEventListener("diagnostics", onDiagnostics);
330 };
331 }, [props.languageService, props.kataSection]);
332
333 useEffect(() => {
334 const theEditor = editor.current;
335 if (!theEditor) return;
336
337 theEditor.getModel()?.setValue(props.code);
338 theEditor.revealLineNearTop(1);
339 setShotCount(props.defaultShots);
340 setRunExpr("");
341 }, [props.code, props.defaultShots]);
342
343 useEffect(() => {
344 errMarks.current.shotDiags = props.shotError ? [props.shotError] : [];
345 markErrors();
346 }, [props.shotError]);
347
348 useEffect(() => {
349 // Whenever the active tab changes, run check again.
350 irRef.current();
351 }, [props.activeTab]);
352
353 useEffect(() => {
354 // Whenever the selected profile changes, update the language service configuration
355 // and run the tabs again.
356 props.languageService.updateConfiguration({
357 targetProfile: profile,
358 });
359 irRef.current();
360 }, [profile]);
361
362 // On reset, reload the initial code
363 function onReset() {
364 const theEditor = editor.current;
365 if (!theEditor) return;
366 theEditor.getModel()?.setValue(props.code || "");
367 setShotCount(props.defaultShots);
368 setRunExpr("");
369 }
370
371 async function onGetLink(ev: MouseEvent) {
372 const code = editor.current?.getModel()?.getValue();
373 if (!code) return;
374
375 let messageText = "Unable to create the link";
376 try {
377 const encodedCode = await codeToCompressedBase64(code);
378 const escapedCode = encodeURIComponent(encodedCode);
379 // Update or add the current URL parameters 'code' and 'profile'
380 const newURL = new URL(window.location.href);
381 newURL.searchParams.set("code", escapedCode);
382 newURL.searchParams.set("profile", profile);
383
384 // Copy link to clipboard and update url without reloading the page
385 navigator.clipboard.writeText(newURL.toString());
386
387 window.history.pushState({}, "", newURL.toString());
388 messageText = "Link was copied to the clipboard";
389 } finally {
390 const popup = document.getElementById("popup") as HTMLDivElement;
391 popup.style.display = "block";
392 popup.innerText = messageText;
393 popup.style.left = `${ev.clientX - 120}px`;
394 popup.style.top = `${ev.clientY - 40}px`;
395
396 setTimeout(() => {
397 popup.style.display = "none";
398 }, 2000);
399 }
400 }
401
402 function shotCountChanged(e: Event) {
403 const target = e.target as HTMLInputElement;
404 setShotCount(parseInt(target.value) || 1);
405 }
406
407 function runExprChanged(e: Event) {
408 const target = e.target as HTMLInputElement;
409 setRunExpr(target.value);
410 }
411
412 function profileChanged(e: Event) {
413 const target = e.target as HTMLInputElement;
414 setProfile(target.value as TargetProfile);
415 }
416
417 return (
418 <div class="editor-column">
419 <div style="display: flex; justify-content: space-between; align-items: center;">
420 <div class="file-name">main.qs</div>
421 <div class="icon-row">
422 <svg
423 onClick={onGetLink}
424 width="24px"
425 height="24px"
426 viewBox="0 0 24 24"
427 fill="none"
428 >
429 <title>Get a link to this code</title>
430 <path
431 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"
432 stroke="#000000"
433 stroke-width="2"
434 stroke-linecap="round"
435 stroke-linejoin="round"
436 />
437 </svg>
438 <svg
439 onClick={onReset}
440 width="24px"
441 height="24px"
442 viewBox="0 0 24 24"
443 fill="none"
444 >
445 <title>Reset code to initial state</title>
446 <path
447 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"
448 stroke="#0C0310"
449 stroke-width="2"
450 stroke-linecap="round"
451 ></path>
452 <path
453 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"
454 stroke="#0C0310"
455 stroke-width="2"
456 stroke-linecap="round"
457 ></path>
458 </svg>
459 </div>
460 </div>
461 <div class="code-editor" ref={editorDiv}></div>
462 <div class="button-row">
463 {props.kataSection ? null : (
464 <>
465 <span>Profile</span>
466 <select value={profile} onChange={profileChanged}>
467 <option value="unrestricted">Unrestricted</option>
468 <option value="adaptive_rif">Adaptive RIF</option>
469 <option value="adaptive_ri">Adaptive RI</option>
470 <option value="base">Base</option>
471 </select>
472 </>
473 )}
474 {props.showExpr ? (
475 <>
476 <span>Start</span>
477 <input
478 style="width: 160px"
479 value={runExpr}
480 onChange={runExprChanged}
481 />
482 </>
483 ) : null}
484 {props.showShots ? (
485 <>
486 <span>Shots</span>
487 <input
488 style="width: 88px;"
489 type="number"
490 value={shotCount || 100}
491 max="1000"
492 min="1"
493 onChange={shotCountChanged}
494 />
495 </>
496 ) : null}
497 <button
498 class="main-button"
499 onClick={onRun}
500 disabled={hasCheckErrors || props.compilerState === "busy"}
501 >
502 Run
503 </button>
504 <button
505 class="main-button"
506 onClick={props.onRestartCompiler}
507 disabled={props.compilerState === "idle"}
508 >
509 Cancel
510 </button>
511 </div>
512 <div class="diag-list">
513 {errors.map((err) => (
514 <div
515 className={`diag-row ${err.severity === monaco.MarkerSeverity.Error ? "error-row" : "warning-row"}`}
516 >
517 <span>{err.location}: </span>
518 <span>{err.msg[0]}</span>
519 {err.msg.length > 1 ? (
520 <div class="diag-help">{err.msg[1]}</div>
521 ) : null}
522 </div>
523 ))}
524 </div>
525 </div>
526 );
527}
528