Auto merge of #13426 - Veykril:client-refactor, r=Veykril

Refactor language client handling

Follow up to https://github.com/rust-lang/rust-analyzer/pull/12847 (turns out they fixed parts of the problem)

The PR will attempt to allow us to dispose more resources at will, so that we can implement restarts for the server properly instead of restating the entire extension as well as allowing us to implement a stop command.

Closes https://github.com/rust-lang/rust-analyzer/issues/12936
Closes https://github.com/rust-lang/rust-analyzer/issues/4697
This commit is contained in:
bors 2022-10-17 14:12:01 +00:00
commit 067c410c45
10 changed files with 504 additions and 465 deletions

View File

@ -11,7 +11,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",
@ -3791,39 +3791,39 @@
}
},
"node_modules/vscode-jsonrpc": {
"version": "8.0.0-next.7",
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.0.0-next.7.tgz",
"integrity": "sha512-JX/F31LEsims0dAlOTKFE4E+AJMiJvdRSRViifFJSqSN7EzeYyWlfuDchF7g91oRNPZOIWfibTkDf3/UMsQGzQ==",
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.0.2.tgz",
"integrity": "sha512-RY7HwI/ydoC1Wwg4gJ3y6LpU9FJRZAUnTYMXthqhFXXu77ErDd/xkREpGuk4MyYkk4a+XDWAMqe0S3KkelYQEQ==",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/vscode-languageclient": {
"version": "8.0.0-next.14",
"resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.0.0-next.14.tgz",
"integrity": "sha512-NqjkOuDTMu8uo+PhoMsV72VO9Gd3wBi/ZpOrkRUOrWKQo7yUdiIw183g8wjH8BImgbK9ZP51HM7TI0ZhCnI1Mw==",
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.0.2.tgz",
"integrity": "sha512-lHlthJtphG9gibGb/y72CKqQUxwPsMXijJVpHEC2bvbFqxmkj9LwQ3aGU9dwjBLqsX1S4KjShYppLvg1UJDF/Q==",
"dependencies": {
"minimatch": "^3.0.4",
"semver": "^7.3.5",
"vscode-languageserver-protocol": "3.17.0-next.16"
"vscode-languageserver-protocol": "3.17.2"
},
"engines": {
"vscode": "^1.66.0"
"vscode": "^1.67.0"
}
},
"node_modules/vscode-languageserver-protocol": {
"version": "3.17.0-next.16",
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.0-next.16.tgz",
"integrity": "sha512-tx4DnXw9u3N7vw+bx6n2NKp6FoxoNwiP/biH83AS30I2AnTGyLd7afSeH6Oewn2E8jvB7K15bs12sMppkKOVeQ==",
"version": "3.17.2",
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.2.tgz",
"integrity": "sha512-8kYisQ3z/SQ2kyjlNeQxbkkTNmVFoQCqkmGrzLH6A9ecPlgTbp3wDTnUNqaUxYr4vlAcloxx8zwy7G5WdguYNg==",
"dependencies": {
"vscode-jsonrpc": "8.0.0-next.7",
"vscode-languageserver-types": "3.17.0-next.9"
"vscode-jsonrpc": "8.0.2",
"vscode-languageserver-types": "3.17.2"
}
},
"node_modules/vscode-languageserver-types": {
"version": "3.17.0-next.9",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.0-next.9.tgz",
"integrity": "sha512-9/PeDNPYduaoXRUzYpqmu4ZV9L01HGo0wH9FUt+sSHR7IXwA7xoXBfNUlv8gB9H0D2WwEmMomSy1NmhjKQyn3A=="
"version": "3.17.2",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz",
"integrity": "sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA=="
},
"node_modules/which": {
"version": "2.0.2",
@ -6634,33 +6634,33 @@
}
},
"vscode-jsonrpc": {
"version": "8.0.0-next.7",
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.0.0-next.7.tgz",
"integrity": "sha512-JX/F31LEsims0dAlOTKFE4E+AJMiJvdRSRViifFJSqSN7EzeYyWlfuDchF7g91oRNPZOIWfibTkDf3/UMsQGzQ=="
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.0.2.tgz",
"integrity": "sha512-RY7HwI/ydoC1Wwg4gJ3y6LpU9FJRZAUnTYMXthqhFXXu77ErDd/xkREpGuk4MyYkk4a+XDWAMqe0S3KkelYQEQ=="
},
"vscode-languageclient": {
"version": "8.0.0-next.14",
"resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.0.0-next.14.tgz",
"integrity": "sha512-NqjkOuDTMu8uo+PhoMsV72VO9Gd3wBi/ZpOrkRUOrWKQo7yUdiIw183g8wjH8BImgbK9ZP51HM7TI0ZhCnI1Mw==",
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.0.2.tgz",
"integrity": "sha512-lHlthJtphG9gibGb/y72CKqQUxwPsMXijJVpHEC2bvbFqxmkj9LwQ3aGU9dwjBLqsX1S4KjShYppLvg1UJDF/Q==",
"requires": {
"minimatch": "^3.0.4",
"semver": "^7.3.5",
"vscode-languageserver-protocol": "3.17.0-next.16"
"vscode-languageserver-protocol": "3.17.2"
}
},
"vscode-languageserver-protocol": {
"version": "3.17.0-next.16",
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.0-next.16.tgz",
"integrity": "sha512-tx4DnXw9u3N7vw+bx6n2NKp6FoxoNwiP/biH83AS30I2AnTGyLd7afSeH6Oewn2E8jvB7K15bs12sMppkKOVeQ==",
"version": "3.17.2",
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.2.tgz",
"integrity": "sha512-8kYisQ3z/SQ2kyjlNeQxbkkTNmVFoQCqkmGrzLH6A9ecPlgTbp3wDTnUNqaUxYr4vlAcloxx8zwy7G5WdguYNg==",
"requires": {
"vscode-jsonrpc": "8.0.0-next.7",
"vscode-languageserver-types": "3.17.0-next.9"
"vscode-jsonrpc": "8.0.2",
"vscode-languageserver-types": "3.17.2"
}
},
"vscode-languageserver-types": {
"version": "3.17.0-next.9",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.0-next.9.tgz",
"integrity": "sha512-9/PeDNPYduaoXRUzYpqmu4ZV9L01HGo0wH9FUt+sSHR7IXwA7xoXBfNUlv8gB9H0D2WwEmMomSy1NmhjKQyn3A=="
"version": "3.17.2",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz",
"integrity": "sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA=="
},
"which": {
"version": "2.0.2",

View File

@ -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",
@ -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"
],
@ -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",

