// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
declare const __PLATFORM__: "browser" | "node";
import { escapeHtml } from "markdown-it/lib/common/utils.mjs";
import { TextDocument, Uri, Range, Location } from "vscode";
import {
getCompilerWorker,
ICompilerWorker,
ILocation,
IPosition,
IQSharpError,
IRange,
IWorkspaceEdit,
VSDiagnostic,
} from "qsharp-lang";
import * as vscode from "vscode";
export const qsharpLanguageId = "qsharp";
export const qsharpCircuitLanguageId = "qsharpcircuit";
export const openqasmLanguageId = "openqasm";
// Returns true for all documents supported by the extension, including unsaved files, notebook cells, circuit files, qasm files, etc.
// excludes text documents we don't want to add support for at all, such as git/pr/chat "virtual" document views
export function isQdkDocument(document: TextDocument): boolean {
return (
!isUnsupportedScheme(document.uri.scheme) &&
isQdkSupportedLanguage(document)
);
}
function isQdkSupportedLanguage(document: TextDocument): boolean {
return (
document.languageId === qsharpLanguageId ||
document.languageId === qsharpCircuitLanguageId ||
document.languageId === openqasmLanguageId
);
}
function isUnsupportedScheme(scheme: string): boolean {
return (
scheme === "git" ||
scheme === "pr" ||
scheme === "review" ||
scheme.startsWith("chat")
);
}
// Returns true for all Q# documents, including unsaved files, notebook cells, circuit files, etc.
export function isQsharpDocument(document: TextDocument): boolean {
return (
!isUnsupportedScheme(document.uri.scheme) &&
document.languageId === qsharpLanguageId
);
}
// Returns true for all circuit documents
export function isCircuitDocument(document: TextDocument): boolean {
return (
!isUnsupportedScheme(document.uri.scheme) &&
document.languageId === qsharpCircuitLanguageId
);
}
export function isQdkNotebookCell(document: TextDocument): boolean {
return isQdkDocument(document) && isNotebookCell(document);
}
// Returns true for all OpenQASM documents, including unsaved files, notebook cells, etc.
export function isOpenQasmDocument(document: TextDocument): boolean {
return (
!isUnsupportedScheme(document.uri.scheme) &&
document.languageId === openqasmLanguageId
);
}
export function isNotebookCell(document: TextDocument): boolean {
return document.uri.scheme === "vscode-notebook-cell";
}
export const qsharpExtensionId = "qsharp-vscode";
export function basename(path: string): string | undefined {
return path.replace(/\/+$/, "").split("/").pop();
}
export function toVsCodeRange(range: IRange): Range {
return new Range(
range.start.line,
range.start.character,
range.end.line,
range.end.character,
);
}
export function toVsCodeLocation(location: ILocation): Location {
return new Location(Uri.parse(location.source), toVsCodeRange(location.span));
}
export function toVsCodeWorkspaceEdit(
iWorkspaceEdit: IWorkspaceEdit,
): vscode.WorkspaceEdit {
const workspaceEdit = new vscode.WorkspaceEdit();
for (const [source, edits] of iWorkspaceEdit.changes) {
const uri = vscode.Uri.parse(source, true);
const vsEdits = edits.map((edit) => {
return new vscode.TextEdit(toVsCodeRange(edit.range), edit.newText);
});
workspaceEdit.set(uri, vsEdits);
}
return workspaceEdit;
}
export function toVsCodeDiagnostic(d: VSDiagnostic): vscode.Diagnostic {
let severity;
switch (d.severity) {
case "error":
severity = vscode.DiagnosticSeverity.Error;
break;
case "warning":
severity = vscode.DiagnosticSeverity.Warning;
break;
case "info":
severity = vscode.DiagnosticSeverity.Information;
break;
case "hint":
severity = vscode.DiagnosticSeverity.Hint;
break;
}
const vscodeDiagnostic = new vscode.Diagnostic(
toVsCodeRange(d.range),
d.message,
severity,
);
if (d.tags) {
// LSP DiagnosticTag values: 1 = Unnecessary, 2 = Deprecated.
// VS Code's DiagnosticTag enum uses the same numeric values.
vscodeDiagnostic.tags = d.tags as vscode.DiagnosticTag[];
}
if (d.uri && d.code) {
vscodeDiagnostic.code = {
value: d.code,
target: vscode.Uri.parse(d.uri),
};
} else if (d.code) {
vscodeDiagnostic.code = d.code;
}
if (d.related) {
vscodeDiagnostic.relatedInformation = d.related.map((r) => {
return new vscode.DiagnosticRelatedInformation(
toVsCodeLocation(r.location),
r.message,
);
});
}
return vscodeDiagnostic;
}
export function loadCompilerWorker(extensionUri: vscode.Uri): ICompilerWorker {
const compilerWorkerScriptPath = vscode.Uri.joinPath(
extensionUri,
`./out/${__PLATFORM__}/compilerWorker.js`,
).toString();
return getCompilerWorker(compilerWorkerScriptPath);
}
export function getPlatformEnv(): string {
return __PLATFORM__;
}
/**
* Formats an array of compiler/runtime errors into HTML to be presented to the user.
*
* @param errors The list of errors to format.
* @returns The HTML formatted errors, to be set as the inner contents of a container element.
*/
export function errorsToHtml(errors: IQSharpError[]) {
let errorHtml = "";
for (const error of errors) {
const { document, diagnostic: diag, stack: rawStack } = error;
const location = documentHtml(false, document, diag.range.start);
const message = escapeHtml(`(${diag.code}) ${diag.message}`).replace(
/\n/g,
"
",
);
errorHtml += `
${location}: ${message}
${stack}`;
}
}
return errorHtml;
}
/**
* If the input is a URI, turns it into a document open link.
* Otherwise returns the HTML-escaped input
*/
function documentHtml(
customCommand: boolean,
maybeUri: string,
position?: IPosition,
) {
try {
// If the error location is a document URI, create a link to that document.
// We use the `vscode.open` command (https://code.visualstudio.com/api/references/commands#commands)
// to open the document in the editor.
// The line and column information is displayed, but are not part of the link.
//
// At the time of writing this is the only way we know to create a direct
// link to a Q# document from a Web View.
//
// If we wanted to handle line/column information from the link, an alternate
// implementation might be having our own command that navigates to the correct
// location. Then this would be a link to that command instead. Yet another
// alternative is to have the webview pass a message back to the extension.
const uri = Uri.parse(maybeUri, true);
const fsPath = escapeHtml(basename(uri.path) ?? uri.fsPath);
const lineColumn = position
? escapeHtml(`:${position.line + 1}:${position.character + 1}`)
: "";
const locations = [
{
source: uri,
span: {
start: position,
end: position,
},
},
];
const args = customCommand && position ? [locations] : [uri];
const openCommand =
customCommand && position ? "qsharp-vscode.gotoLocations" : "vscode.open";
const argsStr = encodeURIComponent(JSON.stringify(args));
const openCommandUri = Uri.parse(`command:${openCommand}?${argsStr}`, true);
const title = `${fsPath}${lineColumn}`;
return `${title}`;
} catch {
// Likely could not parse document URI - it must be a project level error
// or an error from stdlib, use the document name directly
return escapeHtml(maybeUri);
}
}