// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. /// import { useEffect, useRef, useState } from "preact/hooks"; import { CompilerState, ICompilerWorker, ILanguageServiceWorker, QscEventTarget, VSDiagnostic, log, ProgramConfig, LanguageServiceDiagnosticEvent, getTargetProfileFromEntryPoint, } from "qsharp-lang"; import { Exercise, getExerciseSources } from "qsharp-lang/katas-md"; import { codeToCompressedBase64, lsRangeToMonacoRange } from "./utils.js"; import { ActiveTab } from "./main.js"; import type { KataSection } from "qsharp-lang/katas"; type ErrCollection = { checkDiags: VSDiagnostic[]; shotDiags: VSDiagnostic[]; }; function VSDiagsToMarkers(errors: VSDiagnostic[]): 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; case "hint": severity = monaco.MarkerSeverity.Hint; break; } const marker: monaco.editor.IMarkerData = { ...lsRangeToMonacoRange(err.range), severity, message: err.message, // LSP DiagnosticTag values match monaco's MarkerTag (1 = Unnecessary). tags: err.tags as monaco.MarkerTag[] | undefined, relatedInformation: err.related?.map((r) => { const range = lsRangeToMonacoRange(r.location.span); return { resource: monaco.Uri.parse(r.location.source), message: r.message, ...range, }; }), }; if (err.uri && err.code) { marker.code = { value: err.code, target: monaco.Uri.parse(err.uri), }; } else if (err.code) { marker.code = err.code; } return marker; }); } export function Editor(props: { code: string; compiler: ICompilerWorker; compiler_worker_factory: () => ICompilerWorker; compilerState: CompilerState; defaultShots: number; evtTarget: QscEventTarget; kataSection?: KataSection; onRestartCompiler: () => void; shotError?: VSDiagnostic; showExpr: boolean; showShots: boolean; setAst: (ast: string) => void; setHir: (hir: string) => void; setRir: (rir: string[]) => void; setQir: (qir: string) => void; activeTab: ActiveTab; languageService: ILanguageServiceWorker; language?: "qsharp" | "openqasm"; }) { // The editor is remounted (keyed on language in the parent) whenever the // language changes, so this is stable for the lifetime of the component. const language = props.language ?? "qsharp"; const fileName = language === "openqasm" ? "main.qasm" : "main.qs"; const editor = useRef(null); const errMarks = useRef({ checkDiags: [], shotDiags: [] }); const editorDiv = useRef(null); // Maintain a ref to the latest getAst/getHir functions, as it closes over a bunch of stuff const irRef = useRef(async () => { return; }); const [shotCount, setShotCount] = useState(props.defaultShots); const [runExpr, setRunExpr] = useState(""); const [errors, setErrors] = useState< { location: string; severity: monaco.MarkerSeverity; msg: string[] }[] >([]); const [hasCheckErrors, setHasCheckErrors] = useState(false); function markErrors() { const model = editor.current?.getModel(); if (!model) return; const errs = [ ...errMarks.current.checkDiags, ...errMarks.current.shotDiags, ]; const markers = VSDiagsToMarkers(errs); monaco.editor.setModelMarkers(model, "qsharp", markers); const errList = markers // Hints (e.g. grayed-out excluded code) are shown inline only, not in the // diagnostics list below the editor, mirroring VS Code's Problems view. .filter((err) => err.severity !== monaco.MarkerSeverity.Hint) .map((err) => ({ location: `${fileName}@(${err.startLineNumber},${err.startColumn})`, severity: err.severity, msg: err.message.split("\n\n"), })); setErrors(errList); } irRef.current = async function updateIr() { const code = editor.current?.getValue(); if (code == null) return; const config = { sources: [["code", code]] as [string, string][], languageFeatures: [], profile: (language === "openqasm" ? undefined : await getTargetProfileFromEntryPoint("main.qs", code)) || "adaptive_rif", // Default to adaptive_rif for qir and rir generation projectType: language, }; if (props.activeTab === "ast-tab") { props.setAst(await props.compiler.getAst(config)); return; } if (props.activeTab === "hir-tab") { props.setHir(await props.compiler.getHir(config)); return; } if (props.activeTab === "qir-tab" || props.activeTab === "rir-tab") { // OpenQASM codegen runs through the interpreter and is slower than Q#'s // direct path, so allow more time before terminating the worker. const codeGenTimeout = language === "openqasm" ? 10000 : 1000; // ms let timedOut = false; const compiler = props.compiler_worker_factory(); const compilerTimeout = setTimeout(() => { log.info("Compiler timeout. Terminating worker."); timedOut = true; compiler.terminate(); }, codeGenTimeout); try { if (props.activeTab === "rir-tab") { const ir = await compiler.getRir(config); clearTimeout(compilerTimeout); props.setRir(ir); } else { const ir = await compiler.getQir(config); clearTimeout(compilerTimeout); props.setQir(ir); } } catch (e: any) { if (timedOut) { if (props.activeTab === "rir-tab") { props.setRir(["timed out", "timed out"]); } else { props.setQir("timed out"); } } else { if (props.activeTab === "rir-tab") { props.setRir([e.toString(), e.toString()]); } else { props.setQir(e.toString()); } } } finally { compiler.terminate(); } } }; async function onRun() { const code = editor.current?.getValue(); if (code == null) return; props.evtTarget.clearResults(); const config = { sources: [["code", code]], languageFeatures: [], // The entry-point profile annotation is Q#-specific; skip it for OpenQASM. profile: language === "openqasm" ? undefined : await getTargetProfileFromEntryPoint("main.qs", code), projectType: language, } as ProgramConfig; try { if (props.kataSection?.type === "exercise") { // This is for a kata exercise. Provide the sources that implement the solution verification. const sources = await getExerciseSources(props.kataSection as Exercise); // check uses the unrestricted profile and doesn't do code gen, // so we just pass the sources await props.compiler.checkExerciseSolution( code, sources, props.evtTarget, ); } else { performance.mark("compiler-run-start"); await props.compiler.run(config, runExpr, shotCount, props.evtTarget); const runTimer = performance.measure( "compiler-run", "compiler-run-start", ); log.logTelemetry({ id: "compiler-run", data: { duration: runTimer.duration, codeSize: code.length, shotCount, }, }); } } catch (err) { // This could fail for several reasons, e.g. the run being cancelled. if (err === "terminated") { log.info("Run was terminated"); } else { log.error("Run failed with error: %o", err); } } } useEffect(() => { if (!editorDiv.current) return; const newEditor = monaco.editor.create(editorDiv.current, { minimap: { enabled: false }, lineNumbersMinChars: 3, automaticLayout: true, }); editor.current = newEditor; const srcModel = monaco.editor.getModel( monaco.Uri.parse(props.kataSection?.id ?? fileName), ) ?? monaco.editor.createModel( "", language, monaco.Uri.parse(props.kataSection?.id ?? fileName), ); srcModel.setValue(props.code); newEditor.setModel(srcModel); srcModel.onDidChangeContent(() => irRef.current()); // TODO: If the language service ever changes, this callback // will be invalid as it captures the *original* props.languageService // and not the updated one. Not a problem currently since the language // service is never updated, but not correct either. srcModel.onDidChangeContent(async () => { // Reset the shot errors whenever the document changes. // The markers will be refreshed by the onDiagnostics callback // when the language service finishes checking the document. errMarks.current.shotDiags = []; performance.mark("update-document-start"); await props.languageService.updateDocument( srcModel.uri.toString(), srcModel.getVersionId(), srcModel.getValue(), language, ); const measure = performance.measure( "update-document", "update-document-start", ); log.info(`updateDocument took ${measure.duration}ms`); }); function onResize() { newEditor.layout(); } // If the browser window resizes, tell the editor to update it's layout window.addEventListener("resize", onResize); return () => { log.info("Disposing a monaco editor"); window.removeEventListener("resize", onResize); props.languageService.closeDocument(srcModel.uri.toString(), language); newEditor.dispose(); }; }, []); useEffect(() => { props.languageService.updateConfiguration({ packageType: props.kataSection ? "lib" : "exe", lints: props.kataSection ? [] : [{ lint: "needlessOperation", level: "warn" }], }); function onDiagnostics(evt: LanguageServiceDiagnosticEvent) { const diagnostics = evt.detail.diagnostics; errMarks.current.checkDiags = diagnostics; markErrors(); setHasCheckErrors( diagnostics.filter((d) => d.severity === "error").length > 0, ); } props.languageService.addEventListener("diagnostics", onDiagnostics); return () => { log.info("Removing diagnostics listener"); props.languageService.removeEventListener("diagnostics", onDiagnostics); }; }, [props.languageService, props.kataSection]); useEffect(() => { const theEditor = editor.current; if (!theEditor) return; theEditor.getModel()?.setValue(props.code); theEditor.revealLineNearTop(1); setShotCount(props.defaultShots); setRunExpr(""); }, [props.code, props.defaultShots]); useEffect(() => { errMarks.current.shotDiags = props.shotError ? [props.shotError] : []; markErrors(); }, [props.shotError]); useEffect(() => { // Whenever the active tab changes, run check again. irRef.current(); }, [props.activeTab]); // On reset, reload the initial code function onReset() { const theEditor = editor.current; if (!theEditor) return; theEditor.getModel()?.setValue(props.code || ""); setShotCount(props.defaultShots); setRunExpr(""); } async function onGetLink(ev: MouseEvent) { const code = editor.current?.getModel()?.getValue(); if (!code) return; let messageText = "Unable to create the link"; try { const encodedCode = await codeToCompressedBase64(code); const escapedCode = encodeURIComponent(encodedCode); // Update or add the current URL parameter 'code' const newURL = new URL(window.location.href); newURL.searchParams.set("code", escapedCode); // Encode the language so shared OpenQASM links reopen in OpenQASM mode. // Q# is the default, so it's left out to keep existing links working. if (language === "openqasm") { newURL.searchParams.set("lang", language); } else { newURL.searchParams.delete("lang"); } // Copy link to clipboard and update url without reloading the page navigator.clipboard.writeText(newURL.toString()); window.history.pushState({}, "", newURL.toString()); messageText = "Link was copied to the clipboard"; } finally { const popup = document.getElementById("popup") as HTMLDivElement; popup.style.display = "block"; popup.innerText = messageText; popup.style.left = `${ev.clientX - 120}px`; popup.style.top = `${ev.clientY - 40}px`; setTimeout(() => { popup.style.display = "none"; }, 2000); } } function shotCountChanged(e: Event) { const target = e.target as HTMLInputElement; setShotCount(parseInt(target.value) || 1); } function runExprChanged(e: Event) { const target = e.target as HTMLInputElement; setRunExpr(target.value); } return ( {fileName} Get a link to this code Reset code to initial state {props.showExpr ? ( <> Start > ) : null} {props.showShots ? ( <> Shots > ) : null} Run Cancel {errors.map((err) => ( {err.location}: {err.msg[0]} {err.msg.length > 1 ? ( {err.msg[1]} ) : null} ))} ); }