View File

@ -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,
@ -52,8 +54,6 @@ export class AstInspector implements vscode.HoverProvider, vscode.DefinitionProv
this,
ctx.subscriptions
);
ctx.pushCleanup(this);
}
dispose() {
this.setRustEditor(undefined);

View File

@ -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;
}
}

View File

@ -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 {
@ -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,
@ -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;

View File

@ -21,16 +21,16 @@ export function analyzerStatus(ctx: Ctx): Cmd {
readonly uri = vscode.Uri.parse("rust-analyzer-status://status");
readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
provideTextDocumentContent(_uri: vscode.Uri): vscode.ProviderResult<string> {
async provideTextDocumentContent(_uri: vscode.Uri): Promise<string> {
if (!vscode.window.activeTextEditor) return "";
const client = await ctx.getClient();
const params: ra.AnalyzerStatusParams = {};
const doc = ctx.activeRustEditor?.document;
if (doc != null) {
params.textDocument =
ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(doc);
params.textDocument = client.code2ProtocolConverter.asTextDocumentIdentifier(doc);
}
return ctx.client.sendRequest(ra.analyzerStatus, params);
return await client.sendRequest(ra.analyzerStatus, params);
}
get onDidChange(): vscode.Event<vscode.Uri> {
@ -38,7 +38,7 @@ export function analyzerStatus(ctx: Ctx): Cmd {
}
})();
ctx.pushCleanup(
ctx.pushExtCleanup(
vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-status", tdcp)
);
@ -60,9 +60,14 @@ export function memoryUsage(ctx: Ctx): Cmd {
provideTextDocumentContent(_uri: vscode.Uri): vscode.ProviderResult<string> {
if (!vscode.window.activeTextEditor) return "";
return ctx.client.sendRequest(ra.memoryUsage).then((mem: any) => {
return "Per-query memory usage:\n" + mem + "\n(note: database has been cleared)";
});
return ctx
.getClient()
.then((it) => it.sendRequest(ra.memoryUsage))
.then((mem: any) => {
return (
"Per-query memory usage:\n" + mem + "\n(note: database has been cleared)"
);
});
}
get onDidChange(): vscode.Event<vscode.Uri> {
@ -70,7 +75,7 @@ export function memoryUsage(ctx: Ctx): Cmd {
}
})();
ctx.pushCleanup(
ctx.pushExtCleanup(
vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-memory", tdcp)
);
@ -83,23 +88,19 @@ export function memoryUsage(ctx: Ctx): Cmd {
export function shuffleCrateGraph(ctx: Ctx): Cmd {
return async () => {
const client = ctx.client;
if (!client) return;
await client.sendRequest(ra.shuffleCrateGraph);
return ctx.getClient().then((it) => it.sendRequest(ra.shuffleCrateGraph));
};
}
export function matchingBrace(ctx: Ctx): Cmd {
return async () => {
const editor = ctx.activeRustEditor;
const client = ctx.client;
if (!editor || !client) return;
if (!editor) return;
const client = await ctx.getClient();
const response = await client.sendRequest(ra.matchingBrace, {
textDocument: ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(
editor.document
),
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
positions: editor.selections.map((s) =>
client.code2ProtocolConverter.asPosition(s.active)
),
@ -116,14 +117,13 @@ export function matchingBrace(ctx: Ctx): Cmd {
export function joinLines(ctx: Ctx): Cmd {
return async () => {
const editor = ctx.activeRustEditor;
const client = ctx.client;
if (!editor || !client) return;
if (!editor) return;
const client = await ctx.getClient();
const items: lc.TextEdit[] = await client.sendRequest(ra.joinLines, {
ranges: editor.selections.map((it) => client.code2ProtocolConverter.asRange(it)),
textDocument: ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(
editor.document
),
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
});
const textEdits = await client.protocol2CodeConverter.asTextEdits(items);
await editor.edit((builder) => {
@ -145,14 +145,12 @@ export function moveItemDown(ctx: Ctx): Cmd {
export function moveItem(ctx: Ctx, direction: ra.Direction): Cmd {
return async () => {
const editor = ctx.activeRustEditor;
const client = ctx.client;
if (!editor || !client) return;
if (!editor) return;
const client = await ctx.getClient();
const lcEdits = await client.sendRequest(ra.moveItem, {
range: client.code2ProtocolConverter.asRange(editor.selection),
textDocument: ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(
editor.document
),
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
direction,
});
@ -166,13 +164,13 @@ export function moveItem(ctx: Ctx, direction: ra.Direction): Cmd {
export function onEnter(ctx: Ctx): Cmd {
async function handleKeypress() {
const editor = ctx.activeRustEditor;
const client = ctx.client;
if (!editor || !client) return false;
if (!editor) return false;
const client = await ctx.getClient();
const lcEdits = await client
.sendRequest(ra.onEnter, {
textDocument: ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(
editor.document
),
position: client.code2ProtocolConverter.asPosition(editor.selection.active),
@ -198,14 +196,13 @@ export function onEnter(ctx: Ctx): Cmd {
export function parentModule(ctx: Ctx): Cmd {
return async () => {
const editor = vscode.window.activeTextEditor;
const client = ctx.client;
if (!editor || !client) return;
if (!editor) return;
if (!(isRustDocument(editor.document) || isCargoTomlDocument(editor.document))) return;
const client = await ctx.getClient();
const locations = await client.sendRequest(ra.parentModule, {
textDocument: ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(
editor.document
),
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
position: client.code2ProtocolConverter.asPosition(editor.selection.active),
});
if (!locations) return;
@ -236,13 +233,11 @@ export function parentModule(ctx: Ctx): Cmd {
export function openCargoToml(ctx: Ctx): Cmd {
return async () => {
const editor = ctx.activeRustEditor;
const client = ctx.client;
if (!editor || !client) return;
if (!editor) return;
const client = await ctx.getClient();
const response = await client.sendRequest(ra.openCargoToml, {
textDocument: ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(
editor.document
),
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
});
if (!response) return;
@ -259,12 +254,13 @@ export function openCargoToml(ctx: Ctx): Cmd {
export function ssr(ctx: Ctx): Cmd {
return async () => {
const editor = vscode.window.activeTextEditor;
const client = ctx.client;
if (!editor || !client) return;
if (!editor) return;
const client = await ctx.getClient();
const position = editor.selection.active;
const selections = editor.selections;
const textDocument = ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(
const textDocument = client.code2ProtocolConverter.asTextDocumentIdentifier(
editor.document
);
@ -314,6 +310,10 @@ export function ssr(ctx: Ctx): Cmd {
export function serverVersion(ctx: Ctx): Cmd {
return async () => {
if (!ctx.serverPath) {
void vscode.window.showWarningMessage(`rust-analyzer server is not running`);
return;
}
const { stdout } = spawnSync(ctx.serverPath, ["--version"], { encoding: "utf8" });
const versionString = stdout.slice(`rust-analyzer `.length).trim();
@ -354,21 +354,22 @@ export function syntaxTree(ctx: Ctx): Cmd {
}
}
provideTextDocumentContent(
async provideTextDocumentContent(
uri: vscode.Uri,
ct: vscode.CancellationToken
): vscode.ProviderResult<string> {
): Promise<string> {
const rustEditor = ctx.activeRustEditor;
if (!rustEditor) return "";
const client = await ctx.getClient();
// When the range based query is enabled we take the range of the selection
const range =
uri.query === "range=true" && !rustEditor.selection.isEmpty
? ctx.client.code2ProtocolConverter.asRange(rustEditor.selection)
? client.code2ProtocolConverter.asRange(rustEditor.selection)
: null;
const params = { textDocument: { uri: rustEditor.document.uri.toString() }, range };
return ctx.client.sendRequest(ra.syntaxTree, params, ct);
return client.sendRequest(ra.syntaxTree, params, ct);
}
get onDidChange(): vscode.Event<vscode.Uri> {
@ -376,12 +377,11 @@ export function syntaxTree(ctx: Ctx): Cmd {
}
})();
void new AstInspector(ctx);
ctx.pushCleanup(
ctx.pushExtCleanup(new AstInspector(ctx));
ctx.pushExtCleanup(
vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-syntax-tree", tdcp)
);
ctx.pushCleanup(
ctx.pushExtCleanup(
vscode.languages.setLanguageConfiguration("ra_syntax_tree", {
brackets: [["[", ")"]],
})
@ -437,14 +437,14 @@ export function viewHir(ctx: Ctx): Cmd {
}
}
provideTextDocumentContent(
async provideTextDocumentContent(
_uri: vscode.Uri,
ct: vscode.CancellationToken
): vscode.ProviderResult<string> {
): Promise<string> {
const rustEditor = ctx.activeRustEditor;
const client = ctx.client;
if (!rustEditor || !client) return "";
if (!rustEditor) return "";
const client = await ctx.getClient();
const params = {
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(
rustEditor.document
@ -459,7 +459,7 @@ export function viewHir(ctx: Ctx): Cmd {
}
})();
ctx.pushCleanup(
ctx.pushExtCleanup(
vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-hir", tdcp)
);
@ -503,13 +503,13 @@ export function viewFileText(ctx: Ctx): Cmd {
}
}
provideTextDocumentContent(
async provideTextDocumentContent(
_uri: vscode.Uri,
ct: vscode.CancellationToken
): vscode.ProviderResult<string> {
): Promise<string> {
const rustEditor = ctx.activeRustEditor;
const client = ctx.client;
if (!rustEditor || !client) return "";
if (!rustEditor) return "";
const client = await ctx.getClient();
const params = client.code2ProtocolConverter.asTextDocumentIdentifier(
rustEditor.document
@ -522,7 +522,7 @@ export function viewFileText(ctx: Ctx): Cmd {
}
})();
ctx.pushCleanup(
ctx.pushExtCleanup(
vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-file-text", tdcp)
);
@ -566,13 +566,13 @@ export function viewItemTree(ctx: Ctx): Cmd {
}
}
provideTextDocumentContent(
async provideTextDocumentContent(
_uri: vscode.Uri,
ct: vscode.CancellationToken
): vscode.ProviderResult<string> {
): Promise<string> {
const rustEditor = ctx.activeRustEditor;
const client = ctx.client;
if (!rustEditor || !client) return "";
if (!rustEditor) return "";
const client = await ctx.getClient();
const params = {
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(
@ -587,7 +587,7 @@ export function viewItemTree(ctx: Ctx): Cmd {
}
})();
ctx.pushCleanup(
ctx.pushExtCleanup(
vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-item-tree", tdcp)
);
@ -618,8 +618,8 @@ function crateGraph(ctx: Ctx, full: boolean): Cmd {
const params = {
full: full,
};
const dot = await ctx.client.sendRequest(ra.viewCrateGraph, params);
const client = await ctx.getClient();
const dot = await client.sendRequest(ra.viewCrateGraph, params);
const uri = panel.webview.asWebviewUri(nodeModulesPath);
const html = `
@ -690,13 +690,13 @@ export function expandMacro(ctx: Ctx): Cmd {
eventEmitter = new vscode.EventEmitter<vscode.Uri>();
async provideTextDocumentContent(_uri: vscode.Uri): Promise<string> {
const editor = vscode.window.activeTextEditor;
const client = ctx.client;
if (!editor || !client) return "";
if (!editor) return "";
const client = await ctx.getClient();
const position = editor.selection.active;
const expanded = await client.sendRequest(ra.expandMacro, {
textDocument: ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(
editor.document
),
position,
@ -712,7 +712,7 @@ export function expandMacro(ctx: Ctx): Cmd {
}
})();
ctx.pushCleanup(
ctx.pushExtCleanup(
vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-expand-macro", tdcp)
);
@ -724,11 +724,11 @@ export function expandMacro(ctx: Ctx): Cmd {
}
export function reloadWorkspace(ctx: Ctx): Cmd {
return async () => ctx.client.sendRequest(ra.reloadWorkspace);
return async () => (await ctx.getClient()).sendRequest(ra.reloadWorkspace);
}
async function showReferencesImpl(
client: LanguageClient,
client: LanguageClient | undefined,
uri: string,
position: lc.Position,
locations: lc.Location[]
@ -745,7 +745,7 @@ async function showReferencesImpl(
export function showReferences(ctx: Ctx): Cmd {
return async (uri: string, position: lc.Position, locations: lc.Location[]) => {
await showReferencesImpl(ctx.client, uri, position, locations);
await showReferencesImpl(await ctx.getClient(), uri, position, locations);
};
}
@ -762,25 +762,23 @@ export function applyActionGroup(_ctx: Ctx): Cmd {
export function gotoLocation(ctx: Ctx): Cmd {
return async (locationLink: lc.LocationLink) => {
const client = ctx.client;
if (client) {
const uri = client.protocol2CodeConverter.asUri(locationLink.targetUri);
let range = client.protocol2CodeConverter.asRange(locationLink.targetSelectionRange);
// collapse the range to a cursor position
range = range.with({ end: range.start });
const client = await ctx.getClient();
const uri = client.protocol2CodeConverter.asUri(locationLink.targetUri);
let range = client.protocol2CodeConverter.asRange(locationLink.targetSelectionRange);
// collapse the range to a cursor position
range = range.with({ end: range.start });
await vscode.window.showTextDocument(uri, { selection: range });
}
await vscode.window.showTextDocument(uri, { selection: range });
};
}
export function openDocs(ctx: Ctx): Cmd {
return async () => {
const client = ctx.client;
const editor = vscode.window.activeTextEditor;
if (!editor || !client) {
if (!editor) {
return;
}
const client = await ctx.getClient();
const position = editor.selection.active;
const textDocument = { uri: editor.document.uri.toString() };
@ -795,20 +793,21 @@ export function openDocs(ctx: Ctx): Cmd {
export function cancelFlycheck(ctx: Ctx): Cmd {
return async () => {
await ctx.client.sendRequest(ra.cancelFlycheck);
const client = await ctx.getClient();
await client.sendRequest(ra.cancelFlycheck);
};
}
export function resolveCodeAction(ctx: Ctx): Cmd {
const client = ctx.client;
return async (params: lc.CodeAction) => {
const client = await ctx.getClient();
params.command = undefined;
const item = await client.sendRequest(lc.CodeActionResolveRequest.type, params);
if (!item.edit) {
const item = await client?.sendRequest(lc.CodeActionResolveRequest.type, params);
if (!item?.edit) {
return;
}
const itemEdit = item.edit;
const edit = await client.protocol2CodeConverter.asWorkspaceEdit(itemEdit);
const edit = await client?.protocol2CodeConverter.asWorkspaceEdit(itemEdit);
// filter out all text edits and recreate the WorkspaceEdit without them so we can apply
// snippet edits on our own
const lcFileSystemEdit = {
@ -847,11 +846,10 @@ export function run(ctx: Ctx): Cmd {
}
export function peekTests(ctx: Ctx): Cmd {
const client = ctx.client;
return async () => {
const editor = ctx.activeRustEditor;
if (!editor || !client) return;
if (!editor) return;
const client = await ctx.getClient();
await vscode.window.withProgress(
{
@ -937,10 +935,10 @@ export function newDebugConfig(ctx: Ctx): Cmd {
};
}
export function linkToCommand(ctx: Ctx): Cmd {
export function linkToCommand(_: Ctx): Cmd {
return async (commandId: string) => {
const link = LINKED_COMMANDS.get(commandId);
if (ctx.client && link) {
if (link) {
const { command, arguments: args = [] } = link;
await vscode.commands.executeCommand(command, ...args);
}

View File

@ -14,8 +14,6 @@ export class Config {
readonly rootSection = "rust-analyzer";
private readonly requiresWorkspaceReloadOpts = [
"serverPath",
"server",
// FIXME: This shouldn't be here, changing this setting should reload
// `continueCommentsOnNewline` behavior without restart
"typing",
@ -23,6 +21,8 @@ export class Config {
private readonly requiresReloadOpts = [
"cargo",
"procMacro",
"serverPath",
"server",
"files",
"lens", // works as lens.*
]

View File

@ -2,10 +2,12 @@ import * as vscode from "vscode";
import * as lc from "vscode-languageclient/node";
import * as ra from "./lsp_ext";
import { Config } from "./config";
import { Config, substituteVariablesInEnv, substituteVSCodeVariables } from "./config";
import { createClient } from "./client";
import { isRustEditor, RustEditor } from "./util";
import { isRustEditor, log, RustEditor } from "./util";
import { ServerStatusParams } from "./lsp_ext";
import { PersistentState } from "./persistent_state";
import { bootstrap } from "./bootstrap";
export type Workspace =
| {
@ -17,35 +19,121 @@ export type Workspace =
};
export class Ctx {
private constructor(
readonly config: Config,
private readonly extCtx: vscode.ExtensionContext,
readonly client: lc.LanguageClient,
readonly serverPath: string,
readonly statusBar: vscode.StatusBarItem
) {}
readonly statusBar: vscode.StatusBarItem;
readonly config: Config;
static async create(
config: Config,
extCtx: vscode.ExtensionContext,
serverPath: string,
workspace: Workspace
): Promise<Ctx> {
const client = await createClient(serverPath, workspace, config.serverExtraEnv);
private client: lc.LanguageClient | undefined;
private _serverPath: string | undefined;
private traceOutputChannel: vscode.OutputChannel | undefined;
private outputChannel: vscode.OutputChannel | undefined;
private state: PersistentState;
const statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
extCtx.subscriptions.push(statusBar);
statusBar.text = "rust-analyzer";
statusBar.tooltip = "ready";
statusBar.command = "rust-analyzer.analyzerStatus";
statusBar.show();
workspace: Workspace;
const res = new Ctx(config, extCtx, client, serverPath, statusBar);
constructor(readonly extCtx: vscode.ExtensionContext, workspace: Workspace) {
this.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
extCtx.subscriptions.push(this.statusBar);
extCtx.subscriptions.push({
dispose() {
this.dispose();
},
});
this.statusBar.text = "rust-analyzer";
this.statusBar.tooltip = "ready";
this.statusBar.command = "rust-analyzer.analyzerStatus";
this.statusBar.show();
this.workspace = workspace;
res.pushCleanup(client.start());
await client.onReady();
client.onNotification(ra.serverStatus, (params) => res.setServerStatus(params));
return res;
this.state = new PersistentState(extCtx.globalState);
this.config = new Config(extCtx);
}
clientFetcher() {
return {
get client(): lc.LanguageClient | undefined {
return this.client;
},
};
}
async getClient() {
// if server path changes -> dispose
if (!this.traceOutputChannel) {
this.traceOutputChannel = vscode.window.createOutputChannel(
"Rust Analyzer Language Server Trace"
);
this.pushExtCleanup(this.traceOutputChannel);
}
if (!this.outputChannel) {
this.outputChannel = vscode.window.createOutputChannel("Rust Analyzer Language Server");
this.pushExtCleanup(this.outputChannel);
}
if (!this.client) {
this._serverPath = await bootstrap(this.extCtx, this.config, this.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 }';
log.error("Bootstrap error", err);
throw new Error(message);
}
);
const newEnv = substituteVariablesInEnv(
Object.assign({}, process.env, this.config.serverExtraEnv)
);
const run: lc.Executable = {
command: this._serverPath,
options: { env: newEnv },
};
const serverOptions = {
run,
debug: run,
};
let rawInitializationOptions = vscode.workspace.getConfiguration("rust-analyzer");
if (this.workspace.kind === "Detached Files") {
rawInitializationOptions = {
detachedFiles: this.workspace.files.map((file) => file.uri.fsPath),
...rawInitializationOptions,
};
}
const initializationOptions = substituteVSCodeVariables(rawInitializationOptions);
this.client = await createClient(
this.traceOutputChannel,
this.outputChannel,
initializationOptions,
serverOptions
);
this.client.onNotification(ra.serverStatus, (params) => this.setServerStatus(params));
}
return this.client;
}
async activate() {
log.info("Activating language client");
const client = await this.getClient();
await client.start();
return client;
}
async deactivate() {
log.info("Deactivating language client");
await this.client?.stop();
}
async disposeClient() {
log.info("Deactivating language client");
await this.client?.dispose();
this._serverPath = undefined;
this.client = undefined;
}
get activeRustEditor(): RustEditor | undefined {
@ -53,29 +141,18 @@ export class Ctx {
return editor && isRustEditor(editor) ? editor : undefined;
}
get visibleRustEditors(): RustEditor[] {
return vscode.window.visibleTextEditors.filter(isRustEditor);
}
registerCommand(name: string, factory: (ctx: Ctx) => Cmd) {
const fullName = `rust-analyzer.${name}`;
const cmd = factory(this);
const d = vscode.commands.registerCommand(fullName, cmd);
this.pushCleanup(d);
}
get extensionPath(): string {
return this.extCtx.extensionPath;
}
get globalState(): vscode.Memento {
return this.extCtx.globalState;
}
get subscriptions(): Disposable[] {
return this.extCtx.subscriptions;
}
get serverPath(): string | undefined {
return this._serverPath;
}
setServerStatus(status: ServerStatusParams) {
let icon = "";
const statusBar = this.statusBar;
@ -111,7 +188,14 @@ export class Ctx {
statusBar.text = `${icon}rust-analyzer`;
}
pushCleanup(d: Disposable) {
registerCommand(name: string, factory: (ctx: Ctx) => Cmd) {
const fullName = `rust-analyzer.${name}`;
const cmd = factory(this);
const d = vscode.commands.registerCommand(fullName, cmd);
this.pushExtCleanup(d);
}
pushExtCleanup(d: Disposable) {
this.extCtx.subscriptions.push(d);
}
}

View File

@ -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 { 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,54 +49,56 @@ 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. ";
const workspace: Workspace =
folders.length === 0
? {
kind: "Detached Files",
files: rustDocuments,
}
: { kind: "Workspace Folder" };
message += 'See the logs in "OUTPUT > Rust Analyzer Client" (should open automatically). ';
message += 'To enable verbose logs use { "rust-analyzer.trace.extension": true }';
log.error("Bootstrap error", err);
throw new Error(message);
const ctx = new Ctx(context, workspace);
// 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));
async function activateServer(ctx: Ctx): Promise<RustAnalyzerExtensionApi> {
if (ctx.workspace.kind === "Workspace Folder") {
ctx.pushExtCleanup(activateTaskProvider(ctx.config));
}
await initCommonContext(context, ctx);
warnAboutExtensionConflicts();
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: "" })
.catch(log.error),
async (_) => {
await ctx
.clientFetcher()
.client?.sendNotification("workspace/didChangeConfiguration", { settings: "" });
},
null,
ctx.subscriptions
);
return {
client: ctx.client,
};
await ctx.activate().catch((err) => {
void vscode.window.showErrorMessage(`Cannot activate rust-analyzer server: ${err.message}`);
});
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,26 +116,23 @@ 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);
await setContextValue(RUST_PROJECT_CONTEXT_NAME, true);
ctx.pushExtCleanup(defaultOnEnter);
// 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);
// FIXME: We should re-use the client, that is ctx.deactivate() if none of the configs have changed
await ctx.disposeClient();
await ctx.activate();
});
ctx.registerCommand("startServer", (_) => async () => {
await ctx.activate();
});
ctx.registerCommand("stopServer", (_) => async () => {
// FIXME: We should re-use the client, that is ctx.deactivate() if none of the configs have changed
await ctx.disposeClient();
});
ctx.registerCommand("analyzerStatus", commands.analyzerStatus);
ctx.registerCommand("memoryUsage", commands.memoryUsage);
ctx.registerCommand("shuffleCrateGraph", commands.shuffleCrateGraph);
@ -175,9 +158,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 +171,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);
}
/**

View File

@ -18,9 +18,9 @@ export async function selectRunnable(
showButtons: boolean = true
): Promise<RunnableQuickPick | undefined> {
const editor = ctx.activeRustEditor;
const client = ctx.client;
if (!editor || !client) return;
if (!editor) return;
const client = await ctx.getClient();
const textDocument: lc.TextDocumentIdentifier = {
uri: editor.document.uri.toString(),
};