microsoft/qdk
Publicmirrored fromhttps://github.com/microsoft/qdkAvailable
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 | |
| 6 | import { useEffect, useRef, useState } from "preact/hooks"; |
| 7 | import { |
| 8 | CompilerState, |
| 9 | ICompilerWorker, |
| 10 | ILanguageServiceWorker, |
| 11 | QscEventTarget, |
| 12 | VSDiagnostic, |
| 13 | log, |
| 14 | ProgramConfig, |
| 15 | LanguageServiceDiagnosticEvent, |
| 16 | getTargetProfileFromEntryPoint, |
| 17 | } from "qsharp-lang"; |
| 18 | import { Exercise, getExerciseSources } from "qsharp-lang/katas-md"; |
| 19 | import { codeToCompressedBase64, lsRangeToMonacoRange } from "./utils.js"; |
| 20 | import { ActiveTab } from "./main.js"; |
| 21 | |
| 22 | import type { KataSection } from "qsharp-lang/katas"; |
| 23 | |
| 24 | type ErrCollection = { |
| 25 | checkDiags: VSDiagnostic[]; |
| 26 | shotDiags: VSDiagnostic[]; |
| 27 | }; |
| 28 | |
| 29 | function 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 | export 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 | |