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:
commit
067c410c45
64
editors/code/package-lock.json
generated
64
editors/code/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
|
148
editors/code/src/bootstrap.ts
Normal file
148
editors/code/src/bootstrap.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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.*
|
||||
]
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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(),
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user