about summary refs log tree commit diff
path: root/editors/code/src/main.ts
diff options
context:
space:
mode:
authorLaurențiu Nicola <lnicola@dend.ro>2022-10-26 17:40:41 +0300
committerLaurențiu Nicola <lnicola@dend.ro>2022-10-26 17:40:41 +0300
commit8807fc4cc358fa2152b303df0caa2f5fc9efaa9d (patch)
treeda701483c74d2d402617c2482626b70aa2fff536 /editors/code/src/main.ts
parent26a413e015b7fef4c0edb51bda0d39ab499f4950 (diff)
downloadrust-8807fc4cc358fa2152b303df0caa2f5fc9efaa9d.tar.gz
rust-8807fc4cc358fa2152b303df0caa2f5fc9efaa9d.zip
:arrow_up: rust-analyzer
Diffstat (limited to 'editors/code/src/main.ts')
-rw-r--r--editors/code/src/main.ts475
1 files changed, 113 insertions, 362 deletions
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts
index 41bde4195e0..8c3a676ffb0 100644
--- a/editors/code/src/main.ts
+++ b/editors/code/src/main.ts
@@ -1,53 +1,37 @@
 import * as vscode from "vscode";
 import * as lc from "vscode-languageclient/node";
-import * as os from "os";
 
 import * as commands from "./commands";
-import { Ctx } from "./ctx";
-import { Config } from "./config";
-import { log, isValidExecutable, isRustDocument } from "./util";
-import { PersistentState } from "./persistent_state";
+import { CommandFactory, Ctx, Workspace } from "./ctx";
+import { isRustDocument } from "./util";
 import { activateTaskProvider } from "./tasks";
 import { setContextValue } from "./util";
-import { exec } from "child_process";
-
-let ctx: Ctx | undefined;
 
 const RUST_PROJECT_CONTEXT_NAME = "inRustProject";
 
