diff --git a/compiler/qsc_doc_gen/src/generate_docs.rs b/compiler/qsc_doc_gen/src/generate_docs.rs index 311c2c7071..48a22ef3dd 100644 --- a/compiler/qsc_doc_gen/src/generate_docs.rs +++ b/compiler/qsc_doc_gen/src/generate_docs.rs @@ -7,8 +7,9 @@ mod tests; use crate::display::{increase_header_level, parse_doc_for_summary}; use crate::display::{CodeDisplay, Lookup}; use qsc_ast::ast; +use qsc_data_structures::language_features::LanguageFeatures; use qsc_data_structures::target::TargetCapabilityFlags; -use qsc_frontend::compile::{self, PackageStore}; +use qsc_frontend::compile::{self, compile, PackageStore, SourceMap}; use qsc_frontend::resolve; use qsc_hir::hir::{CallableKind, Item, ItemKind, Package, PackageId, Visibility}; use qsc_hir::{hir, ty}; @@ -27,10 +28,34 @@ struct Compilation { } impl Compilation { - /// Creates a new `Compilation` by compiling sources. - pub(crate) fn new() -> Self { + /// Creates a new `Compilation` by compiling standard library + /// and additional sources. + pub(crate) fn new( + additional_sources: Option, + capabilities: Option, + language_features: Option, + ) -> Self { let mut package_store = PackageStore::new(compile::core()); - package_store.insert(compile::std(&package_store, TargetCapabilityFlags::all())); + let actual_capabilities = capabilities.unwrap_or_default(); + let std_unit = compile::std(&package_store, actual_capabilities); + let std_package_id = package_store.insert(std_unit); + + if let Some(sources) = additional_sources { + let actual_language_features = language_features.unwrap_or_default(); + + let unit = compile( + &package_store, + &[std_package_id], + sources, + actual_capabilities, + actual_language_features, + ); + // We ignore errors here (unit.errors vector) and use whatever + // documentation we can produce. In future we may consider + // displaying the fact of error presence on documentation page. + + package_store.insert(unit); + } Self { package_store } } @@ -103,9 +128,15 @@ impl Lookup for Compilation { } } +/// Generates and returns documentation files for the standard library +/// and additional sources (if specified.) #[must_use] -pub fn generate_docs() -> Files { - let compilation = Compilation::new(); +pub fn generate_docs( + additional_sources: Option, + capabilities: Option, + language_features: Option, +) -> Files { + let compilation = Compilation::new(additional_sources, capabilities, language_features); let mut files: Files = vec![]; let display = &CodeDisplay { diff --git a/compiler/qsc_doc_gen/src/generate_docs/tests.rs b/compiler/qsc_doc_gen/src/generate_docs/tests.rs index b24f920e27..928e00cb30 100644 --- a/compiler/qsc_doc_gen/src/generate_docs/tests.rs +++ b/compiler/qsc_doc_gen/src/generate_docs/tests.rs @@ -8,7 +8,7 @@ use expect_test::expect; #[test] fn docs_generation() { - let files = generate_docs(); + let files = generate_docs(None, None, None); let (_, metadata, contents) = files .iter() .find(|(file_name, _, _)| &**file_name == "Microsoft.Quantum.Core/Length.md") diff --git a/npm/qsharp/src/compiler/compiler.ts b/npm/qsharp/src/compiler/compiler.ts index 8e06c3ec06..0775d0bb8c 100644 --- a/npm/qsharp/src/compiler/compiler.ts +++ b/npm/qsharp/src/compiler/compiler.ts @@ -93,7 +93,11 @@ export interface ICompiler { operation?: IOperationInfo, ): Promise; - getDocumentation(): Promise; + getDocumentation( + additionalSources?: [string, string][], + targetProfile?: string, + languageFeatures?: string[], + ): Promise; checkExerciseSolution( userCode: string, @@ -273,8 +277,20 @@ export class Compiler implements ICompiler { ); } - async getDocumentation(): Promise { - return this.wasm.generate_docs(); + // Returns all autogenerated documentation files for the standard library + // and loaded project (if requested). This include file names and metadata, + // including specially formatted table of content file. + async getDocumentation( + additionalSources?: [string, string][], + targetProfile?: string, + languageFeatures?: string[], + ): Promise { + // Get documentation from wasm layer + return this.wasm.generate_docs( + additionalSources, + targetProfile, + languageFeatures, + ); } async checkExerciseSolution( diff --git a/playground/build.js b/playground/build.js index 4329e570f0..e70a24e7c4 100644 --- a/playground/build.js +++ b/playground/build.js @@ -58,7 +58,7 @@ function copyLibs() { mkdirSync(monacoDest, { recursive: true }); cpSync(monacoBase, monacoDest, { recursive: true }); - copyKatex(join(thisDir, "public/libs/katex"), true); + copyKatex(join(thisDir, "public/libs/katex")); copyWasmToPlayground(); } diff --git a/playground/public/index.html b/playground/public/index.html index 62ffcd0197..165b4cb347 100644 --- a/playground/public/index.html +++ b/playground/public/index.html @@ -16,7 +16,7 @@ rel="icon" href="data:image/gif;base64,R0lGODlhEAAQAAAAACwAAAAAEAAQAAACDvAxdbn9YZSTVntx1qcAADs=" /> - + Q# playground diff --git a/samples/algorithms/BellState.qs b/samples/algorithms/BellState.qs index c6ae047991..73661c5419 100644 --- a/samples/algorithms/BellState.qs +++ b/samples/algorithms/BellState.qs @@ -35,23 +35,31 @@ namespace Sample { return measurements; } + /// # Summary + /// Prepares |Φ+⟩ = (|00⟩+|11⟩)/√2 state assuming `register` is in |00⟩ state. operation PreparePhiPlus(register : Qubit[]) : Unit { H(register[0]); // |+0〉 CNOT(register[0], register[1]); // 1/sqrt(2)(|00〉 + |11〉) } + /// # Summary + /// Prepares |Φ−⟩ = (|00⟩-|11⟩)/√2 state assuming `register` is in |00⟩ state. operation PreparePhiMinus(register : Qubit[]) : Unit { H(register[0]); // |+0〉 Z(register[0]); // |-0〉 CNOT(register[0], register[1]); // 1/sqrt(2)(|00〉 - |11〉) } + /// # Summary + /// Prepares |Ψ+⟩ = (|01⟩+|10⟩)/√2 state assuming `register` is in |00⟩ state. operation PreparePsiPlus(register : Qubit[]) : Unit { H(register[0]); // |+0〉 X(register[1]); // |+1〉 CNOT(register[0], register[1]); // 1/sqrt(2)(|01〉 + |10〉) } + /// # Summary + /// Prepares |Ψ−⟩ = (|01⟩-|10⟩)/√2 state assuming `register` is in |00⟩ state. operation PreparePsiMinus(register : Qubit[]) : Unit { H(register[0]); // |+0〉 Z(register[0]); // |-0〉 diff --git a/vscode/build.mjs b/vscode/build.mjs index 7ce44728e6..8357ed965f 100644 --- a/vscode/build.mjs +++ b/vscode/build.mjs @@ -64,9 +64,8 @@ export function copyWasmToVsCode() { /** * * @param {string} [destDir] - * @param {boolean} [useLightTheme] */ -export function copyKatex(destDir, useLightTheme) { +export function copyKatex(destDir) { let katexBase = join(libsDir, `katex/dist`); let katexDest = destDir ?? join(thisDir, `out/katex`); @@ -78,12 +77,13 @@ export function copyKatex(destDir, useLightTheme) { ); // Also copy the GitHub markdown CSS - const cssFileName = useLightTheme - ? "github-markdown-light.css" - : "github-markdown.css"; copyFileSync( - join(libsDir, `github-markdown-css/${cssFileName}`), - join(katexDest, "github-markdown.css"), + join(libsDir, `github-markdown-css/github-markdown-light.css`), + join(katexDest, "github-markdown-light.css"), + ); + copyFileSync( + join(libsDir, `github-markdown-css/github-markdown-dark.css`), + join(katexDest, "github-markdown-dark.css"), ); const fontsDir = join(katexBase, "fonts"); diff --git a/vscode/package.json b/vscode/package.json index 7b25a2ebd0..78ebc7644b 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -199,6 +199,10 @@ "command": "qsharp-vscode.showCircuit", "when": "resourceLangId == qsharp" }, + { + "command": "qsharp-vscode.showDocumentation", + "when": "resourceLangId == qsharp" + }, { "command": "qsharp-vscode.setTargetProfile", "when": "resourceLangId == qsharp" @@ -316,6 +320,11 @@ "title": "Show circuit", "category": "Q#" }, + { + "command": "qsharp-vscode.showDocumentation", + "title": "Show API documentation", + "category": "Q#" + }, { "command": "qsharp-vscode.workspacesRefresh", "category": "Q#", diff --git a/vscode/src/documentation.ts b/vscode/src/documentation.ts new file mode 100644 index 0000000000..d496f86cac --- /dev/null +++ b/vscode/src/documentation.ts @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { getCompilerWorker } from "qsharp-lang"; +import { isQsharpDocument } from "./common"; +import { getTarget } from "./config"; +import { Uri, window } from "vscode"; +import { loadProject } from "./projectSystem"; +import { sendMessageToPanel } from "./webviewPanel"; + +export async function showDocumentationCommand(extensionUri: Uri) { + const editor = window.activeTextEditor; + if (!editor || !isQsharpDocument(editor.document)) { + throw new Error("The currently active window is not a Q# file"); + } + + // Reveal panel and show 'Loading...' for immediate feedback. + sendMessageToPanel( + "documentation", // This is needed to route the message to the proper panel + true, // Reveal panel + null, // With no message + ); + + const docUri = editor.document.uri; + const program = await loadProject(docUri); + const targetProfile = getTarget(); + + // Get API documentation from compiler. + const compilerWorkerScriptPath = Uri.joinPath( + extensionUri, + "./out/compilerWorker.js", + ).toString(); + const worker = getCompilerWorker(compilerWorkerScriptPath); + const docFiles = await worker.getDocumentation( + program.sources, + targetProfile, + program.languageFeatures, + ); + + const documentation: string[] = []; + for (const file of docFiles) { + // Some files may contain information other than documentation + // For example, table of content is a separate file in a special format + // We check presence of qsharp.name in metadata to make sure we take + // only files that contain documentation from some qsharp object. + if (file.metadata.indexOf("qsharp.name:") >= 0) { + documentation.push(file.contents); + } + } + + const message = { + command: "showDocumentationCommand", // This is handled in webview.tsx onMessage + fragmentsToRender: documentation, + }; + + sendMessageToPanel( + "documentation", // This is needed to route the message to the proper panel + true, // Reveal panel + message, // And ask it to display documentation + ); +} diff --git a/vscode/src/webview/docview.tsx b/vscode/src/webview/docview.tsx new file mode 100644 index 0000000000..94627a16be --- /dev/null +++ b/vscode/src/webview/docview.tsx @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Markdown } from "qsharp-lang/ux"; + +export function DocumentationView(props: { fragmentsToRender: string[] }) { + // Concatenate all documentation. + // The following adds an empty line and a horizontal line + // between documentation for different functions. + // We may consider filtering of fragments later. + const contentToRender = props.fragmentsToRender.join("
\n\n---\n\n"); + + return ; +} diff --git a/vscode/src/webview/webview.tsx b/vscode/src/webview/webview.tsx index 9e2d78e809..786c4bf6d3 100644 --- a/vscode/src/webview/webview.tsx +++ b/vscode/src/webview/webview.tsx @@ -15,12 +15,13 @@ import { type ReData, } from "qsharp-lang/ux"; import { HelpPage } from "./help"; +import { DocumentationView } from "./docview"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - there are no types for this import mk from "@vscode/markdown-it-katex"; import markdownIt from "markdown-it"; -const md = markdownIt("commonmark", { html: true, breaks: true }); +const md = markdownIt("commonmark"); md.use(mk, { enableMathBlockInHtml: true, enableMathInlineInHtml: true, @@ -49,19 +50,78 @@ type CircuitState = { props: CircuitProps; }; +type DocumentationState = { + viewType: "documentation"; + fragmentsToRender: string[]; +}; + type State = | { viewType: "loading" } | { viewType: "help" } | HistogramState | EstimatesState - | CircuitState; + | CircuitState + | DocumentationState; const loadingState: State = { viewType: "loading" }; const helpState: State = { viewType: "help" }; let state: State = loadingState; +const themeAttribute = "data-vscode-theme-kind"; + +function updateGitHubTheme() { + let isDark = true; + + const themeType = document.body.getAttribute(themeAttribute); + + switch (themeType) { + case "vscode-light": + case "vscode-high-contrast-light": + isDark = false; + break; + default: + isDark = true; + } + + // Update the stylesheet href + document.head.querySelectorAll("link").forEach((el) => { + const ref = el.getAttribute("href"); + if (ref && ref.includes("github-markdown")) { + const newVal = ref.replace( + /(dark\.css)|(light\.css)/, + isDark ? "dark.css" : "light.css", + ); + el.setAttribute("href", newVal); + } + }); +} + +function setThemeStylesheet() { + // We need to add the right Markdown style-sheet for the theme. + + // For VS Code, there will be an attribute on the body called + // "data-vscode-theme-kind" that is "vscode-light" or "vscode-high-contrast-light" + // for light themes, else assume dark (will be "vscode-dark" or "vscode-high-contrast"). + + // Use a [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) + // to detect changes to the theme attribute. + const callback = (mutations: MutationRecord[]) => { + for (const mutation of mutations) { + if (mutation.attributeName === themeAttribute) { + updateGitHubTheme(); + } + } + }; + const observer = new MutationObserver(callback); + observer.observe(document.body, { attributeFilter: [themeAttribute] }); + + // Run it once for initial value + updateGitHubTheme(); +} + function main() { state = (vscodeApi.getState() as any) || loadingState; render(, document.body); + setThemeStylesheet(); vscodeApi.postMessage({ command: "ready" }); } @@ -121,6 +181,14 @@ function onMessage(event: any) { }; } break; + case "showDocumentationCommand": + { + state = { + viewType: "documentation", + fragmentsToRender: message.fragmentsToRender, + }; + } + break; default: console.error("Unknown command: ", message.command); return; @@ -178,6 +246,12 @@ function App({ state }: { state: State }) { return ; case "help": return ; + case "documentation": + // Ideally we'd have this on all web views, but it makes the font a little + // too large in the others right now. Something to unify later. + document.body.classList.add("markdown-body"); + document.body.style.fontSize = "0.8em"; + return ; default: console.error("Unknown view type in state", state); return
Loading error
; diff --git a/vscode/src/webviewPanel.ts b/vscode/src/webviewPanel.ts index dca9460936..551ffccf37 100644 --- a/vscode/src/webviewPanel.ts +++ b/vscode/src/webviewPanel.ts @@ -23,6 +23,7 @@ import { loadProject } from "./projectSystem"; import { EventType, sendTelemetryEvent } from "./telemetry"; import { getRandomGuid } from "./utils"; import { showCircuitCommand } from "./circuit"; +import { showDocumentationCommand } from "./documentation"; const QSharpWebViewType = "qsharp-webview"; const compilerRunTimeoutMs = 1000 * 60 * 5; // 5 minutes @@ -379,9 +380,20 @@ export function registerWebViewCommands(context: ExtensionContext) { }, ), ); + + context.subscriptions.push( + commands.registerCommand("qsharp-vscode.showDocumentation", async () => { + await showDocumentationCommand(context.extensionUri); + }), + ); } -type PanelType = "histogram" | "estimates" | "help" | "circuit"; +type PanelType = + | "histogram" + | "estimates" + | "help" + | "circuit" + | "documentation"; const panelTypeToPanel: Record< PanelType, @@ -391,6 +403,11 @@ const panelTypeToPanel: Record< estimates: { title: "Q# Estimates", panel: undefined, state: {} }, circuit: { title: "Q# Circuit", panel: undefined, state: {} }, help: { title: "Q# Help", panel: undefined, state: {} }, + documentation: { + title: "Q# Documentation", + panel: undefined, + state: {}, + }, }; export function sendMessageToPanel( @@ -410,6 +427,7 @@ export function sendMessageToPanel( { enableCommandUris: true, enableScripts: true, + enableFindWidget: true, retainContextWhenHidden: true, // Note: If retainContextWhenHidden is false, the webview gets reloaded // every time you hide it by switching to another tab and then switch @@ -458,7 +476,7 @@ export class QSharpWebViewPanel { } const katexCss = getUri(["out", "katex", "katex.min.css"]); - const githubCss = getUri(["out", "katex", "github-markdown.css"]); + const githubCss = getUri(["out", "katex", "github-markdown-dark.css"]); const webviewCss = getUri(["out", "webview", "webview.css"]); const webviewJs = getUri(["out", "webview", "webview.js"]); const resourcesUri = getUri(["resources"]); @@ -528,7 +546,8 @@ export class QSharpViewViewPanelSerializer implements WebviewPanelSerializer { panelType !== "estimates" && panelType !== "histogram" && panelType !== "circuit" && - panelType !== "help" + panelType !== "help" && + panelType != "documentation" ) { // If it was loading when closed, that's fine if (panelType === "loading") { diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs index 764d6ef12c..ac1fe0033d 100644 --- a/wasm/src/lib.rs +++ b/wasm/src/lib.rs @@ -20,6 +20,7 @@ use qsc::{ }, target::Profile, LanguageFeatures, PackageStore, PackageType, SourceContents, SourceMap, SourceName, SparseSim, + TargetCapabilityFlags, }; use qsc_codegen::qir_base::generate_qir; use resource_estimator::{self as re, estimate_entry}; @@ -459,8 +460,22 @@ serializable_type! { #[wasm_bindgen] #[must_use] -pub fn generate_docs() -> Vec { - let docs = qsc_doc_gen::generate_docs::generate_docs(); +pub fn generate_docs( + additionalSources: Option>, + targetProfile: Option, + languageFeatures: Option>, +) -> Vec { + let source_map: Option = additionalSources.map(|s| get_source_map(s, &None)); + + let target_profile: Option = targetProfile.map(|p| { + Profile::from_str(&p) + .expect("invalid target profile") + .into() + }); + + let features: Option = languageFeatures.map(LanguageFeatures::from_iter); + + let docs = qsc_doc_gen::generate_docs::generate_docs(source_map, target_profile, features); let mut result: Vec = vec![]; for (name, metadata, contents) in docs { diff --git a/wasm/src/tests.rs b/wasm/src/tests.rs index 0b30b0c7a4..c5335ab775 100644 --- a/wasm/src/tests.rs +++ b/wasm/src/tests.rs @@ -446,7 +446,7 @@ fn test_runtime_error_default_span() { #[test] fn test_doc_gen() { - let docs = qsc_doc_gen::generate_docs::generate_docs(); + let docs = qsc_doc_gen::generate_docs::generate_docs(None, None, None); assert!(docs.len() > 100); for (name, metadata, contents) in docs { // filename will be something like "Microsoft.Quantum.Canon/ApplyToEachC.md"