microsoft/qdk

Public

mirrored from https://github.com/microsoft/qdkAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
32bcffe86a3647ddd45528ecb551f8feddd749ab

Branches

Tags

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

Clone

HTTPS

Download ZIP

source/vscode/src/webview/docview.tsx

452lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4import { Markdown } from "qsharp-lang/ux";
5import { useEffect, useState } from "preact/hooks";
6
7export interface IDocFile {
8 filename: string;
9 metadata: string;
10 contents: string;
11}
12
13interface ItemDocs {
14 pkg: string;
15 namespace: string;
16 member: string;
17 content: string;
18}
19
20interface PageContents {
21 [name: string]: {
22 onclick: (evt: Event) => void;
23 content: string;
24 anchor: string;
25 };
26}
27
28function GetPageContents(
29 currPath: string,
30 docs: ItemDocs[],
31 setPath: any,
32): PageContents {
33 const contents: PageContents = {};
34
35 if (currPath === "") {
36 // Collect the set of all packages
37 docs.forEach((doc) => {
38 if (!(doc.pkg in contents)) {
39 contents[doc.pkg] = {
40 onclick: () => setPath(doc.pkg),
41 content: "",
42 anchor: "",
43 };
44 }
45 });
46 } else if (currPath.indexOf("/") === -1) {
47 // Render the list of namespaces in the current package
48 docs.forEach((doc) => {
49 if (doc.pkg === currPath) {
50 if (!(doc.namespace in contents)) {
51 contents[doc.namespace] = {
52 onclick: () => setPath(`${currPath}/${doc.namespace}`),
53 content: "",
54 anchor: "",
55 };
56 }
57 }
58 });
59 } else {
60 // Render the list of members in the current namespace
61 docs.forEach((doc) => {
62 if (
63 doc.pkg === currPath.split("/")[0] &&
64 doc.namespace === currPath.split("/")[1]
65 ) {
66 contents[doc.member] = {
67 onclick: (evt: Event) => {
68 evt.preventDefault();
69 scrollToElement(doc.member);
70 },
71 content: doc.content,
72 anchor: doc.member,
73 };
74 }
75 });
76 }
77 return contents;
78}
79
80function scrollToElement(id: string) {
81 const elem = id ? document.getElementById(id) : null;
82 if (!elem) {
83 window.scrollTo({
84 top: 0,
85 behavior: "instant",
86 });
87 } else {
88 const yOffset = -64; // Negative value to offset from the top
89 const yPosition =
90 elem.getBoundingClientRect().top + window.scrollY + yOffset;
91
92 window.scrollTo({
93 top: yPosition,
94 behavior: "instant",
95 });
96 }
97}
98
99interface SearchResult {
100 rank: number;
101 anchor: string;
102 header: string;
103 summary: string;
104}
105
106function getSearchResults(
107 searchText: string,
108 docs: ItemDocs[],
109): SearchResult[] {
110 const results: SearchResult[] = [];
111
112 // Search on member name first, then on namespace name, then on package name, then on content
113 // Prefer earlier matches (e.g. "X" should match "X" before "CX")
114
115 // RegExp groups
116 // 1. is the first header (e.g. 'Foo operation'), group
117 // 2. Is any text after the first header that precedes the next header
118 // 3. Ignore
119 // 4. Is the summary (if present)
120 const summaryRe =
121 /^(?:# )(.*?\n)\s*(.*?)(?=(##+)|$)(## Summary\r?\n.*?(?=(##+)|$))?/s;
122
123 docs.forEach((doc) => {
124 const reMatch = doc.content.match(summaryRe);
125 const summary = (reMatch?.[2] ?? "") + (reMatch?.[4] ?? "");
126 const header = doc.content.match(summaryRe)?.[1] ?? doc.member;
127
128 const lowerText = searchText.toLowerCase();
129 let rank = doc.member.toLowerCase().indexOf(lowerText) + 1;
130 if (rank) {
131 // Matches on larger parts of the name are more important
132 rank += doc.member.length / 100;
133 }
134
135 if (!rank) {
136 rank = (doc.namespace.toLowerCase().indexOf(lowerText) + 1) * 100;
137 if (rank) {
138 rank += doc.namespace.length;
139 }
140 }
141 if (!rank) {
142 rank = (doc.pkg.toLowerCase().indexOf(lowerText) + 1) * 1000;
143 }
144 if (!rank) {
145 rank = (doc.content.toLowerCase().indexOf(lowerText) + 1) * 10000;
146 }
147
148 if (rank) {
149 results.push({
150 rank,
151 anchor: `${doc.pkg}/${doc.namespace}/${doc.member}`,
152 header,
153 summary,
154 });
155 return;
156 }
157 });
158
159 return results.sort((a, b) => a.rank - b.rank);
160}
161
162function DocsPage(props: { fragmentsToRender: ItemDocs[] }) {
163 // currPath is of the format: "<pkg>/<namespace>/<member>", e.g.
164 // "Std/Canon/CCNOT" or "Std/Microsoft.Quantum.Diagnostics/AssertMeasurementEqual" or
165 // "Unsigned/Main/GetInt" or "Main/Particle/Particle". When at the top level, currPath is "".
166
167 const [currPath, setPath] = useState("");
168 const [searchText, setSearchText] = useState("");
169
170 useEffect(() => {
171 // Update the xref links to navigate to the correct member
172 document.querySelectorAll('a[href^="xref:"]').forEach((a) => {
173 const anchor = a as HTMLAnchorElement;
174 const xref = "xref:";
175 let link = anchor.href.slice(xref.length);
176 link = link.replaceAll(".", "/");
177 // Just for Qdk links, we want to strip out the leading "Qdk."
178 if (link.startsWith("Qdk/")) {
179 link = link.slice(4);
180 }
181 a.addEventListener("click", (e) => {
182 e.preventDefault();
183 setPath(link);
184 });
185 });
186
187 // If the member is navigated to, scroll to it after rendering
188 const member = currPath.split("/")[2];
189 scrollToElement(member);
190 }, [currPath]);
191
192 // Skip processing contents if searching
193 const contents =
194 searchText === ""
195 ? GetPageContents(currPath, props.fragmentsToRender, setPath)
196 : {};
197
198 const searchResults =
199 searchText === ""
200 ? []
201 : getSearchResults(searchText, props.fragmentsToRender);
202
203 // Used to bold the text links when hovering
204 function overLi(e: MouseEvent) {
205 (e.target as HTMLElement).style.fontWeight = "600";
206 (e.target as HTMLElement).style.textDecoration = "underline";
207 (e.target as HTMLElement).style.cursor = "pointer";
208 }
209
210 function outLi(e: MouseEvent) {
211 (e.target as HTMLElement).style.fontWeight = "400";
212 (e.target as HTMLElement).style.textDecoration = "none";
213 (e.target as HTMLElement).style.cursor = "default";
214 }
215
216 // Whenever the breadcrumbs are clicked, go up one level
217 function onPathClick() {
218 setSearchText("");
219 if (currPath) {
220 const parts = currPath.split("/");
221 parts.pop();
222 setPath(parts.join("/"));
223 }
224 }
225
226 // Handle the user focusing or updating the search box
227 function onSearchFocus(e: FocusEvent) {
228 e.preventDefault();
229 if (e.target) {
230 const currText = (e.target as HTMLInputElement).value;
231 if (currText) setPath("");
232 setSearchText(currText);
233 }
234 }
235
236 function onSearchInput(e: InputEvent) {
237 e.preventDefault();
238 if (e.target) {
239 setPath("");
240 setSearchText((e.target as HTMLInputElement).value);
241 }
242 }
243
244 function searchResultClicked(anchor: string) {
245 setSearchText("");
246 setPath(anchor);
247 }
248
249 return (
250 <div
251 class="qs-docsPage"
252 style="width: 100%; padding-bottom: 1em; position: relative; background-color: var(--main-background); color: var(--main-color)"
253 >
254 <div
255 class="qs-docsHeader"
256 style="height: 3em; display: flex; justify-content: space-between; align-items: center; margin-top: 0px; padding-top: 1.5em; padding-bottom: 1em; position: fixed; top: 0; width: 95%; background-color: var(--main-background); z-index: 1;"
257 >
258 <div
259 onClick={() => {
260 setSearchText("");
261 setPath("");
262 }}
263 onMouseOver={overLi}
264 onMouseOut={outLi}
265 >
266 <svg
267 style="height: 2.25em; width: 2.25em; margin: 0.25em"
268 viewBox="0 0 100 100"
269 >
270 <g transform="translate(10,10)" fill="#888">
271 <path d="M10,50 l40,-40 l40,40 Z"></path>
272 <path d="M20,49 l0,30 l20,0 l0,-30Z"></path>
273 <path d="M60,49 l0,30 l20,0 l0,-30Z"></path>
274 <path d="M39,49 l0,8 l24,0 l0,-30Z"></path>
275 <path d="M77,40 l0,-20 l-10,0 l0,20"></path>
276 </g>
277 </svg>
278 </div>
279 <div
280 style="flex-grow: 1; font-size: 1.4em; margin-left: 0.5em;"
281 onClick={onPathClick}
282 >
283 {currPath
284 ? currPath.replaceAll("/", " > ")
285 : searchText
286 ? "Search results"
287 : "Q# API documentation"}
288 </div>
289 <div>
290 <input
291 type="text"
292 placeholder="Search..."
293 onFocus={onSearchFocus}
294 onInput={onSearchInput}
295 />
296 </div>
297 <div>
298 <svg
299 style="height: 2.25em; width: 2.25em; margin: 0.25em"
300 viewBox="0 0 100 100"
301 >
302 <g stroke="#888" fill="none" transform="translate(0,10)">
303 <circle stroke-width="6" cx="40" cy="35" r="25"></circle>
304 <path stroke-width="8" d="M52,57 l20,30"></path>
305 </g>
306 </svg>
307 </div>
308 </div>
309 {searchText ? (
310 <div
311 class="qs-searchResults"
312 style="margin: 2em; position: relative; top: 2em;"
313 >
314 {searchResults.map((result) => (
315 <div>
316 <hr />
317 <h1
318 onMouseOver={overLi}
319 onMouseOut={outLi}
320 onClick={() => searchResultClicked(result.anchor)}
321 >
322 {result.header}
323 </h1>
324 <Markdown markdown={result.summary} />
325 </div>
326 ))}
327 </div>
328 ) : (
329 <div
330 class="qs-docsContent"
331 style="margin: 2em; position: relative; top: 2em;"
332 >
333 <div
334 class="qs-index"
335 style="background: var(--vscode-textCodeBlock-background); padding: 0.1em"
336 >
337 <p style="font-size: 1.1em; font-weight: 600; margin: 0.8em;">
338 {currPath === ""
339 ? "Packages"
340 : currPath.indexOf("/") === -1
341 ? "Namespaces"
342 : "Members"}
343 </p>
344 <ul>
345 {Object.keys(contents).map((key) => (
346 <li
347 onClick={contents[key].onclick}
348 onMouseOver={overLi}
349 onMouseOut={outLi}
350 >
351 {key}
352 </li>
353 ))}
354 </ul>
355 </div>
356
357 {Object.keys(contents).map((key) =>
358 !contents[key].content ? null : (
359 <div id={contents[key].anchor} style="margin-top: 12px;">
360 <Markdown markdown={contents[key].content} />
361 <hr />
362 </div>
363 ),
364 )}
365 </div>
366 )}
367 </div>
368 );
369}
370
371export function DocumentationView(props: {
372 fragmentsToRender: IDocFile[];
373 projectName: string;
374}) {
375 const docs: ItemDocs[] = [];
376
377 props.fragmentsToRender.forEach((doc) => {
378 if (!doc.metadata) {
379 return;
380 }
381 const pkg = doc.metadata.match(/^(?:qsharp\.package: )(.*)$/m)?.[1];
382 const namespace = doc.metadata.match(/^(?:qsharp\.namespace: )(.*)$/m)?.[1];
383 const member = doc.metadata.match(/^(?:qsharp\.name: )(.*)$/m)?.[1];
384
385 if (pkg && namespace && member) {
386 docs.push({
387 pkg:
388 pkg === "__Core__" || pkg === "__Std__"
389 ? "Std"
390 : pkg === "__Main__"
391 ? props.projectName
392 : pkg,
393 // For some reason Std namespaces include the package name. Remove it.
394 namespace: namespace.startsWith("Std.")
395 ? namespace.slice(4)
396 : namespace,
397 member,
398 content: doc.contents,
399 });
400 }
401 });
402
403 docs.sort((a, b) => {
404 if (a.pkg != b.pkg) {
405 // Sorted by __Main__, then reference packages, then __Std__ (which includes __Core__)
406 if (a.pkg === props.projectName || b.pkg === "Std") {
407 return -1;
408 } else if (b.pkg === props.projectName || a.pkg === "Std") {
409 return 1;
410 } else {
411 return a.pkg.localeCompare(b.pkg);
412 }
413 } else if (a.namespace != b.namespace) {
414 // Main namespace comes first and "Microsoft.Quantum.*" comes last
415 if (
416 a.namespace === props.projectName ||
417 b.namespace.startsWith("Microsoft.Quantum")
418 ) {
419 return -1;
420 } else if (
421 b.namespace === props.projectName ||
422 a.namespace.startsWith("Microsoft.Quantum")
423 ) {
424 return 1;
425 } else {
426 return a.namespace.localeCompare(b.namespace);
427 }
428 } else {
429 return a.member.localeCompare(b.member);
430 }
431 });
432
433 const style = document.createElement("style");
434 style.textContent = `
435body {
436 background-color: var(--main-background);
437 margin: 0;
438 padding: 0;
439}
440
441.markdown-body {
442 background-color: var(--main-background);
443}
444
445.markdown-body code {
446 color: var(--main-color);
447}
448`;
449 document.head.appendChild(style);
450
451 return <DocsPage fragmentsToRender={docs} />;
452}
453