Skip to content

Commit 6f43597

Browse files
committed
Refactor language client handling
1 parent 40cbeb5 commit 6f43597

File tree

8 files changed

+460
-441
lines changed

8 files changed

+460
-441
lines changed

editors/code/package-lock.json

Lines changed: 39 additions & 39 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

editors/code/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@
3737
"dependencies": {
3838
"d3": "^7.6.1",
3939
"d3-graphviz": "^4.1.1",
40-
"vscode-languageclient": "^8.0.0-next.14"
40+
"vscode-languageclient": "^8.0.2"
4141
},
4242
"devDependencies": {
4343
"@types/node": "~16.11.7",
44-
"@types/vscode": "~1.66.0",
44+
"@types/vscode": "~1.72.0",
4545
"@typescript-eslint/eslint-plugin": "^5.30.5",
4646
"@typescript-eslint/parser": "^5.30.5",
4747
"@vscode/test-electron": "^2.1.5",

editors/code/src/ast_inspector.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@ export class AstInspector implements vscode.HoverProvider, vscode.DefinitionProv
3535
});
3636

3737
constructor(ctx: Ctx) {
38-
ctx.pushCleanup(vscode.languages.registerHoverProvider({ scheme: "rust-analyzer" }, this));
39-
ctx.pushCleanup(vscode.languages.registerDefinitionProvider({ language: "rust" }, this));
38+
ctx.pushExtCleanup(
39+
vscode.languages.registerHoverProvider({ scheme: "rust-analyzer" }, this)
40+
);
41+
ctx.pushExtCleanup(vscode.languages.registerDefinitionProvider({ language: "rust" }, this));
4042
vscode.workspace.onDidCloseTextDocument(
4143
this.onDidCloseTextDocument,
4244
this,
@@ -53,7 +55,7 @@ export class AstInspector implements vscode.HoverProvider, vscode.DefinitionProv
5355
ctx.subscriptions
5456
);
5557

56-
ctx.pushCleanup(this);
58+
ctx.pushExtCleanup(this);
5759
}
5860
dispose() {
5961
this.setRustEditor(undefined);

editors/code/src/bootstrap.ts

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import * as vscode from "vscode";
2+
import * as os from "os";
3+
import { Config } from "./config";
4+
import { log, isValidExecutable } from "./util";
5+
import { PersistentState } from "./persistent_state";
6+
import { exec } from "child_process";
7+
8+
export async function bootstrap(
9+
context: vscode.ExtensionContext,
10+
config: Config,
11+
state: PersistentState
12+
): Promise<string> {
13+
const path = await getServer(context, config, state);
14+
if (!path) {
15+
throw new Error(
16+
"Rust Analyzer Language Server is not available. " +
17+
"Please, ensure its [proper installation](https://rust-analyzer.github.io/manual.html#installation)."
18+
);
19+
}
20+
21+
log.info("Using server binary at", path);
22+
23+
if (!isValidExecutable(path)) {
24+
if (config.serverPath) {
25+
throw new Error(`Failed to execute ${path} --version. \`config.server.path\` or \`config.serverPath\` has been set explicitly.\
26+
Consider removing this config or making a valid server binary available at that path.`);
27+
} else {
28+
throw new Error(`Failed to execute ${path} --version`);
29+
}
30+
}
31+
32+
return path;
33+
}
34+
35+
async function patchelf(dest: vscode.Uri): Promise<void> {
36+
await vscode.window.withProgress(
37+
{
38+
location: vscode.ProgressLocation.Notification,
39+
title: "Patching rust-analyzer for NixOS",
40+
},
41+
async (progress, _) => {
42+
const expression = `
43+
{srcStr, pkgs ? import <nixpkgs> {}}:
44+
pkgs.stdenv.mkDerivation {
45+
name = "rust-analyzer";
46+
src = /. + srcStr;
47+
phases = [ "installPhase" "fixupPhase" ];
48+
installPhase = "cp $src $out";
49+
fixupPhase = ''
50+
chmod 755 $out
51+
patchelf --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" $out
52+
'';
53+
}
54+
`;
55+
const origFile = vscode.Uri.file(dest.fsPath + "-orig");
56+
await vscode.workspace.fs.rename(dest, origFile, { overwrite: true });
57+
try {
58+
progress.report({ message: "Patching executable", increment: 20 });
59+
await new Promise((resolve, reject) => {
60+
const handle = exec(
61+
`nix-build -E - --argstr srcStr '${origFile.fsPath}' -o '${dest.fsPath}'`,
62+
(err, stdout, stderr) => {
63+
if (err != null) {
64+
reject(Error(stderr));
65+
} else {
66+
resolve(stdout);
67+
}
68+
}
69+
);
70+
handle.stdin?.write(expression);
71+
handle.stdin?.end();
72+
});
73+
} finally {
74+
await vscode.workspace.fs.delete(origFile);
75+
}
76+
}
77+
);
78+
}
79+
80+
async function getServer(
81+
context: vscode.ExtensionContext,
82+
config: Config,
83+
state: PersistentState
84+
): Promise<string | undefined> {
85+
const explicitPath = serverPath(config);
86+
if (explicitPath) {
87+
if (explicitPath.startsWith("~/")) {
88+
return os.homedir() + explicitPath.slice("~".length);
89+
}
90+
return explicitPath;
91+
}
92+
if (config.package.releaseTag === null) return "rust-analyzer";
93+
94+
const ext = process.platform === "win32" ? ".exe" : "";
95+
const bundled = vscode.Uri.joinPath(context.extensionUri, "server", `rust-analyzer${ext}`);
96+
const bundledExists = await vscode.workspace.fs.stat(bundled).then(
97+
() => true,
98+
() => false
99+
);
100+
if (bundledExists) {
101+
let server = bundled;
102+
if (await isNixOs()) {
103+
await vscode.workspace.fs.createDirectory(config.globalStorageUri).then();
104+
const dest = vscode.Uri.joinPath(config.globalStorageUri, `rust-analyzer${ext}`);
105+
let exists = await vscode.workspace.fs.stat(dest).then(
106+
() => true,
107+
() => false
108+
);
109+
if (exists && config.package.version !== state.serverVersion) {
110+
await vscode.workspace.fs.delete(dest);
111+
exists = false;
112+
}
113+
if (!exists) {
114+
await vscode.workspace.fs.copy(bundled, dest);
115+
await patchelf(dest);
116+
}
117+
server = dest;
118+
}
119+
await state.updateServerVersion(config.package.version);
120+
return server.fsPath;
121+
}
122+
123+
await state.updateServerVersion(undefined);
124+
await vscode.window.showErrorMessage(
125+
"Unfortunately we don't ship binaries for your platform yet. " +
126+
"You need to manually clone the rust-analyzer repository and " +
127+
"run `cargo xtask install --server` to build the language server from sources. " +
128+
"If you feel that your platform should be supported, please create an issue " +
129+
"about that [here](https://github.com/rust-lang/rust-analyzer/issues) and we " +
130+
"will consider it."
131+
);
132+
return undefined;
133+
}
134+
function serverPath(config: Config): string | null {
135+
return process.env.__RA_LSP_SERVER_DEBUG ?? config.serverPath;
136+
}
137+
138+
async function isNixOs(): Promise<boolean> {
139+
try {
140+
const contents = (
141+
await vscode.workspace.fs.readFile(vscode.Uri.file("/etc/os-release"))
142+
).toString();
143+
const idString = contents.split("\n").find((a) => a.startsWith("ID=")) || "ID=linux";
144+
return idString.indexOf("nixos") !== -1;
145+
} catch {
146+
return false;
147+
}
148+
}

editors/code/src/client.ts

Lines changed: 10 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@ import * as ra from "../src/lsp_ext";
44
import * as Is from "vscode-languageclient/lib/common/utils/is";
55
import { assert } from "./util";
66
import { WorkspaceEdit } from "vscode";
7-
import { Workspace } from "./ctx";
8-
import { substituteVariablesInEnv, substituteVSCodeVariables } from "./config";
9-
import { outputChannel, traceOutputChannel } from "./main";
7+
import { substituteVSCodeVariables } from "./config";
108
import { randomUUID } from "crypto";
119

1210
export interface Env {
@@ -65,41 +63,17 @@ function renderHoverActions(actions: ra.CommandLinkGroup[]): vscode.MarkdownStri
6563
}
6664

6765
export async function createClient(
68-
serverPath: string,
69-
workspace: Workspace,
70-
extraEnv: Env
66+
traceOutputChannel: vscode.OutputChannel,
67+
outputChannel: vscode.OutputChannel,
68+
initializationOptions: vscode.WorkspaceConfiguration,
69+
serverOptions: lc.ServerOptions
7170
): Promise<lc.LanguageClient> {
72-
// '.' Is the fallback if no folder is open
73-
// TODO?: Workspace folders support Uri's (eg: file://test.txt).
74-
// It might be a good idea to test if the uri points to a file.
75-
76-
const newEnv = substituteVariablesInEnv(Object.assign({}, process.env, extraEnv));
77-
const run: lc.Executable = {
78-
command: serverPath,
79-
options: { env: newEnv },
80-
};
81-
const serverOptions: lc.ServerOptions = {
82-
run,
83-
debug: run,
84-
};
85-
86-
let rawInitializationOptions = vscode.workspace.getConfiguration("rust-analyzer");
87-
88-
if (workspace.kind === "Detached Files") {
89-
rawInitializationOptions = {
90-
detachedFiles: workspace.files.map((file) => file.uri.fsPath),
91-
...rawInitializationOptions,
92-
};
93-
}
94-
95-
const initializationOptions = substituteVSCodeVariables(rawInitializationOptions);
96-
9771
const clientOptions: lc.LanguageClientOptions = {
9872
documentSelector: [{ scheme: "file", language: "rust" }],
9973
initializationOptions,
10074
diagnosticCollectionName: "rustc",
101-
traceOutputChannel: traceOutputChannel(),
102-
outputChannel: outputChannel(),
75+
traceOutputChannel,
76+
outputChannel,
10377
middleware: {
10478
workspace: {
10579
async configuration(
@@ -273,6 +247,9 @@ export async function createClient(
273247
}
274248

275249
class ExperimentalFeatures implements lc.StaticFeature {
250+
getState(): lc.FeatureState {
251+
return { kind: "static" };
252+
}
276253
fillClientCapabilities(capabilities: lc.ClientCapabilities): void {
277254
const caps: any = capabilities.experimental ?? {};
278255
caps.snippetTextEdit = true;

0 commit comments

Comments
 (0)