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/main.tsx

537lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4// Use esbuild to bundle and copy the CSS files to the output directory.
5import "modern-normalize/modern-normalize.css";
6import "./main.css";
7
8import { render } from "preact";
9
10import type {
11 CompilerState,
12 VSDiagnostic,
13 LogLevel,
14 ILanguageService,
15} from "qsharp-lang";
16
17import {
18 QscEventTarget,
19 getCompilerWorker,
20 loadWasmModule,
21 log,
22 samples,
23 getLanguageServiceWorker,
24} from "qsharp-lang";
25
26// The playground Katas viewer uses the Markdown version of the katas
27import { Kata, getAllKatas } from "qsharp-lang/katas-md";
28
29import { Nav } from "./nav.js";
30import { Editor } from "./editor.js";
31import { OutputTabs } from "./tabs.js";
32import { useEffect, useState } from "preact/hooks";
33import { Kata as Katas } from "./kata.js";
34import {
35 DocumentationDisplay,
36 getNamespaces,
37 processDocumentFiles,
38} from "./docs.js";
39import {
40 compressedBase64ToCode,
41 lsRangeToMonacoRange,
42 lsToMonacoWorkspaceEdit,
43 monacoPositionToLsPosition,
44 monacoRangetoLsRange,
45} from "./utils.js";
46
47// Set up the Markdown renderer with KaTeX support
48import mk from "@vscode/markdown-it-katex";
49import markdownIt from "markdown-it";
50import { setRenderer } from "qsharp-lang/ux";
51
52const md = markdownIt("commonmark");
53md.use((mk as any).default, {
54 enableMathBlockInHtml: true,
55 enableMathInlineInHtml: true,
56}); // Not sure why it's not using the default export automatically :-/
57setRenderer((input: string) => md.render(input));
58
59export type ActiveTab =
60 | "results-tab"
61 | "ast-tab"
62 | "hir-tab"
63 | "rir-tab"
64 | "qir-tab";
65
66const basePath = (window as any).qscBasePath || "";
67const monacoPath = basePath + "libs/monaco/vs";
68const modulePath = basePath + "libs/qsharp/qsc_wasm_bg.wasm";
69const compilerWorkerPath = basePath + "libs/compiler-worker.js";
70const languageServiceWorkerPath = basePath + "libs/language-service-worker.js";
71
72function telemetryHandler({ id, data }: { id: string; data?: any }) {
73 // NOTE: This is for demo purposes. Wire up to the real telemetry library.
74 console.log(`Received telemetry event: "%s" with payload: %o`, id, data);
75}
76
77function createCompiler(onStateChange: (val: CompilerState) => void) {
78 log.info("In createCompiler");
79 const compiler = getCompilerWorker(compilerWorkerPath);
80 compiler.onstatechange = onStateChange;
81 return compiler;
82}
83
84function App(props: { katas: Kata[]; linkedCode?: string }) {
85 const [compilerState, setCompilerState] = useState<CompilerState>("idle");
86 const [compiler, setCompiler] = useState(() =>
87 createCompiler(setCompilerState),
88 );
89
90 const [compiler_worker_factory] = useState(() => {
91 const compiler_worker_factory = () => getCompilerWorker(compilerWorkerPath);
92 return compiler_worker_factory;
93 });
94
95 const [evtTarget] = useState(() => new QscEventTarget(true));
96
97 const [languageService] = useState(() => {
98 const languageService = getLanguageServiceWorker(languageServiceWorkerPath);
99 registerMonacoLanguageServiceProviders(languageService);
100 return languageService;
101 });
102
103 const [currentNavItem, setCurrentNavItem] = useState(
104 props.linkedCode ? "linked" : "sample-Minimal",
105 );
106 const [shotError, setShotError] = useState<VSDiagnostic | undefined>(
107 undefined,
108 );
109
110 const [ast, setAst] = useState<string>("");
111 const [hir, setHir] = useState<string>("");
112 const [rir, setRir] = useState<string[]>(["", ""]);
113 const [qir, setQir] = useState<string>("");
114 const [activeTab, setActiveTab] = useState<ActiveTab>("results-tab");
115
116 const onRestartCompiler = () => {
117 compiler.terminate();
118 const newCompiler = createCompiler(setCompilerState);
119 setCompiler(newCompiler);
120 setCompilerState("idle");
121 };
122
123 const kataTitles = props.katas.map((elem) => elem.title);
124 const sampleTitles = samples.map((sample) => sample.title);
125
126 const [documentation, setDocumentation] = useState<
127 Map<string, string> | undefined
128 >(undefined);
129 useEffect(() => {
130 createDocumentation();
131 }, []);
132 async function createDocumentation() {
133 const docFiles = await compiler.getDocumentation();
134 setDocumentation(processDocumentFiles(docFiles));
135 }
136
137 const sampleCode =
138 samples.find((sample) => "sample-" + sample.title === currentNavItem)
139 ?.code || props.linkedCode;
140
141 const defaultShots =
142 samples.find((sample) => sample.title === currentNavItem)?.shots || 100;
143
144 const activeKata = kataTitles.includes(currentNavItem)
145 ? props.katas.find((kata) => kata.title === currentNavItem)
146 : undefined;
147
148 function onNavItemSelected(name: string) {
149 // If there was a ?code link on the URL before, clear it out
150 const newURL = new URL(window.location.href);
151 if (newURL.searchParams.get("code")) {
152 newURL.searchParams.delete("code");
153 newURL.searchParams.delete("profile");
154 window.history.pushState({}, "", newURL.toString());
155 props.linkedCode = undefined;
156 }
157 setCurrentNavItem(name);
158 }
159
160 return (
161 <>
162 <header class="page-header">Q# playground</header>
163 <Nav
164 selected={currentNavItem}
165 navSelected={onNavItemSelected}
166 katas={kataTitles}
167 samples={sampleTitles}
168 namespaces={getNamespaces(documentation)}
169 ></Nav>
170 {sampleCode ? (
171 <>
172 <Editor
173 code={sampleCode}
174 compiler={compiler}
175 compiler_worker_factory={compiler_worker_factory}
176 compilerState={compilerState}
177 onRestartCompiler={onRestartCompiler}
178 evtTarget={evtTarget}
179 defaultShots={defaultShots}
180 showShots={true}
181 showExpr={true}
182 shotError={shotError}
183 setAst={setAst}
184 setHir={setHir}
185 setRir={setRir}
186 setQir={setQir}
187 activeTab={activeTab}
188 languageService={languageService}
189 ></Editor>
190 <OutputTabs
191 evtTarget={evtTarget}
192 showPanel={true}
193 onShotError={(diag?: VSDiagnostic) => setShotError(diag)}
194 ast={ast}
195 hir={hir}
196 rir={rir}
197 qir={qir}
198 activeTab={activeTab}
199 setActiveTab={setActiveTab}
200 ></OutputTabs>
201 </>
202 ) : activeKata ? (
203 <Katas
204 kata={activeKata!}
205 compiler={compiler}
206 compiler_worker_factory={compiler_worker_factory}
207 compilerState={compilerState}
208 onRestartCompiler={onRestartCompiler}
209 languageService={languageService}
210 ></Katas>
211 ) : (
212 <DocumentationDisplay
213 currentNamespace={currentNavItem}
214 documentation={documentation}
215 ></DocumentationDisplay>
216 )}
217 <div id="popup"></div>
218 </>
219 );
220}
221
222// Called once Monaco is ready
223async function loaded() {
224 // Configure any logging as early as possible
225 const logLevelUri = new URLSearchParams(window.location.search).get(
226 "logLevel",
227 );
228 if (logLevelUri) {
229 log.setLogLevel(logLevelUri as LogLevel);
230 } else {
231 log.setLogLevel("error");
232 }
233 log.setTelemetryCollector(telemetryHandler);
234
235 await loadWasmModule(modulePath);
236
237 const katas = await getAllKatas({ includeUnpublished: true });
238
239 // If URL is a sharing link, populate the editor with the code from the link.
240 // Otherwise, populate with sample code.
241 let linkedCode: string | undefined;
242 const paramCode = new URLSearchParams(window.location.search).get("code");
243 if (paramCode) {
244 try {
245 const base64code = decodeURIComponent(paramCode);
246 linkedCode = await compressedBase64ToCode(base64code);
247 } catch {
248 linkedCode = "// Unable to decode the code in the URL\n";
249 }
250 }
251
252 render(<App katas={katas} linkedCode={linkedCode}></App>, document.body);
253}
254
255function registerMonacoLanguageServiceProviders(
256 languageService: ILanguageService,
257) {
258 monaco.languages.registerCompletionItemProvider("qsharp", {
259 // @ts-expect-error - Monaco's types expect range to be defined,
260 // but it's actually optional and the default behavior is better
261 provideCompletionItems: async (
262 model: monaco.editor.ITextModel,
263 position: monaco.Position,
264 ) => {
265 const completions = await languageService.getCompletions(
266 model.uri.toString(),
267 monacoPositionToLsPosition(position),
268 );
269 return {
270 suggestions: completions.items.map((i) => {
271 let kind;
272 switch (i.kind) {
273 case "function":
274 kind = monaco.languages.CompletionItemKind.Function;
275 break;
276 case "interface":
277 kind = monaco.languages.CompletionItemKind.Interface;
278 break;
279 case "keyword":
280 kind = monaco.languages.CompletionItemKind.Keyword;
281 break;
282 case "variable":
283 kind = monaco.languages.CompletionItemKind.Variable;
284 break;
285 case "typeParameter":
286 kind = monaco.languages.CompletionItemKind.TypeParameter;
287 break;
288 case "module":
289 kind = monaco.languages.CompletionItemKind.Module;
290 break;
291 case "property":
292 kind = monaco.languages.CompletionItemKind.Property;
293 break;
294 case "field":
295 kind = monaco.languages.CompletionItemKind.Field;
296 break;
297 case "class":
298 kind = monaco.languages.CompletionItemKind.Class;
299 break;
300 }
301 return {
302 label: i.label,
303 kind: kind,
304 insertText: i.label,
305 sortText: i.sortText,
306 detail: i.detail,
307 additionalTextEdits: i.additionalTextEdits?.map((edit) => {
308 const range = edit.range;
309 const textEdit: monaco.languages.TextEdit = {
310 range: lsRangeToMonacoRange(range),
311 text: edit.newText,
312 };
313 return textEdit;
314 }),
315 range: undefined,
316 };
317 }),
318 };
319 },
320 // Trigger characters should be kept in sync with the ones in `vscode/src/extension.ts`
321 triggerCharacters: ["@", "."],
322 });
323
324 monaco.languages.registerHoverProvider("qsharp", {
325 provideHover: async (
326 model: monaco.editor.ITextModel,
327 position: monaco.Position,
328 ) => {
329 const hover = await languageService.getHover(
330 model.uri.toString(),
331 monacoPositionToLsPosition(position),
332 );
333
334 if (hover) {
335 return {
336 contents: [{ value: hover.contents }],
337 range: lsRangeToMonacoRange(hover.span),
338 };
339 }
340 return null;
341 },
342 });
343
344 monaco.languages.registerDefinitionProvider("qsharp", {
345 provideDefinition: async (
346 model: monaco.editor.ITextModel,
347 position: monaco.Position,
348 ) => {
349 const definition = await languageService.getDefinition(
350 model.uri.toString(),
351 monacoPositionToLsPosition(position),
352 );
353 if (!definition) return null;
354 const uri = monaco.Uri.parse(definition.source);
355 if (uri.toString() !== model.uri.toString()) return null;
356 return {
357 uri,
358 range: lsRangeToMonacoRange(definition.span),
359 };
360 },
361 });
362
363 monaco.languages.registerReferenceProvider("qsharp", {
364 provideReferences: async (
365 model: monaco.editor.ITextModel,
366 position: monaco.Position,
367 context: monaco.languages.ReferenceContext,
368 ) => {
369 const lsReferences = await languageService.getReferences(
370 model.uri.toString(),
371 monacoPositionToLsPosition(position),
372 context.includeDeclaration,
373 );
374 if (!lsReferences) return [];
375 const references: monaco.languages.Location[] = [];
376 for (const reference of lsReferences) {
377 const uri = monaco.Uri.parse(reference.source);
378 // the playground doesn't support sources other than the current source
379 if (uri.toString() == model.uri.toString()) {
380 references.push({
381 uri,
382 range: lsRangeToMonacoRange(reference.span),
383 });
384 }
385 }
386 return references;
387 },
388 });
389
390 monaco.languages.registerSignatureHelpProvider("qsharp", {
391 signatureHelpTriggerCharacters: ["(", ","],
392 provideSignatureHelp: async (
393 model: monaco.editor.ITextModel,
394 position: monaco.Position,
395 ) => {
396 const sigHelpLs = await languageService.getSignatureHelp(
397 model.uri.toString(),
398 monacoPositionToLsPosition(position),
399 );
400 if (!sigHelpLs) return null;
401 return {
402 dispose: () => {},
403 value: {
404 activeParameter: sigHelpLs.activeParameter,
405 activeSignature: sigHelpLs.activeSignature,
406 signatures: sigHelpLs.signatures.map((sig) => {
407 return {
408 label: sig.label,
409 documentation: {
410 value: sig.documentation,
411 } as monaco.IMarkdownString,
412 parameters: sig.parameters.map((param) => {
413 return {
414 label: param.label,
415 documentation: {
416 value: param.documentation,
417 } as monaco.IMarkdownString,
418 };
419 }),
420 };
421 }),
422 },
423 };
424 },
425 });
426
427 monaco.languages.registerRenameProvider("qsharp", {
428 provideRenameEdits: async (
429 model: monaco.editor.ITextModel,
430 position: monaco.Position,
431 newName: string,
432 ) => {
433 const rename = await languageService.getRename(
434 model.uri.toString(),
435 monacoPositionToLsPosition(position),
436 newName,
437 );
438 if (!rename) return null;
439 return lsToMonacoWorkspaceEdit(rename);
440 },
441 resolveRenameLocation: async (
442 model: monaco.editor.ITextModel,
443 position: monaco.Position,
444 ) => {
445 const prepareRename = await languageService.prepareRename(
446 model.uri.toString(),
447 monacoPositionToLsPosition(position),
448 );
449 if (prepareRename) {
450 return {
451 range: lsRangeToMonacoRange(prepareRename.range),
452 text: prepareRename.newText,
453 } as monaco.languages.RenameLocation;
454 } else {
455 return {
456 rejectReason: "Rename is unavailable at this location.",
457 } as monaco.languages.RenameLocation & monaco.languages.Rejection;
458 }
459 },
460 });
461
462 async function getFormatChanges(
463 model: monaco.editor.ITextModel,
464 range?: monaco.Range,
465 ) {
466 const lsEdits = await languageService.getFormatChanges(
467 model.uri.toString(),
468 );
469 if (!lsEdits) {
470 return [];
471 }
472 let edits = lsEdits.map((edit) => {
473 return {
474 range: lsRangeToMonacoRange(edit.range),
475 text: edit.newText,
476 } as monaco.languages.TextEdit;
477 });
478 if (range) {
479 edits = edits.filter((e) => monaco.Range.areIntersecting(range, e.range));
480 }
481 return edits;
482 }
483
484 monaco.languages.registerDocumentFormattingEditProvider("qsharp", {
485 provideDocumentFormattingEdits: async (model: monaco.editor.ITextModel) => {
486 return getFormatChanges(model);
487 },
488 });
489
490 monaco.languages.registerDocumentRangeFormattingEditProvider("qsharp", {
491 provideDocumentRangeFormattingEdits: async (
492 model: monaco.editor.ITextModel,
493 range: monaco.Range,
494 ) => {
495 return getFormatChanges(model, range);
496 },
497 });
498
499 monaco.languages.registerCodeActionProvider("qsharp", {
500 provideCodeActions: async (
501 model: monaco.editor.ITextModel,
502 range: monaco.Range,
503 ) => {
504 const lsCodeActions = await languageService.getCodeActions(
505 model.uri.toString(),
506 monacoRangetoLsRange(range),
507 );
508
509 const codeActions = lsCodeActions.map((lsCodeAction) => {
510 let edit;
511 if (lsCodeAction.edit) {
512 edit = lsToMonacoWorkspaceEdit(lsCodeAction.edit);
513 }
514
515 return {
516 title: lsCodeAction.title,
517 edit: edit,
518 kind: lsCodeAction.kind,
519 isPreferred: lsCodeAction.isPreferred,
520 } as monaco.languages.CodeAction;
521 });
522
523 return {
524 actions: codeActions,
525 dispose: () => {},
526 } as monaco.languages.CodeActionList;
527 },
528 });
529}
530
531// Monaco provides the 'require' global for loading modules.
532declare const require: {
533 config: (settings: object) => void;
534 (base: string[], onready: () => void): void;
535};
536require.config({ paths: { vs: monacoPath } });
537require(["vs/editor/editor.main"], loaded);
538