Skip to content

internal: Refactor language client handling #13426

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Oct 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 32 additions & 32 deletions editors/code/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 12 additions & 1 deletion editors/code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"dependencies": {
"d3": "^7.6.1",
"d3-graphviz": "^4.1.1",
"vscode-languageclient": "^8.0.0-next.14"
"vscode-languageclient": "^8.0.2"
},
"devDependencies": {
"@types/node": "~16.11.7",
Expand All @@ -60,6 +60,7 @@
"onCommand:rust-analyzer.analyzerStatus",
"onCommand:rust-analyzer.memoryUsage",
"onCommand:rust-analyzer.reloadWorkspace",
"onCommand:rust-analyzer.startServer",
"workspaceContains:*/Cargo.toml",
"workspaceContains:*/rust-project.json"
],
Expand Down Expand Up @@ -191,6 +192,16 @@
"title": "Restart server",
"category": "rust-analyzer"
},
{
"command": "rust-analyzer.startServer",
"title": "Start server",
"category": "rust-analyzer"
},
{
"command": "rust-analyzer.stopServer",
"title": "Stop server",
"category": "rust-analyzer"
},
{
"command": "rust-analyzer.onEnter",
"title": "Enhanced enter key",
Expand Down
8 changes: 4 additions & 4 deletions editors/code/src/ast_inspector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ export class AstInspector implements vscode.HoverProvider, vscode.DefinitionProv
});

constructor(ctx: Ctx) {
ctx.pushCleanup(vscode.languages.registerHoverProvider({ scheme: "rust-analyzer" }, this));
ctx.pushCleanup(vscode.languages.registerDefinitionProvider({ language: "rust" }, this));
ctx.pushExtCleanup(
vscode.languages.registerHoverProvider({ scheme: "rust-analyzer" }, this)
);
ctx.pushExtCleanup(vscode.languages.registerDefinitionProvider({ language: "rust" }, this));
vscode.workspace.onDidCloseTextDocument(
this.onDidCloseTextDocument,
this,
Expand All @@ -52,8 +54,6 @@ export class AstInspector implements vscode.HoverProvider, vscode.DefinitionProv
this,
ctx.subscriptions
);

ctx.pushCleanup(this);
}
dispose() {
this.setRustEditor(undefined);
Expand Down
148 changes: 148 additions & 0 deletions editors/code/src/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import * as vscode from "vscode";
import * as os from "os";
import { Config } from "./config";
import { log, isValidExecutable } from "./util";
import { PersistentState } from "./persistent_state";
import { exec } from "child_process";

export 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;
}
}
51 changes: 18 additions & 33 deletions editors/code/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import * as ra from "../src/lsp_ext";
import * as Is from "vscode-languageclient/lib/common/utils/is";
import { assert } from "./util";
import { WorkspaceEdit } from "vscode";
import { Workspace } from "./ctx";
import { substituteVariablesInEnv, substituteVSCodeVariables } from "./config";
import { outputChannel, traceOutputChannel } from "./main";
import { substituteVSCodeVariables } from "./config";
import { randomUUID } from "crypto";

export interface Env {
Expand Down Expand Up @@ -65,43 +63,27 @@ function renderHoverActions(actions: ra.CommandLinkGroup[]): vscode.MarkdownStri
}

export async function createClient(
serverPath: string,
workspace: Workspace,
extraEnv: Env
traceOutputChannel: vscode.OutputChannel,
outputChannel: vscode.OutputChannel,
initializationOptions: vscode.WorkspaceConfiguration,
serverOptions: lc.ServerOptions
): Promise<lc.LanguageClient> {
// '.' Is the fallback if no folder is open
// TODO?: Workspace folders support Uri's (eg: file://test.txt).
// It might be a good idea to test if the uri points to a file.

const newEnv = substituteVariablesInEnv(Object.assign({}, process.env, extraEnv));
const run: lc.Executable = {
command: serverPath,
options: { env: newEnv },
};
const serverOptions: lc.ServerOptions = {
run,
debug: run,
};

let rawInitializationOptions = vscode.workspace.getConfiguration("rust-analyzer");

if (workspace.kind === "Detached Files") {
rawInitializationOptions = {
detachedFiles: workspace.files.map((file) => file.uri.fsPath),
...rawInitializationOptions,
};
}

const initializationOptions = substituteVSCodeVariables(rawInitializationOptions);

const clientOptions: lc.LanguageClientOptions = {
documentSelector: [{ scheme: "file", language: "rust" }],
initializationOptions,
diagnosticCollectionName: "rustc",
traceOutputChannel: traceOutputChannel(),
outputChannel: outputChannel(),
traceOutputChannel,
outputChannel,
middleware: {
workspace: {
// HACK: This is a workaround, when the client has been disposed, VSCode
// continues to emit events to the client and the default one for this event
// attempt to restart the client for no reason
async didChangeWatchedFile(event, next) {
if (client.isRunning()) {
await next(event);
}
},
async configuration(
params: lc.ConfigurationParams,
token: vscode.CancellationToken,
Expand Down Expand Up @@ -273,6 +255,9 @@ export async function createClient(
}

class ExperimentalFeatures implements lc.StaticFeature {
getState(): lc.FeatureState {
return { kind: "static" };
}
fillClientCapabilities(capabilities: lc.ClientCapabilities): void {
const caps: any = capabilities.experimental ?? {};
caps.snippetTextEdit = true;
Expand Down
Loading