From b342047c891c8f8f81641c2535bc99660da41a93 Mon Sep 17 00:00:00 2001 From: Mine Starks Date: Fri, 6 Dec 2024 15:10:54 -0800 Subject: [PATCH] Move VS Code language service modules into their own folder --- vscode/src/common.ts | 39 ++- vscode/src/diagnostics.ts | 104 +----- vscode/src/extension.ts | 307 +----------------- vscode/src/language-service/activate.ts | 302 +++++++++++++++++ .../src/{ => language-service}/codeActions.ts | 2 +- vscode/src/{ => language-service}/codeLens.ts | 2 +- .../src/{ => language-service}/completion.ts | 4 +- .../src/{ => language-service}/definition.ts | 2 +- vscode/src/language-service/diagnostics.ts | 49 +++ vscode/src/{ => language-service}/format.ts | 6 +- vscode/src/{ => language-service}/hover.ts | 2 +- vscode/src/language-service/notebook.ts | 135 ++++++++ .../src/{ => language-service}/references.ts | 2 +- vscode/src/{ => language-service}/rename.ts | 2 +- .../src/{ => language-service}/signature.ts | 0 vscode/src/notebook.ts | 135 +------- 16 files changed, 558 insertions(+), 535 deletions(-) create mode 100644 vscode/src/language-service/activate.ts rename vscode/src/{ => language-service}/codeActions.ts (97%) rename vscode/src/{ => language-service}/codeLens.ts (98%) rename vscode/src/{ => language-service}/completion.ts (96%) rename vscode/src/{ => language-service}/definition.ts (94%) create mode 100644 vscode/src/language-service/diagnostics.ts rename vscode/src/{ => language-service}/format.ts (92%) rename vscode/src/{ => language-service}/hover.ts (94%) create mode 100644 vscode/src/language-service/notebook.ts rename vscode/src/{ => language-service}/references.ts (95%) rename vscode/src/{ => language-service}/rename.ts (95%) rename vscode/src/{ => language-service}/signature.ts (100%) diff --git a/vscode/src/common.ts b/vscode/src/common.ts index ea6da48537..71de7e08b6 100644 --- a/vscode/src/common.ts +++ b/vscode/src/common.ts @@ -3,7 +3,7 @@ import { TextDocument, Uri, Range, Location } from "vscode"; import { Utils } from "vscode-uri"; -import { ILocation, IRange, IWorkspaceEdit } from "qsharp-lang"; +import { ILocation, IRange, IWorkspaceEdit, VSDiagnostic } from "qsharp-lang"; import * as vscode from "vscode"; export const qsharpLanguageId = "qsharp"; @@ -58,3 +58,40 @@ export function toVscodeWorkspaceEdit( } 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; + } + const vscodeDiagnostic = new vscode.Diagnostic( + toVscodeRange(d.range), + d.message, + severity, + ); + 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; +} diff --git a/vscode/src/diagnostics.ts b/vscode/src/diagnostics.ts index 3a4dd2503d..e0fd154c86 100644 --- a/vscode/src/diagnostics.ts +++ b/vscode/src/diagnostics.ts @@ -1,100 +1,20 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { - ILanguageService, - IQSharpError, - VSDiagnostic, - log, - qsharpLibraryUriScheme, -} from "qsharp-lang"; +import { IQSharpError, log, qsharpLibraryUriScheme } from "qsharp-lang"; import * as vscode from "vscode"; -import { toVscodeLocation, toVscodeRange, qsharpLanguageId } from "./common.js"; +import { qsharpLanguageId, toVsCodeDiagnostic } from "./common.js"; -export function startCheckingQSharp( - languageService: ILanguageService, -): vscode.Disposable[] { - return [ - ...startLanguageServiceDiagnostics(languageService), - ...startQsharpJsonDiagnostics(), - ...startCommandDiagnostics(), - ]; -} - -function startLanguageServiceDiagnostics( - languageService: ILanguageService, -): vscode.Disposable[] { - const diagCollection = - vscode.languages.createDiagnosticCollection(qsharpLanguageId); - - async function onDiagnostics(evt: { - detail: { - uri: string; - version: number; - diagnostics: VSDiagnostic[]; - }; - }) { - const diagnostics = evt.detail; - const uri = vscode.Uri.parse(diagnostics.uri); - - if (uri.scheme === qsharpLibraryUriScheme) { - // Don't report diagnostics for library files. - return; - } - - diagCollection.set( - uri, - diagnostics.diagnostics.map((d) => toVsCodeDiagnostic(d)), - ); - } - - languageService.addEventListener("diagnostics", onDiagnostics); - - return [ - { - dispose: () => { - languageService.removeEventListener("diagnostics", onDiagnostics); - }, - }, - diagCollection, - ]; -} - -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; - } - const vscodeDiagnostic = new vscode.Diagnostic( - toVscodeRange(d.range), - d.message, - severity, - ); - 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; +/** + * Initialize diagnostics for `qsharp.json` files and failures + * that get reported from various Q# commands. + * + * These are distinct from the errors reported by the Q# language + * service, (a.k.a. compiler errors that get reported as you type). + * Those are initialized in `language-service/diagnostics.js` + */ +export function startOtherQSharpDiagnostics(): vscode.Disposable[] { + return [...startQsharpJsonDiagnostics(), ...startCommandDiagnostics()]; } // diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index a0061bbc17..750700f373 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -2,31 +2,17 @@ // Licensed under the MIT License. import { - ILanguageService, - getLanguageService, getLibrarySourceContent, - loadWasmModule, log, qsharpGithubUriScheme, qsharpLibraryUriScheme, } from "qsharp-lang"; import * as vscode from "vscode"; import { initAzureWorkspaces } from "./azure/commands.js"; -import { createCodeActionsProvider } from "./codeActions.js"; -import { createCodeLensProvider } from "./codeLens.js"; -import { - isQsharpDocument, - isQsharpNotebookCell, - qsharpLanguageId, -} from "./common.js"; -import { createCompletionItemProvider } from "./completion"; -import { getTarget } from "./config"; import { initProjectCreator } from "./createProject.js"; -import { activateDebugger } from "./debugger/activate"; -import { createDefinitionProvider } from "./definition"; -import { startCheckingQSharp } from "./diagnostics"; -import { createFormattingProvider } from "./format.js"; -import { createHoverProvider } from "./hover"; +import { activateDebugger } from "./debugger/activate.js"; +import { startOtherQSharpDiagnostics } from "./diagnostics.js"; +import { activateLanguageService } from "./language-service/activate.js"; import { Logging, initLogForwarder, @@ -35,29 +21,12 @@ import { import { initFileSystem } from "./memfs.js"; import { registerCreateNotebookCommand, - registerQSharpNotebookCellUpdateHandlers, registerQSharpNotebookHandlers, } from "./notebook.js"; -import { - fetchGithubRaw, - findManifestDirectory, - getGithubSourceContent, - listDirectory, - readFile, - resolvePath, - setGithubEndpoint, -} from "./projectSystem.js"; +import { getGithubSourceContent, setGithubEndpoint } from "./projectSystem.js"; import { initCodegen } from "./qirGeneration.js"; -import { createReferenceProvider } from "./references.js"; -import { createRenameProvider } from "./rename.js"; -import { createSignatureHelpProvider } from "./signature.js"; import { activateTargetProfileStatusBarItem } from "./statusbar.js"; -import { - EventType, - QsharpDocumentType, - initTelemetry, - sendTelemetryEvent, -} from "./telemetry.js"; +import { initTelemetry } from "./telemetry.js"; import { registerWebViewCommands } from "./webviewPanel.js"; export async function activate( @@ -102,6 +71,8 @@ export async function activate( ...(await activateLanguageService(context.extensionUri)), ); + context.subscriptions.push(...startOtherQSharpDiagnostics()); + context.subscriptions.push(...registerQSharpNotebookHandlers()); initAzureWorkspaces(context); @@ -123,270 +94,6 @@ export interface ExtensionApi { setGithubEndpoint: (endpoint: string) => void; } -function registerDocumentUpdateHandlers(languageService: ILanguageService) { - vscode.workspace.textDocuments.forEach((document) => { - updateIfQsharpDocument(document); - }); - - // we manually send an OpenDocument telemetry event if this is a Q# document, because the - // below subscriptions won't fire for documents that are already open when the extension is activated - vscode.workspace.textDocuments.forEach((document) => { - if (isQsharpDocument(document)) { - const documentType = isQsharpNotebookCell(document) - ? QsharpDocumentType.JupyterCell - : QsharpDocumentType.Qsharp; - sendTelemetryEvent( - EventType.OpenedDocument, - { documentType }, - { linesOfCode: document.lineCount }, - ); - } - }); - - const subscriptions = []; - subscriptions.push( - vscode.workspace.onDidOpenTextDocument((document) => { - const documentType = isQsharpNotebookCell(document) - ? QsharpDocumentType.JupyterCell - : isQsharpDocument(document) - ? QsharpDocumentType.Qsharp - : QsharpDocumentType.Other; - if (documentType !== QsharpDocumentType.Other) { - sendTelemetryEvent( - EventType.OpenedDocument, - { documentType }, - { linesOfCode: document.lineCount }, - ); - } - updateIfQsharpDocument(document); - }), - ); - - subscriptions.push( - vscode.workspace.onDidChangeTextDocument((evt) => { - updateIfQsharpDocument(evt.document); - }), - ); - - subscriptions.push( - vscode.workspace.onDidCloseTextDocument((document) => { - if (isQsharpDocument(document) && !isQsharpNotebookCell(document)) { - languageService.closeDocument(document.uri.toString()); - } - }), - ); - - // Watch manifest changes and update each document in the same project as the manifest. - subscriptions.push( - vscode.workspace.onDidSaveTextDocument((manifest) => { - updateProjectDocuments(manifest.uri); - }), - ); - - // Trigger an update on all .qs child documents when their manifest is deleted, - // so that they can get reparented to single-file-projects. - subscriptions.push( - vscode.workspace.onDidDeleteFiles((event) => { - event.files.forEach((uri) => { - updateProjectDocuments(uri); - }); - }), - ); - - // Checks if the URI belongs to a qsharp manifest, and updates all - // open documents in the same project as the manifest. - function updateProjectDocuments(manifest: vscode.Uri) { - if (manifest.scheme === "file" && manifest.fsPath.endsWith("qsharp.json")) { - const project_folder = manifest.fsPath.slice( - 0, - manifest.fsPath.length - "qsharp.json".length, - ); - vscode.workspace.textDocuments.forEach((document) => { - if ( - !document.isClosed && - // Check that the document is on the same project as the manifest. - document.fileName.startsWith(project_folder) - ) { - updateIfQsharpDocument(document); - } - }); - } - } - - function updateIfQsharpDocument(document: vscode.TextDocument) { - if (isQsharpDocument(document) && !isQsharpNotebookCell(document)) { - // Regular (not notebook) Q# document. - languageService.updateDocument( - document.uri.toString(), - document.version, - document.getText(), - ); - } - } - - return subscriptions; -} - -async function activateLanguageService(extensionUri: vscode.Uri) { - const subscriptions: vscode.Disposable[] = []; - - const languageService = await loadLanguageService(extensionUri); - - // diagnostics - subscriptions.push(...startCheckingQSharp(languageService)); - - // synchronize document contents - subscriptions.push(...registerDocumentUpdateHandlers(languageService)); - - // synchronize notebook cell contents - subscriptions.push( - ...registerQSharpNotebookCellUpdateHandlers(languageService), - ); - - // synchronize configuration - subscriptions.push(registerConfigurationChangeHandlers(languageService)); - - // format document - subscriptions.push( - vscode.languages.registerDocumentFormattingEditProvider( - qsharpLanguageId, - createFormattingProvider(languageService), - ), - ); - - // format range - subscriptions.push( - vscode.languages.registerDocumentRangeFormattingEditProvider( - qsharpLanguageId, - createFormattingProvider(languageService), - ), - ); - - // completions - subscriptions.push( - vscode.languages.registerCompletionItemProvider( - qsharpLanguageId, - createCompletionItemProvider(languageService), - // Trigger characters should be kept in sync with the ones in `playground/src/main.tsx` - "@", - ".", - ), - ); - - // hover - subscriptions.push( - vscode.languages.registerHoverProvider( - qsharpLanguageId, - createHoverProvider(languageService), - ), - ); - - // go to def - subscriptions.push( - vscode.languages.registerDefinitionProvider( - qsharpLanguageId, - createDefinitionProvider(languageService), - ), - ); - - // find references - subscriptions.push( - vscode.languages.registerReferenceProvider( - qsharpLanguageId, - createReferenceProvider(languageService), - ), - ); - - // signature help - subscriptions.push( - vscode.languages.registerSignatureHelpProvider( - qsharpLanguageId, - createSignatureHelpProvider(languageService), - "(", - ",", - ), - ); - - // rename symbol - subscriptions.push( - vscode.languages.registerRenameProvider( - qsharpLanguageId, - createRenameProvider(languageService), - ), - ); - - // code lens - subscriptions.push( - vscode.languages.registerCodeLensProvider( - qsharpLanguageId, - createCodeLensProvider(languageService), - ), - ); - - subscriptions.push( - vscode.languages.registerCodeActionsProvider( - qsharpLanguageId, - createCodeActionsProvider(languageService), - ), - ); - - // add the language service dispose handler as well - subscriptions.push(languageService); - - return subscriptions; -} - -async function updateLanguageServiceProfile(languageService: ILanguageService) { - const targetProfile = getTarget(); - - switch (targetProfile) { - case "base": - case "adaptive_ri": - case "unrestricted": - break; - default: - log.warn(`Invalid value for target profile: ${targetProfile}`); - } - log.debug("Target profile set to: " + targetProfile); - - languageService.updateConfiguration({ - targetProfile: targetProfile, - lints: [{ lint: "needlessOperation", level: "warn" }], - }); -} - -async function loadLanguageService(baseUri: vscode.Uri) { - const start = performance.now(); - const wasmUri = vscode.Uri.joinPath(baseUri, "./wasm/qsc_wasm_bg.wasm"); - const wasmBytes = await vscode.workspace.fs.readFile(wasmUri); - await loadWasmModule(wasmBytes); - const languageService = await getLanguageService({ - findManifestDirectory, - readFile, - listDirectory, - resolvePath: async (a, b) => resolvePath(a, b), - fetchGithub: fetchGithubRaw, - }); - await updateLanguageServiceProfile(languageService); - const end = performance.now(); - sendTelemetryEvent( - EventType.LoadLanguageService, - {}, - { timeToStartMs: end - start }, - ); - return languageService; -} - -function registerConfigurationChangeHandlers( - languageService: ILanguageService, -) { - return vscode.workspace.onDidChangeConfiguration((event) => { - if (event.affectsConfiguration("Q#.qir.targetProfile")) { - updateLanguageServiceProfile(languageService); - } - }); -} - export class QsTextDocumentContentProvider implements vscode.TextDocumentContentProvider { diff --git a/vscode/src/language-service/activate.ts b/vscode/src/language-service/activate.ts new file mode 100644 index 0000000000..d4815d8713 --- /dev/null +++ b/vscode/src/language-service/activate.ts @@ -0,0 +1,302 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { + ILanguageService, + getLanguageService, + loadWasmModule, + log, +} from "qsharp-lang"; +import * as vscode from "vscode"; +import { + isQsharpDocument, + isQsharpNotebookCell, + qsharpLanguageId, +} from "../common.js"; +import { getTarget } from "../config.js"; +import { + fetchGithubRaw, + findManifestDirectory, + listDirectory, + readFile, + resolvePath, +} from "../projectSystem.js"; +import { + EventType, + QsharpDocumentType, + sendTelemetryEvent, +} from "../telemetry.js"; +import { createCodeActionsProvider } from "./codeActions.js"; +import { createCodeLensProvider } from "./codeLens.js"; +import { createCompletionItemProvider } from "./completion.js"; +import { createDefinitionProvider } from "./definition.js"; +import { startLanguageServiceDiagnostics } from "./diagnostics.js"; +import { createFormattingProvider } from "./format.js"; +import { createHoverProvider } from "./hover.js"; +import { registerQSharpNotebookCellUpdateHandlers } from "./notebook.js"; +import { createReferenceProvider } from "./references.js"; +import { createRenameProvider } from "./rename.js"; +import { createSignatureHelpProvider } from "./signature.js"; + +export async function activateLanguageService(extensionUri: vscode.Uri) { + const subscriptions: vscode.Disposable[] = []; + + const languageService = await loadLanguageService(extensionUri); + + // diagnostics + subscriptions.push(...startLanguageServiceDiagnostics(languageService)); + + // synchronize document contents + subscriptions.push(...registerDocumentUpdateHandlers(languageService)); + + // synchronize notebook cell contents + subscriptions.push( + ...registerQSharpNotebookCellUpdateHandlers(languageService), + ); + + // synchronize configuration + subscriptions.push(registerConfigurationChangeHandlers(languageService)); + + // format document + subscriptions.push( + vscode.languages.registerDocumentFormattingEditProvider( + qsharpLanguageId, + createFormattingProvider(languageService), + ), + ); + + // format range + subscriptions.push( + vscode.languages.registerDocumentRangeFormattingEditProvider( + qsharpLanguageId, + createFormattingProvider(languageService), + ), + ); + + // completions + subscriptions.push( + vscode.languages.registerCompletionItemProvider( + qsharpLanguageId, + createCompletionItemProvider(languageService), + // Trigger characters should be kept in sync with the ones in `playground/src/main.tsx` + "@", + ".", + ), + ); + + // hover + subscriptions.push( + vscode.languages.registerHoverProvider( + qsharpLanguageId, + createHoverProvider(languageService), + ), + ); + + // go to def + subscriptions.push( + vscode.languages.registerDefinitionProvider( + qsharpLanguageId, + createDefinitionProvider(languageService), + ), + ); + + // find references + subscriptions.push( + vscode.languages.registerReferenceProvider( + qsharpLanguageId, + createReferenceProvider(languageService), + ), + ); + + // signature help + subscriptions.push( + vscode.languages.registerSignatureHelpProvider( + qsharpLanguageId, + createSignatureHelpProvider(languageService), + "(", + ",", + ), + ); + + // rename symbol + subscriptions.push( + vscode.languages.registerRenameProvider( + qsharpLanguageId, + createRenameProvider(languageService), + ), + ); + + // code lens + subscriptions.push( + vscode.languages.registerCodeLensProvider( + qsharpLanguageId, + createCodeLensProvider(languageService), + ), + ); + + subscriptions.push( + vscode.languages.registerCodeActionsProvider( + qsharpLanguageId, + createCodeActionsProvider(languageService), + ), + ); + + // add the language service dispose handler as well + subscriptions.push(languageService); + + return subscriptions; +} + +async function loadLanguageService(baseUri: vscode.Uri) { + const start = performance.now(); + const wasmUri = vscode.Uri.joinPath(baseUri, "./wasm/qsc_wasm_bg.wasm"); + const wasmBytes = await vscode.workspace.fs.readFile(wasmUri); + await loadWasmModule(wasmBytes); + const languageService = await getLanguageService({ + findManifestDirectory, + readFile, + listDirectory, + resolvePath: async (a, b) => resolvePath(a, b), + fetchGithub: fetchGithubRaw, + }); + await updateLanguageServiceProfile(languageService); + const end = performance.now(); + sendTelemetryEvent( + EventType.LoadLanguageService, + {}, + { timeToStartMs: end - start }, + ); + return languageService; +} +function registerDocumentUpdateHandlers(languageService: ILanguageService) { + vscode.workspace.textDocuments.forEach((document) => { + updateIfQsharpDocument(document); + }); + + // we manually send an OpenDocument telemetry event if this is a Q# document, because the + // below subscriptions won't fire for documents that are already open when the extension is activated + vscode.workspace.textDocuments.forEach((document) => { + if (isQsharpDocument(document)) { + const documentType = isQsharpNotebookCell(document) + ? QsharpDocumentType.JupyterCell + : QsharpDocumentType.Qsharp; + sendTelemetryEvent( + EventType.OpenedDocument, + { documentType }, + { linesOfCode: document.lineCount }, + ); + } + }); + + const subscriptions = []; + subscriptions.push( + vscode.workspace.onDidOpenTextDocument((document) => { + const documentType = isQsharpNotebookCell(document) + ? QsharpDocumentType.JupyterCell + : isQsharpDocument(document) + ? QsharpDocumentType.Qsharp + : QsharpDocumentType.Other; + if (documentType !== QsharpDocumentType.Other) { + sendTelemetryEvent( + EventType.OpenedDocument, + { documentType }, + { linesOfCode: document.lineCount }, + ); + } + updateIfQsharpDocument(document); + }), + ); + + subscriptions.push( + vscode.workspace.onDidChangeTextDocument((evt) => { + updateIfQsharpDocument(evt.document); + }), + ); + + subscriptions.push( + vscode.workspace.onDidCloseTextDocument((document) => { + if (isQsharpDocument(document) && !isQsharpNotebookCell(document)) { + languageService.closeDocument(document.uri.toString()); + } + }), + ); + + // Watch manifest changes and update each document in the same project as the manifest. + subscriptions.push( + vscode.workspace.onDidSaveTextDocument((manifest) => { + updateProjectDocuments(manifest.uri); + }), + ); + + // Trigger an update on all .qs child documents when their manifest is deleted, + // so that they can get reparented to single-file-projects. + subscriptions.push( + vscode.workspace.onDidDeleteFiles((event) => { + event.files.forEach((uri) => { + updateProjectDocuments(uri); + }); + }), + ); + + // Checks if the URI belongs to a qsharp manifest, and updates all + // open documents in the same project as the manifest. + function updateProjectDocuments(manifest: vscode.Uri) { + if (manifest.scheme === "file" && manifest.fsPath.endsWith("qsharp.json")) { + const project_folder = manifest.fsPath.slice( + 0, + manifest.fsPath.length - "qsharp.json".length, + ); + vscode.workspace.textDocuments.forEach((document) => { + if ( + !document.isClosed && + // Check that the document is on the same project as the manifest. + document.fileName.startsWith(project_folder) + ) { + updateIfQsharpDocument(document); + } + }); + } + } + + function updateIfQsharpDocument(document: vscode.TextDocument) { + if (isQsharpDocument(document) && !isQsharpNotebookCell(document)) { + // Regular (not notebook) Q# document. + languageService.updateDocument( + document.uri.toString(), + document.version, + document.getText(), + ); + } + } + + return subscriptions; +} + +function registerConfigurationChangeHandlers( + languageService: ILanguageService, +) { + return vscode.workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration("Q#.qir.targetProfile")) { + updateLanguageServiceProfile(languageService); + } + }); +} + +async function updateLanguageServiceProfile(languageService: ILanguageService) { + const targetProfile = getTarget(); + + switch (targetProfile) { + case "base": + case "adaptive_ri": + case "unrestricted": + break; + default: + log.warn(`Invalid value for target profile: ${targetProfile}`); + } + log.debug("Target profile set to: " + targetProfile); + + languageService.updateConfiguration({ + targetProfile: targetProfile, + lints: [{ lint: "needlessOperation", level: "warn" }], + }); +} diff --git a/vscode/src/codeActions.ts b/vscode/src/language-service/codeActions.ts similarity index 97% rename from vscode/src/codeActions.ts rename to vscode/src/language-service/codeActions.ts index 03fb7869c7..513f28fe88 100644 --- a/vscode/src/codeActions.ts +++ b/vscode/src/language-service/codeActions.ts @@ -3,7 +3,7 @@ import { ILanguageService, ICodeAction } from "qsharp-lang"; import * as vscode from "vscode"; -import { toVscodeWorkspaceEdit } from "./common"; +import { toVscodeWorkspaceEdit } from "../common"; export function createCodeActionsProvider(languageService: ILanguageService) { return new QSharpCodeActionProvider(languageService); diff --git a/vscode/src/codeLens.ts b/vscode/src/language-service/codeLens.ts similarity index 98% rename from vscode/src/codeLens.ts rename to vscode/src/language-service/codeLens.ts index 7790628105..98672811cb 100644 --- a/vscode/src/codeLens.ts +++ b/vscode/src/language-service/codeLens.ts @@ -7,7 +7,7 @@ import { qsharpLibraryUriScheme, } from "qsharp-lang"; import * as vscode from "vscode"; -import { toVscodeRange } from "./common"; +import { toVscodeRange } from "../common"; export function createCodeLensProvider(languageService: ILanguageService) { return new QSharpCodeLensProvider(languageService); diff --git a/vscode/src/completion.ts b/vscode/src/language-service/completion.ts similarity index 96% rename from vscode/src/completion.ts rename to vscode/src/language-service/completion.ts index 6526962c0b..92f2fc8bc8 100644 --- a/vscode/src/completion.ts +++ b/vscode/src/language-service/completion.ts @@ -4,8 +4,8 @@ import { ILanguageService, samples } from "qsharp-lang"; import * as vscode from "vscode"; import { CompletionItem } from "vscode"; -import { EventType, sendTelemetryEvent } from "./telemetry"; -import { toVscodeRange } from "./common"; +import { toVscodeRange } from "../common"; +import { EventType, sendTelemetryEvent } from "../telemetry"; export function createCompletionItemProvider( languageService: ILanguageService, diff --git a/vscode/src/definition.ts b/vscode/src/language-service/definition.ts similarity index 94% rename from vscode/src/definition.ts rename to vscode/src/language-service/definition.ts index 0800224b4a..fb2f6a6a23 100644 --- a/vscode/src/definition.ts +++ b/vscode/src/language-service/definition.ts @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { toVscodeLocation } from "./common"; import { ILanguageService } from "qsharp-lang"; import * as vscode from "vscode"; +import { toVscodeLocation } from "../common"; export function createDefinitionProvider(languageService: ILanguageService) { return new QSharpDefinitionProvider(languageService); diff --git a/vscode/src/language-service/diagnostics.ts b/vscode/src/language-service/diagnostics.ts new file mode 100644 index 0000000000..cbd0e9ac1d --- /dev/null +++ b/vscode/src/language-service/diagnostics.ts @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { + ILanguageService, + VSDiagnostic, + qsharpLibraryUriScheme, +} from "qsharp-lang"; +import * as vscode from "vscode"; +import { qsharpLanguageId, toVsCodeDiagnostic } from "../common"; + +export function startLanguageServiceDiagnostics( + languageService: ILanguageService, +): vscode.Disposable[] { + const diagCollection = + vscode.languages.createDiagnosticCollection(qsharpLanguageId); + + async function onDiagnostics(evt: { + detail: { + uri: string; + version: number; + diagnostics: VSDiagnostic[]; + }; + }) { + const diagnostics = evt.detail; + const uri = vscode.Uri.parse(diagnostics.uri); + + if (uri.scheme === qsharpLibraryUriScheme) { + // Don't report diagnostics for library files. + return; + } + + diagCollection.set( + uri, + diagnostics.diagnostics.map((d) => toVsCodeDiagnostic(d)), + ); + } + + languageService.addEventListener("diagnostics", onDiagnostics); + + return [ + { + dispose: () => { + languageService.removeEventListener("diagnostics", onDiagnostics); + }, + }, + diagCollection, + ]; +} diff --git a/vscode/src/format.ts b/vscode/src/language-service/format.ts similarity index 92% rename from vscode/src/format.ts rename to vscode/src/language-service/format.ts index 71765f7a89..fb9275dfd5 100644 --- a/vscode/src/format.ts +++ b/vscode/src/language-service/format.ts @@ -3,9 +3,9 @@ import { ILanguageService } from "qsharp-lang"; import * as vscode from "vscode"; -import { toVscodeRange } from "./common"; -import { EventType, FormatEvent, sendTelemetryEvent } from "./telemetry"; -import { getRandomGuid } from "./utils"; +import { toVscodeRange } from "../common"; +import { EventType, FormatEvent, sendTelemetryEvent } from "../telemetry"; +import { getRandomGuid } from "../utils"; export function createFormattingProvider(languageService: ILanguageService) { return new QSharpFormattingProvider(languageService); diff --git a/vscode/src/hover.ts b/vscode/src/language-service/hover.ts similarity index 94% rename from vscode/src/hover.ts rename to vscode/src/language-service/hover.ts index 6cab20afad..4307174099 100644 --- a/vscode/src/hover.ts +++ b/vscode/src/language-service/hover.ts @@ -3,7 +3,7 @@ import { ILanguageService } from "qsharp-lang"; import * as vscode from "vscode"; -import { toVscodeRange } from "./common"; +import { toVscodeRange } from "../common"; export function createHoverProvider(languageService: ILanguageService) { return new QSharpHoverProvider(languageService); diff --git a/vscode/src/language-service/notebook.ts b/vscode/src/language-service/notebook.ts new file mode 100644 index 0000000000..c17e542501 --- /dev/null +++ b/vscode/src/language-service/notebook.ts @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ILanguageService, log } from "qsharp-lang"; +import * as vscode from "vscode"; +import { isQsharpNotebookCell } from "../common.js"; +import { findQSharpCellMagic, jupyterNotebookType } from "../notebook.js"; + +const qsharpConfigMimeType = "application/x.qsharp-config"; + +const openQSharpNotebooks = new Set(); + +/** + * Document update handlers for syncing notebook cell contents with the language service. + */ +export function registerQSharpNotebookCellUpdateHandlers( + languageService: ILanguageService, +) { + vscode.workspace.notebookDocuments.forEach((notebook) => { + updateIfQsharpNotebook(notebook); + }); + + const subscriptions = []; + subscriptions.push( + vscode.workspace.onDidOpenNotebookDocument((notebook) => { + updateIfQsharpNotebook(notebook); + }), + ); + + subscriptions.push( + vscode.workspace.onDidChangeNotebookDocument((event) => { + updateIfQsharpNotebook(event.notebook); + }), + ); + + subscriptions.push( + vscode.workspace.onDidCloseNotebookDocument((notebook) => { + closeIfKnownQsharpNotebook(notebook); + }), + ); + + function updateIfQsharpNotebook(notebook: vscode.NotebookDocument) { + if (notebook.notebookType === jupyterNotebookType) { + const qsharpMetadata = getQSharpConfigMetadata(notebook); + const qsharpCells = getQSharpCells(notebook); + const notebookUri = notebook.uri.toString(); + if (qsharpCells.length > 0) { + openQSharpNotebooks.add(notebookUri); + languageService.updateNotebookDocument( + notebookUri, + notebook.version, + qsharpMetadata, + qsharpCells.map((cell) => { + return { + uri: cell.document.uri.toString(), + version: cell.document.version, + code: getQSharpText(cell.document), + }; + }), + ); + } else { + // All Q# cells could have been deleted, check if we know this doc from previous calls + closeIfKnownQsharpNotebook(notebook); + } + } + } + + function closeIfKnownQsharpNotebook(notebook: vscode.NotebookDocument) { + const notebookUri = notebook.uri.toString(); + if (openQSharpNotebooks.has(notebookUri)) { + languageService.closeNotebookDocument(notebookUri); + openQSharpNotebooks.delete(notebook.uri.toString()); + } + } + + function getQSharpCells(notebook: vscode.NotebookDocument) { + return notebook + .getCells() + .filter((cell) => isQsharpNotebookCell(cell.document)); + } + + function getQSharpText(document: vscode.TextDocument) { + const magicRange = findQSharpCellMagic(document); + if (magicRange) { + const magicStartOffset = document.offsetAt(magicRange.start); + const magicEndOffset = document.offsetAt(magicRange.end); + // Erase the %%qsharp magic line if it's there. + // Replace it with a comment so that document offsets remain the same. + // This will save us from having to map offsets later when + // communicating with the language service. + const text = document.getText(); + return ( + text.substring(0, magicStartOffset) + + "//qsharp" + + text.substring(magicEndOffset) + ); + } else { + // No %%qsharp magic. This can happen if the user manually sets the + // cell language to Q#. Python won't recognize the cell as a Q# cell, + // so this will fail at runtime, but as the language service we respect + // the manually set cell language, so we treat this as any other + // Q# cell. We could consider raising a warning here to help the user. + log.info( + "found Q# cell without %%qsharp magic: " + document.uri.toString(), + ); + return document.getText(); + } + } + + return subscriptions; +} + +/** + * Finds an output cell that contains an item with the Q# config MIME type, + * and returns the data from it. This data and is generated by the execution of a + * `qsharp.init()` call. It's Q# configuration data to be passed + * to the language service as "notebook metadata". + */ +function getQSharpConfigMetadata(notebook: vscode.NotebookDocument): object { + const data = notebook + .getCells() + .flatMap((cell) => cell.outputs) + .flatMap((output) => output.items) + .find((item) => { + return item.mime === qsharpConfigMimeType; + })?.data; + + if (data) { + const dataString = new TextDecoder().decode(data); + log.trace("found Q# config metadata: " + dataString); + return JSON.parse(dataString); + } else { + return {}; + } +} diff --git a/vscode/src/references.ts b/vscode/src/language-service/references.ts similarity index 95% rename from vscode/src/references.ts rename to vscode/src/language-service/references.ts index 7531cff9f4..528038c189 100644 --- a/vscode/src/references.ts +++ b/vscode/src/language-service/references.ts @@ -3,7 +3,7 @@ import { ILanguageService } from "qsharp-lang"; import * as vscode from "vscode"; -import { toVscodeLocation } from "./common"; +import { toVscodeLocation } from "../common"; export function createReferenceProvider(languageService: ILanguageService) { return new QSharpReferenceProvider(languageService); diff --git a/vscode/src/rename.ts b/vscode/src/language-service/rename.ts similarity index 95% rename from vscode/src/rename.ts rename to vscode/src/language-service/rename.ts index 0569a73801..02060ab4f5 100644 --- a/vscode/src/rename.ts +++ b/vscode/src/language-service/rename.ts @@ -3,7 +3,7 @@ import { ILanguageService } from "qsharp-lang"; import * as vscode from "vscode"; -import { toVscodeRange, toVscodeWorkspaceEdit } from "./common"; +import { toVscodeRange, toVscodeWorkspaceEdit } from "../common"; export function createRenameProvider(languageService: ILanguageService) { return new QSharpRenameProvider(languageService); diff --git a/vscode/src/signature.ts b/vscode/src/language-service/signature.ts similarity index 100% rename from vscode/src/signature.ts rename to vscode/src/language-service/signature.ts diff --git a/vscode/src/notebook.ts b/vscode/src/notebook.ts index 2d370b0be9..5e53cb54bd 100644 --- a/vscode/src/notebook.ts +++ b/vscode/src/notebook.ts @@ -1,16 +1,15 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { ILanguageService, log } from "qsharp-lang"; +import { log } from "qsharp-lang"; import * as vscode from "vscode"; -import { isQsharpNotebookCell, qsharpLanguageId } from "./common.js"; import { WorkspaceTreeProvider } from "./azure/treeView.js"; import { getPythonCodeForWorkspace } from "./azure/workspaceActions.js"; +import { qsharpLanguageId } from "./common.js"; import { notebookTemplate } from "./notebookTemplate.js"; const qsharpCellMagic = "%%qsharp"; -const jupyterNotebookType = "jupyter-notebook"; -const qsharpConfigMimeType = "application/x.qsharp-config"; +export const jupyterNotebookType = "jupyter-notebook"; let defaultLanguageId: string | undefined; /** @@ -97,13 +96,11 @@ export function registerQSharpNotebookHandlers() { return subscriptions; } -const openQSharpNotebooks = new Set(); - /** * Returns the range of the `%%qsharp` cell magic, or `undefined` * if it does not exist. */ -function findQSharpCellMagic(document: vscode.TextDocument) { +export function findQSharpCellMagic(document: vscode.TextDocument) { // Ignore whitespace before the cell magic for (let i = 0; i < document.lineCount; i++) { const line = document.lineAt(i); @@ -126,130 +123,6 @@ function findQSharpCellMagic(document: vscode.TextDocument) { return undefined; } -/** - * This one is for syncing with the language service - */ -export function registerQSharpNotebookCellUpdateHandlers( - languageService: ILanguageService, -) { - vscode.workspace.notebookDocuments.forEach((notebook) => { - updateIfQsharpNotebook(notebook); - }); - - const subscriptions = []; - subscriptions.push( - vscode.workspace.onDidOpenNotebookDocument((notebook) => { - updateIfQsharpNotebook(notebook); - }), - ); - - subscriptions.push( - vscode.workspace.onDidChangeNotebookDocument((event) => { - updateIfQsharpNotebook(event.notebook); - }), - ); - - subscriptions.push( - vscode.workspace.onDidCloseNotebookDocument((notebook) => { - closeIfKnownQsharpNotebook(notebook); - }), - ); - - function updateIfQsharpNotebook(notebook: vscode.NotebookDocument) { - if (notebook.notebookType === jupyterNotebookType) { - const qsharpMetadata = getQSharpConfigMetadata(notebook); - const qsharpCells = getQSharpCells(notebook); - const notebookUri = notebook.uri.toString(); - if (qsharpCells.length > 0) { - openQSharpNotebooks.add(notebookUri); - languageService.updateNotebookDocument( - notebookUri, - notebook.version, - qsharpMetadata, - qsharpCells.map((cell) => { - return { - uri: cell.document.uri.toString(), - version: cell.document.version, - code: getQSharpText(cell.document), - }; - }), - ); - } else { - // All Q# cells could have been deleted, check if we know this doc from previous calls - closeIfKnownQsharpNotebook(notebook); - } - } - } - - function closeIfKnownQsharpNotebook(notebook: vscode.NotebookDocument) { - const notebookUri = notebook.uri.toString(); - if (openQSharpNotebooks.has(notebookUri)) { - languageService.closeNotebookDocument(notebookUri); - openQSharpNotebooks.delete(notebook.uri.toString()); - } - } - - function getQSharpCells(notebook: vscode.NotebookDocument) { - return notebook - .getCells() - .filter((cell) => isQsharpNotebookCell(cell.document)); - } - - function getQSharpText(document: vscode.TextDocument) { - const magicRange = findQSharpCellMagic(document); - if (magicRange) { - const magicStartOffset = document.offsetAt(magicRange.start); - const magicEndOffset = document.offsetAt(magicRange.end); - // Erase the %%qsharp magic line if it's there. - // Replace it with a comment so that document offsets remain the same. - // This will save us from having to map offsets later when - // communicating with the language service. - const text = document.getText(); - return ( - text.substring(0, magicStartOffset) + - "//qsharp" + - text.substring(magicEndOffset) - ); - } else { - // No %%qsharp magic. This can happen if the user manually sets the - // cell language to Q#. Python won't recognize the cell as a Q# cell, - // so this will fail at runtime, but as the language service we respect - // the manually set cell language, so we treat this as any other - // Q# cell. We could consider raising a warning here to help the user. - log.info( - "found Q# cell without %%qsharp magic: " + document.uri.toString(), - ); - return document.getText(); - } - } - - return subscriptions; -} - -/** - * Finds an output cell that contains an item with the Q# config MIME type, - * and returns the data from it. This data and is generated by the execution of a - * `qsharp.init()` call. It's Q# configuration data to be passed - * to the language service as "notebook metadata". - */ -function getQSharpConfigMetadata(notebook: vscode.NotebookDocument): object { - const data = notebook - .getCells() - .flatMap((cell) => cell.outputs) - .flatMap((output) => output.items) - .find((item) => { - return item.mime === qsharpConfigMimeType; - })?.data; - - if (data) { - const dataString = new TextDecoder().decode(data); - log.trace("found Q# config metadata: " + dataString); - return JSON.parse(dataString); - } else { - return {}; - } -} - // Yes, this function is long, but mostly to deal with multi-folder VS Code workspace or multi // Azure Quantum workspace connection scenarios. The actual notebook creation is pretty simple. export function registerCreateNotebookCommand(