diff --git a/editors/code/package-lock.json b/editors/code/package-lock.json index 3ff4b6897a1..a72865d4fe4 100644 --- a/editors/code/package-lock.json +++ b/editors/code/package-lock.json @@ -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", diff --git a/editors/code/package.json b/editors/code/package.json index f1dd3aa79ff..1afe2087c71 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -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", diff --git a/editors/code/src/ast_inspector.ts b/editors/code/src/ast_inspector.ts index e57fb20e2cf..176040120f4 100644 --- a/editors/code/src/ast_inspector.ts +++ b/editors/code/src/ast_inspector.ts @@ -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); diff --git a/editors/code/src/bootstrap.ts b/editors/code/src/bootstrap.ts new file mode 100644 index 00000000000..374c3b8144c --- /dev/null +++ b/editors/code/src/bootstrap.ts @@ -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 { + 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 { + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: "Patching rust-analyzer for NixOS", + }, + async (progress, _) => { + const expression = ` + {srcStr, pkgs ? import {}}: + 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 { + 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 { + 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; + } +} diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts index 45a7970b217..fb667619c86 100644 --- a/editors/code/src/client.ts +++ b/editors/code/src/client.ts @@ -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 { - // '.' 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; diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts index b9ad525e361..12ceb4f2df8 100644 --- a/editors/code/src/commands.ts +++ b/editors/code/src/commands.ts @@ -21,16 +21,16 @@ export function analyzerStatus(ctx: Ctx): Cmd { readonly uri = vscode.Uri.parse("rust-analyzer-status://status"); readonly eventEmitter = new vscode.EventEmitter(); - provideTextDocumentContent(_uri: vscode.Uri): vscode.ProviderResult { + async provideTextDocumentContent(_uri: vscode.Uri): Promise { 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 { @@ -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 { 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 { @@ -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 { + ): Promise { 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 { @@ -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 { + ): Promise { 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 { + ): Promise { 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 { + ): Promise { 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(); async provideTextDocumentContent(_uri: vscode.Uri): Promise { 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); } diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts index 199db6e30fc..10e243dc896 100644 --- a/editors/code/src/config.ts +++ b/editors/code/src/config.ts @@ -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.* ] diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts index 26510011d43..e94d4365c37 100644 --- a/editors/code/src/ctx.ts +++ b/editors/code/src/ctx.ts @@ -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 { - 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); } } diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index 41bde4195e0..f65620aebc6 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts @@ -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 { - // 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 { // 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 { - 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 { + 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 { - 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 { - await vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - title: "Patching rust-analyzer for NixOS", - }, - async (progress, _) => { - const expression = ` - {srcStr, pkgs ? import {}}: - 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 { - 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 { - 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); } /** diff --git a/editors/code/src/run.ts b/editors/code/src/run.ts index 22e5eda6827..dadaa41b1d1 100644 --- a/editors/code/src/run.ts +++ b/editors/code/src/run.ts @@ -18,9 +18,9 @@ export async function selectRunnable( showButtons: boolean = true ): Promise { 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(), };