-let TRACE_OUTPUT_CHANNEL: vscode.OutputChannel | null = null;
-export function traceOutputChannel() {
-    if (!TRACE_OUTPUT_CHANNEL) {
-        TRACE_OUTPUT_CHANNEL = vscode.window.createOutputChannel(
-            "Rust Analyzer Language Server Trace"
-        );
-    }
-    return TRACE_OUTPUT_CHANNEL;
-}
-let OUTPUT_CHANNEL: vscode.OutputChannel | null = null;
-export function outputChannel() {
-    if (!OUTPUT_CHANNEL) {
-        OUTPUT_CHANNEL = vscode.window.createOutputChannel("Rust Analyzer Language Server");
-    }
-    return OUTPUT_CHANNEL;
+export interface RustAnalyzerExtensionApi {
+    // FIXME: this should be non-optional
+    readonly client?: lc.LanguageClient;
 }
 
-export interface RustAnalyzerExtensionApi {
-    client?: lc.LanguageClient;
+export async function deactivate() {
+    await setContextValue(RUST_PROJECT_CONTEXT_NAME, undefined);
 }
 
 export async function activate(
     context: vscode.ExtensionContext
 ): Promise<RustAnalyzerExtensionApi> {
-    // VS Code doesn't show a notification when an extension fails to activate
-    // so we do it ourselves.
-    return await tryActivate(context).catch((err) => {
-        void vscode.window.showErrorMessage(`Cannot activate rust-analyzer: ${err.message}`);
-        throw err;
-    });
-}
+    if (vscode.extensions.getExtension("rust-lang.rust")) {
+        vscode.window
+            .showWarningMessage(
+                `You have both the rust-analyzer (rust-lang.rust-analyzer) and Rust (rust-lang.rust) ` +
+                    "plugins enabled. These are known to conflict and cause various functions of " +
+                    "both plugins to not work correctly. You should disable one of them.",
+                "Got it"
+            )
+            .then(() => {}, console.error);
+    }
 
-async function tryActivate(context: vscode.ExtensionContext): Promise<RustAnalyzerExtensionApi> {
     // We only support local folders, not eg. Live Share (`vlsl:` scheme), so don't activate if
     // only those are in use.
     // (r-a still somewhat works with Live Share, because commands are tunneled to the host)
@@ -65,351 +49,118 @@ async function tryActivate(context: vscode.ExtensionContext): Promise<RustAnalyz
         return {};
     }
 
-    const config = new Config(context);
-    const state = new PersistentState(context.globalState);
-    const serverPath = await bootstrap(context, config, state).catch((err) => {
-        let message = "bootstrap error. ";
-
-        message += 'See the logs in "OUTPUT > Rust Analyzer Client" (should open automatically). ';
-        message += 'To enable verbose logs use { "rust-analyzer.trace.extension": true }';
+    const workspace: Workspace =
+        folders.length === 0
+            ? {
+                  kind: "Detached Files",
+                  files: rustDocuments,
+              }
+            : { kind: "Workspace Folder" };
 
-        log.error("Bootstrap error", err);
-        throw new Error(message);
+    const ctx = new Ctx(context, workspace, createCommands());
+    // VS Code doesn't show a notification when an extension fails to activate
+    // so we do it ourselves.
+    const api = await activateServer(ctx).catch((err) => {
+        void vscode.window.showErrorMessage(
+            `Cannot activate rust-analyzer extension: ${err.message}`
+        );
+        throw err;
     });
+    await setContextValue(RUST_PROJECT_CONTEXT_NAME, true);
+    return api;
+}
 
-    if (folders.length === 0) {
-        ctx = await Ctx.create(config, context, serverPath, {
-            kind: "Detached Files",
-            files: rustDocuments,
-        });
-    } else {
-        // Note: we try to start the server before we activate type hints so that it
-        // registers its `onDidChangeDocument` handler before us.
-        //
-        // This a horribly, horribly wrong way to deal with this problem.
-        ctx = await Ctx.create(config, context, serverPath, { kind: "Workspace Folder" });
-        ctx.pushCleanup(activateTaskProvider(ctx.config));
-    }
-    await initCommonContext(context, ctx);
-
-    warnAboutExtensionConflicts();
-
-    if (config.typingContinueCommentsOnNewline) {
-        ctx.pushCleanup(configureLanguage());
+async function activateServer(ctx: Ctx): Promise<RustAnalyzerExtensionApi> {
+    if (ctx.workspace.kind === "Workspace Folder") {
+        ctx.pushExtCleanup(activateTaskProvider(ctx.config));
     }
 
     vscode.workspace.onDidChangeConfiguration(
-        (_) =>
-            ctx?.client
-                ?.sendNotification("workspace/didChangeConfiguration", { settings: "" })
-                .catch(log.error),
+        async (_) => {
+            await ctx
+                .clientFetcher()
+                .client?.sendNotification("workspace/didChangeConfiguration", { settings: "" });
+        },
         null,
         ctx.subscriptions
     );
 
-    return {
-        client: ctx.client,
-    };
-}
-
-async function initCommonContext(context: vscode.ExtensionContext, ctx: Ctx) {
-    // Register a "dumb" onEnter command for the case where server fails to
-    // start.
-    //
-    // FIXME: refactor command registration code such that commands are
-    // **always** registered, even if the server does not start. Use API like
-    // this perhaps?
-    //
-    // ```TypeScript
-    // registerCommand(
-    //    factory: (Ctx) => ((Ctx) => any),
-    //    fallback: () => any = () => vscode.window.showErrorMessage(
-    //        "rust-analyzer is not available"
-    //    ),
-    // )
-    const defaultOnEnter = vscode.commands.registerCommand("rust-analyzer.onEnter", () =>
-        vscode.commands.executeCommand("default:type", { text: "\n" })
-    );
-    context.subscriptions.push(defaultOnEnter);
-
-    await setContextValue(RUST_PROJECT_CONTEXT_NAME, true);
-
-    // Commands which invokes manually via command palette, shortcut, etc.
-
-    // Reloading is inspired by @DanTup maneuver: https://github.com/microsoft/vscode/issues/45774#issuecomment-373423895
-    ctx.registerCommand("reload", (_) => async () => {
-        void vscode.window.showInformationMessage("Reloading rust-analyzer...");
-        await doDeactivate();
-        while (context.subscriptions.length > 0) {
-            try {
-                context.subscriptions.pop()!.dispose();
-            } catch (err) {
-                log.error("Dispose error:", err);
-            }
-        }
-        await activate(context).catch(log.error);
-    });
-
-    ctx.registerCommand("analyzerStatus", commands.analyzerStatus);
-    ctx.registerCommand("memoryUsage", commands.memoryUsage);
-    ctx.registerCommand("shuffleCrateGraph", commands.shuffleCrateGraph);
-    ctx.registerCommand("reloadWorkspace", commands.reloadWorkspace);
-    ctx.registerCommand("matchingBrace", commands.matchingBrace);
-    ctx.registerCommand("joinLines", commands.joinLines);
-    ctx.registerCommand("parentModule", commands.parentModule);
-    ctx.registerCommand("syntaxTree", commands.syntaxTree);
-    ctx.registerCommand("viewHir", commands.viewHir);
-    ctx.registerCommand("viewFileText", commands.viewFileText);
-    ctx.registerCommand("viewItemTree", commands.viewItemTree);
-    ctx.registerCommand("viewCrateGraph", commands.viewCrateGraph);
-    ctx.registerCommand("viewFullCrateGraph", commands.viewFullCrateGraph);
-    ctx.registerCommand("expandMacro", commands.expandMacro);
-    ctx.registerCommand("run", commands.run);
-    ctx.registerCommand("copyRunCommandLine", commands.copyRunCommandLine);
-    ctx.registerCommand("debug", commands.debug);
-    ctx.registerCommand("newDebugConfig", commands.newDebugConfig);
-    ctx.registerCommand("openDocs", commands.openDocs);
-    ctx.registerCommand("openCargoToml", commands.openCargoToml);
-    ctx.registerCommand("peekTests", commands.peekTests);
-    ctx.registerCommand("moveItemUp", commands.moveItemUp);
-    ctx.registerCommand("moveItemDown", commands.moveItemDown);
-    ctx.registerCommand("cancelFlycheck", commands.cancelFlycheck);
-
-    defaultOnEnter.dispose();
-    ctx.registerCommand("onEnter", commands.onEnter);
-
-    ctx.registerCommand("ssr", commands.ssr);
-    ctx.registerCommand("serverVersion", commands.serverVersion);
-
-    // Internal commands which are invoked by the server.
-    ctx.registerCommand("runSingle", commands.runSingle);
-    ctx.registerCommand("debugSingle", commands.debugSingle);
-    ctx.registerCommand("showReferences", commands.showReferences);
-    ctx.registerCommand("applySnippetWorkspaceEdit", commands.applySnippetWorkspaceEditCommand);
-    ctx.registerCommand("resolveCodeAction", commands.resolveCodeAction);
-    ctx.registerCommand("applyActionGroup", commands.applyActionGroup);
-    ctx.registerCommand("gotoLocation", commands.gotoLocation);
-
-    ctx.registerCommand("linkToCommand", commands.linkToCommand);
-}
-
-export async function deactivate() {
-    TRACE_OUTPUT_CHANNEL?.dispose();
-    TRACE_OUTPUT_CHANNEL = null;
-    OUTPUT_CHANNEL?.dispose();
-    OUTPUT_CHANNEL = null;
-    await doDeactivate();
-}
-
-async function doDeactivate() {
-    await setContextValue(RUST_PROJECT_CONTEXT_NAME, undefined);
-    await ctx?.client.stop();
-    ctx = undefined;
-}
-
-async function bootstrap(
-    context: vscode.ExtensionContext,
-    config: Config,
-    state: PersistentState
-): Promise<string> {
-    const path = await getServer(context, config, state);
-    if (!path) {
-        throw new Error(
-            "Rust Analyzer Language Server is not available. " +
-                "Please, ensure its [proper installation](https://rust-analyzer.github.io/manual.html#installation)."
-        );
-    }
-
-    log.info("Using server binary at", path);
-
-    if (!isValidExecutable(path)) {
-        if (config.serverPath) {
-            throw new Error(`Failed to execute ${path} --version. \`config.server.path\` or \`config.serverPath\` has been set explicitly.\
-            Consider removing this config or making a valid server binary available at that path.`);
-        } else {
-            throw new Error(`Failed to execute ${path} --version`);
-        }
-    }
-
-    return path;
+    await ctx.activate();
+    return ctx.clientFetcher();
 }
 
-async function patchelf(dest: vscode.Uri): Promise<void> {
-    await vscode.window.withProgress(
-        {
-            location: vscode.ProgressLocation.Notification,
-            title: "Patching rust-analyzer for NixOS",
+function createCommands(): Record<string, CommandFactory> {
+    return {
+        onEnter: {
+            enabled: commands.onEnter,
+            disabled: (_) => () => vscode.commands.executeCommand("default:type", { text: "\n" }),
         },
-        async (progress, _) => {
-            const expression = `
-            {srcStr, pkgs ? import <nixpkgs> {}}:
-                pkgs.stdenv.mkDerivation {
-                    name = "rust-analyzer";
-                    src = /. + srcStr;
-                    phases = [ "installPhase" "fixupPhase" ];
-                    installPhase = "cp $src $out";
-                    fixupPhase = ''
-                    chmod 755 $out
-                    patchelf --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" $out
-                    '';
-                }
-            `;
-            const origFile = vscode.Uri.file(dest.fsPath + "-orig");
-            await vscode.workspace.fs.rename(dest, origFile, { overwrite: true });
-            try {
-                progress.report({ message: "Patching executable", increment: 20 });
-                await new Promise((resolve, reject) => {
-                    const handle = exec(
-                        `nix-build -E - --argstr srcStr '${origFile.fsPath}' -o '${dest.fsPath}'`,
-                        (err, stdout, stderr) => {
-                            if (err != null) {
-                                reject(Error(stderr));
-                            } else {
-                                resolve(stdout);
-                            }
-                        }
-                    );
-                    handle.stdin?.write(expression);
-                    handle.stdin?.end();
-                });
-            } finally {
-                await vscode.workspace.fs.delete(origFile);
-            }
-        }
-    );
-}
-
-async function getServer(
-    context: vscode.ExtensionContext,
-    config: Config,
-    state: PersistentState
-): Promise<string | undefined> {
-    const explicitPath = serverPath(config);
-    if (explicitPath) {
-        if (explicitPath.startsWith("~/")) {
-            return os.homedir() + explicitPath.slice("~".length);
-        }
-        return explicitPath;
-    }
-    if (config.package.releaseTag === null) return "rust-analyzer";
-
-    const ext = process.platform === "win32" ? ".exe" : "";
-    const bundled = vscode.Uri.joinPath(context.extensionUri, "server", `rust-analyzer${ext}`);
-    const bundledExists = await vscode.workspace.fs.stat(bundled).then(
-        () => true,
-        () => false
-    );
-    if (bundledExists) {
-        let server = bundled;
-        if (await isNixOs()) {
-            await vscode.workspace.fs.createDirectory(config.globalStorageUri).then();
-            const dest = vscode.Uri.joinPath(config.globalStorageUri, `rust-analyzer${ext}`);
-            let exists = await vscode.workspace.fs.stat(dest).then(
-                () => true,
-                () => false
-            );
-            if (exists && config.package.version !== state.serverVersion) {
-                await vscode.workspace.fs.delete(dest);
-                exists = false;
-            }
-            if (!exists) {
-                await vscode.workspace.fs.copy(bundled, dest);
-                await patchelf(dest);
-            }
-            server = dest;
-        }
-        await state.updateServerVersion(config.package.version);
-        return server.fsPath;
-    }
-
-    await state.updateServerVersion(undefined);
-    await vscode.window.showErrorMessage(
-        "Unfortunately we don't ship binaries for your platform yet. " +
-            "You need to manually clone the rust-analyzer repository and " +
-            "run `cargo xtask install --server` to build the language server from sources. " +
-            "If you feel that your platform should be supported, please create an issue " +
-            "about that [here](https://github.com/rust-lang/rust-analyzer/issues) and we " +
-            "will consider it."
-    );
-    return undefined;
-}
-
-function serverPath(config: Config): string | null {
-    return process.env.__RA_LSP_SERVER_DEBUG ?? config.serverPath;
-}
-
-async function isNixOs(): Promise<boolean> {
-    try {
-        const contents = (
-            await vscode.workspace.fs.readFile(vscode.Uri.file("/etc/os-release"))
-        ).toString();
-        const idString = contents.split("\n").find((a) => a.startsWith("ID=")) || "ID=linux";
-        return idString.indexOf("nixos") !== -1;
-    } catch {
-        return false;
-    }
-}
-
-function warnAboutExtensionConflicts() {
-    if (vscode.extensions.getExtension("rust-lang.rust")) {
-        vscode.window
-            .showWarningMessage(
-                `You have both the rust-analyzer (rust-lang.rust-analyzer) and Rust (rust-lang.rust) ` +
-                    "plugins enabled. These are known to conflict and cause various functions of " +
-                    "both plugins to not work correctly. You should disable one of them.",
-                "Got it"
-            )
-            .then(() => {}, console.error);
-    }
-}
-
-/**
- * Sets up additional language configuration that's impossible to do via a
- * separate language-configuration.json file. See [1] for more information.
- *
- * [1]: https://github.com/Microsoft/vscode/issues/11514#issuecomment-244707076
- */
-function configureLanguage(): vscode.Disposable {
-    const indentAction = vscode.IndentAction.None;
-    return vscode.languages.setLanguageConfiguration("rust", {
-        onEnterRules: [
-            {
-                // Doc single-line comment
-                // e.g. ///|
-                beforeText: /^\s*\/{3}.*$/,
-                action: { indentAction, appendText: "/// " },
-            },
-            {
-                // Parent doc single-line comment
-                // e.g. //!|
-                beforeText: /^\s*\/{2}\!.*$/,
-                action: { indentAction, appendText: "//! " },
+        reload: {
+            enabled: (ctx) => async () => {
+                void vscode.window.showInformationMessage("Reloading rust-analyzer...");
+                // FIXME: We should re-use the client, that is ctx.deactivate() if none of the configs have changed
+                await ctx.stop();
+                await ctx.activate();
             },
-            {
-                // Begins an auto-closed multi-line comment (standard or parent doc)
-                // e.g. /** | */ or /*! | */
-                beforeText: /^\s*\/\*(\*|\!)(?!\/)([^\*]|\*(?!\/))*$/,
-                afterText: /^\s*\*\/$/,
-                action: { indentAction: vscode.IndentAction.IndentOutdent, appendText: " * " },
+            disabled: (ctx) => async () => {
+                void vscode.window.showInformationMessage("Reloading rust-analyzer...");
+                await ctx.activate();
             },
-            {
-                // Begins a multi-line comment (standard or parent doc)
-                // e.g. /** ...| or /*! ...|
-                beforeText: /^\s*\/\*(\*|\!)(?!\/)([^\*]|\*(?!\/))*$/,
-                action: { indentAction, appendText: " * " },
+        },
+        startServer: {
+            enabled: (ctx) => async () => {
+                await ctx.activate();
             },
-            {
-                // Continues a multi-line comment
-                // e.g.  * ...|
-                beforeText: /^(\ \ )*\ \*(\ ([^\*]|\*(?!\/))*)?$/,
-                action: { indentAction, appendText: "* " },
+            disabled: (ctx) => async () => {
+                await ctx.activate();
             },
-            {
-                // Dedents after closing a multi-line comment
-                // e.g.  */|
-                beforeText: /^(\ \ )*\ \*\/\s*$/,
-                action: { indentAction, removeText: 1 },
+        },
+        stopServer: {
+            enabled: (ctx) => async () => {
+                // FIXME: We should re-use the client, that is ctx.deactivate() if none of the configs have changed
+                await ctx.stop();
+                ctx.setServerStatus({
+                    health: "ok",
+                    quiescent: true,
+                    message: "server is not running",
+                });
             },
-        ],
-    });
+        },
+
+        analyzerStatus: { enabled: commands.analyzerStatus },
+        memoryUsage: { enabled: commands.memoryUsage },
+        shuffleCrateGraph: { enabled: commands.shuffleCrateGraph },
+        reloadWorkspace: { enabled: commands.reloadWorkspace },
+        matchingBrace: { enabled: commands.matchingBrace },
+        joinLines: { enabled: commands.joinLines },
+        parentModule: { enabled: commands.parentModule },
+        syntaxTree: { enabled: commands.syntaxTree },
+        viewHir: { enabled: commands.viewHir },
+        viewFileText: { enabled: commands.viewFileText },
+        viewItemTree: { enabled: commands.viewItemTree },
+        viewCrateGraph: { enabled: commands.viewCrateGraph },
+        viewFullCrateGraph: { enabled: commands.viewFullCrateGraph },
+        expandMacro: { enabled: commands.expandMacro },
+        run: { enabled: commands.run },
+        copyRunCommandLine: { enabled: commands.copyRunCommandLine },
+        debug: { enabled: commands.debug },
+        newDebugConfig: { enabled: commands.newDebugConfig },
+        openDocs: { enabled: commands.openDocs },
+        openCargoToml: { enabled: commands.openCargoToml },
+        peekTests: { enabled: commands.peekTests },
+        moveItemUp: { enabled: commands.moveItemUp },
+        moveItemDown: { enabled: commands.moveItemDown },
+        cancelFlycheck: { enabled: commands.cancelFlycheck },
+        ssr: { enabled: commands.ssr },
+        serverVersion: { enabled: commands.serverVersion },
+        // Internal commands which are invoked by the server.
+        applyActionGroup: { enabled: commands.applyActionGroup },
+        applySnippetWorkspaceEdit: { enabled: commands.applySnippetWorkspaceEditCommand },
+        debugSingle: { enabled: commands.debugSingle },
+        gotoLocation: { enabled: commands.gotoLocation },
+        linkToCommand: { enabled: commands.linkToCommand },
+        resolveCodeAction: { enabled: commands.resolveCodeAction },
+        runSingle: { enabled: commands.runSingle },
+        showReferences: { enabled: commands.showReferences },
+    };
 }