diff --git a/crates/rust-analyzer/src/bin/args.rs b/crates/rust-analyzer/src/bin/args.rs index 3cf394bb41ff..2ab59311af26 100644 --- a/crates/rust-analyzer/src/bin/args.rs +++ b/crates/rust-analyzer/src/bin/args.rs @@ -11,6 +11,7 @@ use std::{fmt::Write, path::PathBuf}; pub(crate) struct Args { pub(crate) verbosity: Verbosity, + pub(crate) use_cwd: bool, pub(crate) command: Command, } @@ -43,9 +44,15 @@ impl Args { pub(crate) fn parse() -> Result> { let mut matches = Arguments::from_env(); + let use_cwd = matches.contains("--use-cwd"); + if matches.contains("--version") { matches.finish().or_else(handle_extra_flags)?; - return Ok(Ok(Args { verbosity: Verbosity::Normal, command: Command::Version })); + return Ok(Ok(Args { + verbosity: Verbosity::Normal, + use_cwd, + command: Command::Version, + })); } let verbosity = match ( @@ -65,7 +72,7 @@ impl Args { Some(it) => it, None => { matches.finish().or_else(handle_extra_flags)?; - return Ok(Ok(Args { verbosity, command: Command::RunServer })); + return Ok(Ok(Args { verbosity, use_cwd, command: Command::RunServer })); } }; let command = match subcommand.as_str() { @@ -230,7 +237,7 @@ SUBCOMMANDS: return Ok(Err(HelpPrinted)); } }; - Ok(Ok(Args { verbosity, command })) + Ok(Ok(Args { verbosity, use_cwd, command })) } } diff --git a/crates/rust-analyzer/src/bin/main.rs b/crates/rust-analyzer/src/bin/main.rs index 608f4f67b2c8..6a6a8970048e 100644 --- a/crates/rust-analyzer/src/bin/main.rs +++ b/crates/rust-analyzer/src/bin/main.rs @@ -39,7 +39,7 @@ fn main() -> Result<()> { cli::analysis_bench(args.verbosity, path.as_ref(), what, load_output_dirs)? } - args::Command::RunServer => run_server()?, + args::Command::RunServer => run_server(args.use_cwd)?, args::Command::Version => println!("rust-analyzer {}", env!("REV")), } Ok(()) @@ -52,7 +52,7 @@ fn setup_logging() -> Result<()> { Ok(()) } -fn run_server() -> Result<()> { +fn run_server(use_cwd: bool) -> Result<()> { log::info!("lifecycle: server started"); let (connection, io_threads) = Connection::stdio(); @@ -67,15 +67,28 @@ fn run_server() -> Result<()> { } let cwd = std::env::current_dir()?; - let root = initialize_params.root_uri.and_then(|it| it.to_file_path().ok()).unwrap_or(cwd); - - let workspace_roots = initialize_params - .workspace_folders - .map(|workspaces| { - workspaces.into_iter().filter_map(|it| it.uri.to_file_path().ok()).collect::>() - }) - .filter(|workspaces| !workspaces.is_empty()) - .unwrap_or_else(|| vec![root]); + let root = if use_cwd { + cwd + } else { + initialize_params.root_uri.and_then(|it| it.to_file_path().ok()).unwrap_or(cwd) + }; + + log::info!("lifecycle: server started"); + + let workspace_roots = if use_cwd { + vec![root] + } else { + initialize_params + .workspace_folders + .map(|workspaces| { + workspaces + .into_iter() + .filter_map(|it| it.uri.to_file_path().ok()) + .collect::>() + }) + .filter(|workspaces| !workspaces.is_empty()) + .unwrap_or_else(|| vec![root]) + }; let config = { let mut config = Config::default(); diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts index 0ad4b63aeb18..703898c9500e 100644 --- a/editors/code/src/client.ts +++ b/editors/code/src/client.ts @@ -4,14 +4,15 @@ import * as vscode from 'vscode'; import { CallHierarchyFeature } from 'vscode-languageclient/lib/callHierarchy.proposed'; import { SemanticTokensFeature, DocumentSemanticsTokensSignature } from 'vscode-languageclient/lib/semanticTokens.proposed'; -export async function createClient(serverPath: string, cwd: string): Promise { +export async function createClient(serverPath: string, cwd: vscode.WorkspaceFolder): 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 run: lc.Executable = { command: serverPath, - options: { cwd }, + args: ["--use-cwd"], + options: { cwd: cwd.uri.fsPath }, }; const serverOptions: lc.ServerOptions = { run, @@ -22,7 +23,7 @@ export async function createClient(serverPath: string, cwd: string): Promise { const client = await createClient(serverPath, cwd); - const res = new Ctx(config, extCtx, client, serverPath); + + const statusDisplay = new StatusDisplay(config.checkOnSave.command); + const res = new Ctx(config, extCtx, client, serverPath, [], statusDisplay); + res.pushCleanup(client.start()); await client.onReady(); + + res.pushCleanup(res.status); + if (client != null) { + res.pushCleanup(client.onProgress( + WorkDoneProgress.type, + 'rustAnalyzer/cargoWatcher', + params => res.status.handleProgressNotification(params) + )); + } return res; } @@ -39,23 +55,27 @@ export class Ctx { 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 globalState(): vscode.Memento { return this.extCtx.globalState; } - get subscriptions(): Disposable[] { - return this.extCtx.subscriptions; + show() { + this.status.statusBarItem.show(); + } + + hide() { + this.status.statusBarItem.hide(); + } + + dispose() { + for (const d of this.subscriptions) { + d.dispose(); + } } pushCleanup(d: Disposable) { - this.extCtx.subscriptions.push(d); + this.subscriptions.push(d); } } diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index efd56a84b52f..22a4c00e86bb 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts @@ -5,18 +5,67 @@ import { promises as fs } from "fs"; import * as commands from './commands'; import { activateInlayHints } from './inlay_hints'; -import { activateStatusDisplay } from './status_display'; -import { Ctx } from './ctx'; +import { Ctx, Cmd } from './ctx'; import { Config, NIGHTLY_TAG } from './config'; -import { log, assert } from './util'; +import { log, assert, isRustDocument, nearestParentWithCargoToml, createWorkspaceWithNewLocation } from './util'; import { PersistentState } from './persistent_state'; import { fetchRelease, download } from './net'; import { spawnSync } from 'child_process'; import { activateTaskProvider } from './tasks'; let ctx: Ctx | undefined; +const ctxes: Map = new Map(); -export async function activate(context: vscode.ExtensionContext) { + +function registerCtxCommand(name: string, factory: (ctx: Ctx) => Cmd, fallback: Cmd | undefined, context: vscode.ExtensionContext) { + const fullName = `rust-analyzer.${name}`; + + async function wrappedCmd(...args: any[]): Promise { + if (ctx) { + const cmd = factory(ctx); + return await cmd(args); + } else if (fallback) { + return await fallback(args); + } + return; + } + + const d = vscode.commands.registerCommand(fullName, wrappedCmd); + context.subscriptions.push(d); +} + +async function whenOpeningTextDocument(doc: vscode.TextDocument, context: vscode.ExtensionContext) { + if (!isRustDocument(doc)) { + return; + } + + const workspaceRoot = vscode.workspace.getWorkspaceFolder(doc.uri); + if (!workspaceRoot) { + return; + } + + const cargoRoot = await nearestParentWithCargoToml(workspaceRoot.uri, doc.uri); + if (cargoRoot == null) { + vscode.window.showWarningMessage("Cargo.toml could not be located"); + return; + } + + ctx?.hide(); + + if (ctxes.has(cargoRoot.path)) { + ctx = ctxes.get(cargoRoot.path); + return; + } else { + const workspaceOnCargoRoot = createWorkspaceWithNewLocation(workspaceRoot, cargoRoot); + const newCtx = await activateNew(workspaceOnCargoRoot, context); + ctxes.set(cargoRoot.path, newCtx); + ctx = newCtx; + } + ctx?.show(); + +} + +async function activateNew(workspaceFolder: vscode.WorkspaceFolder, context: vscode.ExtensionContext): Promise { // Register a "dumb" onEnter command for the case where server fails to // start. // @@ -31,33 +80,51 @@ export async function activate(context: vscode.ExtensionContext) { // "rust-analyzer is not available" // ), // ) - const defaultOnEnter = vscode.commands.registerCommand( - 'rust-analyzer.onEnter', - () => vscode.commands.executeCommand('default:type', { text: '\n' }), - ); - context.subscriptions.push(defaultOnEnter); const config = new Config(context); const state = new PersistentState(context.globalState); const serverPath = await bootstrap(config, state); - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; - if (workspaceFolder === undefined) { - const err = "Cannot activate rust-analyzer when no folder is opened"; - void vscode.window.showErrorMessage(err); - throw new Error(err); - } - // 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, workspaceFolder.uri.fsPath); + const ctx = await Ctx.create(config, context, serverPath, workspaceFolder); - // Commands which invokes manually via command palette, shortcut, etc. + context.subscriptions.push(activateTaskProvider(workspaceFolder)); - // Reloading is inspired by @DanTup maneuver: https://github.com/microsoft/vscode/issues/45774#issuecomment-373423895 - ctx.registerCommand('reload', _ => async () => { + + activateInlayHints(ctx); + + return ctx; +} + +export async function activate(context: vscode.ExtensionContext) { + + // context.subscriptions.push(defaultOnEnter); + + // Commands which invokes manually via command palette, shortcut, etc., they will attempt to find the use the RA for the specific document's project + const register = (name: string, command: (ctx: Ctx) => Cmd) => registerCtxCommand(name, command, undefined, context); + const registerWithFallBack = (name: string, command: (ctx: Ctx) => Cmd, fallback: Cmd) => registerCtxCommand(name, command, fallback, context); + register('analyzerStatus', commands.analyzerStatus); + register('collectGarbage', commands.collectGarbage); + register('matchingBrace', commands.matchingBrace); + register('joinLines', commands.joinLines); + register('parentModule', commands.parentModule); + register('syntaxTree', commands.syntaxTree); + register('expandMacro', commands.expandMacro); + register('run', commands.run); + + registerWithFallBack('onEnter', commands.onEnter, () => vscode.commands.executeCommand('default:type', { text: '\n' })); + + register('ssr', commands.ssr); + register('serverVersion', commands.serverVersion); + + // Internal commands which are invoked by the server. + register('runSingle', commands.runSingle); + register('debugSingle', commands.debugSingle); + register('showReferences', commands.showReferences); + register('applySourceChange', commands.applySourceChange); + register('selectAndApplySourceChange', commands.selectAndApplySourceChange); + + register('reload', _ => async () => { void vscode.window.showInformationMessage('Reloading rust-analyzer...'); await deactivate(); while (context.subscriptions.length > 0) { @@ -69,44 +136,30 @@ export async function activate(context: vscode.ExtensionContext) { } await activate(context).catch(log.error); }); + // Reloading is inspired by @DanTup maneuver: https://github.com/microsoft/vscode/issues/45774#issuecomment-373423895 - ctx.registerCommand('analyzerStatus', commands.analyzerStatus); - ctx.registerCommand('collectGarbage', commands.collectGarbage); - ctx.registerCommand('matchingBrace', commands.matchingBrace); - ctx.registerCommand('joinLines', commands.joinLines); - ctx.registerCommand('parentModule', commands.parentModule); - ctx.registerCommand('syntaxTree', commands.syntaxTree); - ctx.registerCommand('expandMacro', commands.expandMacro); - ctx.registerCommand('run', commands.run); - - defaultOnEnter.dispose(); - ctx.registerCommand('onEnter', commands.onEnter); - - ctx.registerCommand('ssr', commands.ssr); - ctx.registerCommand('serverVersion', commands.serverVersion); - - // Internal commands which are invoked by the server. - ctx.registerCommand('runSingle', commands.runSingle); - ctx.registerCommand('debugSingle', commands.debugSingle); - ctx.registerCommand('showReferences', commands.showReferences); - ctx.registerCommand('applySourceChange', commands.applySourceChange); - ctx.registerCommand('selectAndApplySourceChange', commands.selectAndApplySourceChange); - - ctx.pushCleanup(activateTaskProvider(workspaceFolder)); - - activateStatusDisplay(ctx); + vscode.workspace.onDidOpenTextDocument(doc => whenOpeningTextDocument(doc, context), null, context.subscriptions); + vscode.workspace.textDocuments.forEach(doc => whenOpeningTextDocument(doc, context)); - activateInlayHints(ctx); + function changeConfig() { + for (const ctx of ctxes.values()) { + ctx.client?.sendNotification('workspace/didChangeConfiguration', { settings: "" }); + } + } vscode.workspace.onDidChangeConfiguration( - _ => ctx?.client?.sendNotification('workspace/didChangeConfiguration', { settings: "" }), + _ => changeConfig, null, - ctx.subscriptions, + context.subscriptions, ); + } export async function deactivate() { - await ctx?.client.stop(); + for (const ctx of ctxes.values()) { + await ctx.dispose(); + } + ctxes.clear(); ctx = undefined; } diff --git a/editors/code/src/status_display.ts b/editors/code/src/status_display.ts index f9cadc8a2258..669171bb2772 100644 --- a/editors/code/src/status_display.ts +++ b/editors/code/src/status_display.ts @@ -1,29 +1,16 @@ import * as vscode from 'vscode'; -import { WorkDoneProgress, WorkDoneProgressBegin, WorkDoneProgressReport, WorkDoneProgressEnd, Disposable } from 'vscode-languageclient'; - -import { Ctx } from './ctx'; +import { WorkDoneProgressBegin, WorkDoneProgressReport, WorkDoneProgressEnd, Disposable } from 'vscode-languageclient'; const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; -export function activateStatusDisplay(ctx: Ctx) { - const statusDisplay = new StatusDisplay(ctx.config.checkOnSave.command); - ctx.pushCleanup(statusDisplay); - const client = ctx.client; - if (client != null) { - ctx.pushCleanup(client.onProgress( - WorkDoneProgress.type, - 'rustAnalyzer/cargoWatcher', - params => statusDisplay.handleProgressNotification(params) - )); - } -} -class StatusDisplay implements Disposable { + +export class StatusDisplay implements Disposable { packageName?: string; private i: number = 0; - private statusBarItem: vscode.StatusBarItem; + public statusBarItem: vscode.StatusBarItem; private command: string; private timer?: NodeJS.Timeout; @@ -33,7 +20,7 @@ class StatusDisplay implements Disposable { 10, ); this.command = command; - this.statusBarItem.hide(); + this.statusBarItem.show(); } show() { diff --git a/editors/code/src/tasks.ts b/editors/code/src/tasks.ts index fa1c4a951ddb..f440c82722e2 100644 --- a/editors/code/src/tasks.ts +++ b/editors/code/src/tasks.ts @@ -42,7 +42,7 @@ function getStandardCargoTasks(target: vscode.WorkspaceFolder): vscode.Task[] { `cargo ${command}`, 'rust', // What to do when this command is executed. - new vscode.ShellExecution('cargo', [command]), + new vscode.ShellExecution('cargo', [command], { cwd: target.uri.fsPath }), // Problem matchers. ['$rustc'], ); diff --git a/editors/code/src/util.ts b/editors/code/src/util.ts index 6f91f81d63ed..068f4ed60dca 100644 --- a/editors/code/src/util.ts +++ b/editors/code/src/util.ts @@ -1,5 +1,8 @@ import * as lc from "vscode-languageclient"; import * as vscode from "vscode"; +import * as path from 'path'; +import * as fs from 'fs'; +import * as util from 'util'; import { strict as nativeAssert } from "assert"; export function assert(condition: boolean, explanation: string): asserts condition { @@ -11,6 +14,7 @@ export function assert(condition: boolean, explanation: string): asserts conditi } } + export const log = new class { private enabled = true; @@ -82,3 +86,51 @@ export function isRustDocument(document: vscode.TextDocument): document is RustD export function isRustEditor(editor: vscode.TextEditor): editor is RustEditor { return isRustDocument(editor.document); } + +export function createWorkspaceWithNewLocation(workspace: vscode.WorkspaceFolder, newLoc: vscode.Uri) { + return { + ...workspace, + name: path.basename(newLoc.fsPath), + uri: newLoc, + }; +} + +// searches up the folder structure until it finds a Cargo.toml +export async function nearestParentWithCargoToml( + workspaceRootUri: vscode.Uri, + fileLoc: vscode.Uri, +): Promise { + const fileExists: (path: fs.PathLike) => Promise = util.promisify(fs.exists); + // check that the workspace folder already contains the "Cargo.toml" + const workspaceRoot = workspaceRootUri.fsPath; + const rootManifest = path.join(workspaceRoot, 'Cargo.toml'); + if (await fileExists(rootManifest)) { + return workspaceRootUri; + } + + // algorithm that will strip one folder at a time and check if that folder contains "Cargo.toml" + let current = fileLoc.fsPath; + while (true) { + const old = current; + current = path.dirname(current); + + // break in case there is a bug that could result in a busy loop + if (old === current) { + break; + } + + // break in case the strip folder reached the workspace root + if (workspaceRoot === current) { + break; + } + + // check if "Cargo.toml" is present in the parent folder + const cargoPath = path.join(current, 'Cargo.toml'); + if (await fileExists(cargoPath)) { + // ghetto change the uri on Workspace folder to make vscode think it's located elsewhere + return vscode.Uri.file(current); + } + } + + return null; +}