about summary refs log tree commit diff
path: root/editors/code/src/main.ts
diff options
context:
space:
mode:
authorLukas Wirth <lukastw97@gmail.com>2022-10-17 14:20:14 +0200
committerLukas Wirth <lukastw97@gmail.com>2022-10-17 14:21:12 +0200
commit6f435977df34e722b45d787d7eadac23cf5b040e (patch)
treeb13dab6901885f3291773d7c723eac700cfa9519 /editors/code/src/main.ts
parent40cbeb5b3d1ec37e8857844e43c75b6980f588e3 (diff)
downloadrust-6f435977df34e722b45d787d7eadac23cf5b040e.tar.gz
rust-6f435977df34e722b45d787d7eadac23cf5b040e.zip
Refactor language client handling
Diffstat (limited to 'editors/code/src/main.ts')
-rw-r--r--editors/code/src/main.ts294
1 files changed, 54 insertions, 240 deletions
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts
index 41bde4195e0..4ff27e0b551 100644
--- a/editors/code/src/main.ts
+++ b/editors/code/src/main.ts
@@ -1,53 +1,36 @@
 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 { Ctx, Workspace } from "./ctx";
+import { log, isRustDocument } from "./util";
 import { PersistentState } from "./persistent_state";
 import { activateTaskProvider } from "./tasks";
 import { setContextValue } from "./util";
-import { exec } from "child_process";
-
-let ctx: Ctx | undefined;
+import { bootstrap } from "./bootstrap";
+import { Config } from "./config";
 
 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 {
-    client?: lc.LanguageClient;
+    // FIXME: this should be non-optional
+    readonly client?: lc.LanguageClient;
 }
 
 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,8 +48,17 @@ async function tryActivate(context: vscode.ExtensionContext): Promise<RustAnalyz
         return {};
     }
 
-    const config = new Config(context);
+    const workspace: Workspace =
+        folders.length === 0
+            ? {
+                  kind: "Detached Files",
+                  files: rustDocuments,
+              }
+            : { kind: "Workspace Folder" };
+
     const state = new PersistentState(context.globalState);
+    const config = new Config(context);
+
     const serverPath = await bootstrap(context, config, state).catch((err) => {
         let message = "bootstrap error. ";
 
@@ -77,42 +69,43 @@ async function tryActivate(context: vscode.ExtensionContext): Promise<RustAnalyz
         throw new Error(message);
     });
 
-    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));
+    const ctx = new Ctx(context, config, serverPath, workspace);
+    // VS Code doesn't show a notification when an extension fails to activate
+    // so we do it ourselves.
+    return await activateServer(ctx).catch((err) => {
+        void vscode.window.showErrorMessage(`Cannot activate rust-analyzer: ${err.message}`);
+        throw err;
+    });
+}
+
+async function activateServer(ctx: Ctx): Promise<RustAnalyzerExtensionApi> {
+    if (ctx.workspace.kind === "Workspace Folder") {
+        ctx.pushExtCleanup(activateTaskProvider(ctx.config));
     }
-    await initCommonContext(context, ctx);
 
-    warnAboutExtensionConflicts();
+    await ctx.activate();
+    await initCommonContext(ctx);
 
-    if (config.typingContinueCommentsOnNewline) {
-        ctx.pushCleanup(configureLanguage());
+    if (ctx.config.typingContinueCommentsOnNewline) {
+        ctx.pushExtCleanup(configureLanguage());
     }
 
     vscode.workspace.onDidChangeConfiguration(
         (_) =>
-            ctx?.client
-                ?.sendNotification("workspace/didChangeConfiguration", { settings: "" })
+            ctx
+                .getClient()
+                .then((it) =>
+                    it.sendNotification("workspace/didChangeConfiguration", { settings: "" })
+                )
                 .catch(log.error),
         null,
         ctx.subscriptions
     );
 
-    return {
-        client: ctx.client,
-    };
+    return ctx.clientFetcher();
 }
 
-async function initCommonContext(context: vscode.ExtensionContext, ctx: Ctx) {
+async function initCommonContext(ctx: Ctx) {
     // Register a "dumb" onEnter command for the case where server fails to
     // start.
     //
@@ -130,24 +123,15 @@ async function initCommonContext(context: vscode.ExtensionContext, ctx: Ctx) {
     const defaultOnEnter = vscode.commands.registerCommand("rust-analyzer.onEnter", () =>
         vscode.commands.executeCommand("default:type", { text: "\n" })
     );
-    context.subscriptions.push(defaultOnEnter);
+    ctx.pushExtCleanup(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);
+        await ctx.disposeClient();
+        await ctx.activate();
     });
 
     ctx.registerCommand("analyzerStatus", commands.analyzerStatus);
@@ -175,9 +159,6 @@ async function initCommonContext(context: vscode.ExtensionContext, ctx: Ctx) {
     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);
 
@@ -191,176 +172,9 @@ async function initCommonContext(context: vscode.ExtensionContext, ctx: Ctx) {
     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;
-}
-
-async function patchelf(dest: vscode.Uri): Promise<void> {
-    await vscode.window.withProgress(
-        {
-            location: vscode.ProgressLocation.Notification,
-            title: "Patching rust-analyzer for NixOS",
-        },
-        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);
-    }
+    defaultOnEnter.dispose();
+    ctx.registerCommand("onEnter", commands.onEnter);
 }
 
 /**