diff --git a/crates/rust-analyzer/src/bin/args.rs b/crates/rust-analyzer/src/bin/args.rs index f5981588abc9..3cf51ffc747b 100644 --- a/crates/rust-analyzer/src/bin/args.rs +++ b/crates/rust-analyzer/src/bin/args.rs @@ -12,6 +12,7 @@ use std::{fmt::Write, path::PathBuf}; pub(crate) struct Args { pub(crate) verbosity: Verbosity, pub(crate) command: Command, + pub(crate) roots: Option>, } pub(crate) enum Command { @@ -47,12 +48,25 @@ pub(crate) enum Command { } impl Args { + fn get_roots(mut matches: Arguments) -> Result>> { + if matches.contains("--roots") { + Ok(Some(matches.free()?)) + } else { + matches.finish().or_else(handle_extra_flags)?; + Ok(None) + } + } + pub(crate) fn parse() -> Result> { let mut matches = Arguments::from_env(); 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, + command: Command::Version, + roots: None, + })); } let verbosity = match ( @@ -71,8 +85,11 @@ impl Args { let subcommand = match matches.subcommand()? { Some(it) => it, None => { - matches.finish().or_else(handle_extra_flags)?; - return Ok(Ok(Args { verbosity, command: Command::RunServer })); + return Ok(Ok(Args { + verbosity, + command: Command::RunServer, + roots: Self::get_roots(matches)?, + })); } }; let command = match subcommand.as_str() { @@ -269,7 +286,7 @@ SUBCOMMANDS: return Ok(Err(HelpPrinted)); } }; - Ok(Ok(Args { verbosity, command })) + Ok(Ok(Args { verbosity, command, roots: None })) } } diff --git a/crates/rust-analyzer/src/bin/main.rs b/crates/rust-analyzer/src/bin/main.rs index 7cfc44f01fdc..15fe809a9129 100644 --- a/crates/rust-analyzer/src/bin/main.rs +++ b/crates/rust-analyzer/src/bin/main.rs @@ -43,7 +43,7 @@ fn main() -> Result<()> { cli::diagnostics(path.as_ref(), load_output_dirs, all)? } - args::Command::RunServer => run_server()?, + args::Command::RunServer => run_server(args.roots)?, args::Command::Version => println!("rust-analyzer {}", env!("REV")), } Ok(()) @@ -56,7 +56,7 @@ fn setup_logging() -> Result<()> { Ok(()) } -fn run_server() -> Result<()> { +fn run_server(roots: Option>) -> Result<()> { log::info!("lifecycle: server started"); let (connection, io_threads) = Connection::stdio(); @@ -73,13 +73,20 @@ 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 workspace_roots = if let Some(roots) = roots { + roots.into_iter().map(Into::into).collect::>() + } 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/package.json b/editors/code/package.json index 5f73c8d8389f..7fc5d055f2e4 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -360,6 +360,11 @@ "default": true, "description": "Whether to ask for permission before downloading any files from the Internet" }, + "rust-analyzer.server.autoRestartOnNew": { + "type": "boolean", + "default": false, + "description": "automatically restarts rust-analyzer server when a new project is discovered" + }, "rust-analyzer.serverPath": { "type": [ "null", diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts index 0ad4b63aeb18..1e0ef5d288d4 100644 --- a/editors/code/src/client.ts +++ b/editors/code/src/client.ts @@ -4,13 +4,14 @@ 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: string, projectFolders: string[]): 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, + args: ["--roots"].concat(projectFolders), options: { cwd }, }; const serverOptions: lc.ServerOptions = { diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts index 35a05131c887..2d1dcb3488dc 100644 --- a/editors/code/src/config.ts +++ b/editors/code/src/config.ts @@ -91,6 +91,8 @@ export class Config { get askBeforeDownload() { return this.get("updates.askBeforeDownload"); } get traceExtension() { return this.get("trace.extension"); } + get autoRestartOnNew() { return this.get("server.autoRestartOnNew"); } + get inlayHints() { return { diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts index f7ed62d0356b..6ff3e1b4cf05 100644 --- a/editors/code/src/ctx.ts +++ b/editors/code/src/ctx.ts @@ -20,8 +20,9 @@ export class Ctx { extCtx: vscode.ExtensionContext, serverPath: string, cwd: string, + projectFolders: string[], ): Promise { - const client = await createClient(serverPath, cwd); + const client = await createClient(serverPath, cwd, projectFolders); const res = new Ctx(config, extCtx, client, serverPath); res.pushCleanup(client.start()); await client.onReady(); diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index efd56a84b52f..1119b844ef2a 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts @@ -8,15 +8,76 @@ import { activateInlayHints } from './inlay_hints'; import { activateStatusDisplay } from './status_display'; import { Ctx } from './ctx'; import { Config, NIGHTLY_TAG } from './config'; -import { log, assert } from './util'; +import { log, assert, 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 foundProjects: Set = new Set(); +let config: Config | undefined = undefined; + +async function locateRustProjects(root: vscode.Uri) { + const cargoRoots = await Promise.all(vscode.workspace.textDocuments.map((doc) => nearestParentWithCargoToml(root, doc.uri))); + for (const cargoRoot of cargoRoots) { + if (cargoRoot != null) { + foundProjects.add(cargoRoot.fsPath); + } + } +} export async function activate(context: vscode.ExtensionContext) { + + config = new Config(context); + + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + if (workspaceFolder !== undefined) { + await locateRustProjects(workspaceFolder.uri); + } + + vscode.workspace.onDidOpenTextDocument(async (doc) => { + const wf = vscode.workspace.getWorkspaceFolder(doc.uri); + if (wf) { + const cargoRoot = await nearestParentWithCargoToml(wf.uri, doc.uri); + if (cargoRoot != null) { + const isMissing = !foundProjects.has(cargoRoot.fsPath); + + foundProjects.add(cargoRoot.fsPath); + if (isMissing) { + if (config?.autoRestartOnNew) { + restart(context); + } else { + vscode.window.showInformationMessage( + `Found a new project at ${cargoRoot.fsPath}.` + + " Manually run: \"Rust Analyzer: Restart server\"" + + " or set rust-analyzer.server.autoRestartOnNew=true" + ); + } + } + } + } + }); + + startRA(context); + +} + +export async function restart(context: vscode.ExtensionContext) { + void vscode.window.showInformationMessage('Reloading rust-analyzer...'); + await deactivate(); + while (context.subscriptions.length > 0) { + try { + context.subscriptions.pop()!.dispose(); + } catch (err) { + log.error("Dispose error:", err); + } + } + await startRA(context).catch(log.error); +} + +export async function startRA(context: vscode.ExtensionContext) { + // Register a "dumb" onEnter command for the case where server fails to // start. // @@ -31,13 +92,15 @@ 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); + config = new Config(context); + const state = new PersistentState(context.globalState); const serverPath = await bootstrap(config, state); @@ -52,23 +115,11 @@ export async function activate(context: vscode.ExtensionContext) { // 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); - + ctx = await Ctx.create(config, context, serverPath, workspaceFolder.uri.fsPath, Array.from(foundProjects)); // 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 deactivate(); - while (context.subscriptions.length > 0) { - try { - context.subscriptions.pop()!.dispose(); - } catch (err) { - log.error("Dispose error:", err); - } - } - await activate(context).catch(log.error); - }); + ctx.registerCommand('reload', _ => restart); ctx.registerCommand('analyzerStatus', commands.analyzerStatus); ctx.registerCommand('collectGarbage', commands.collectGarbage); @@ -92,7 +143,9 @@ export async function activate(context: vscode.ExtensionContext) { ctx.registerCommand('applySourceChange', commands.applySourceChange); ctx.registerCommand('selectAndApplySourceChange', commands.selectAndApplySourceChange); - ctx.pushCleanup(activateTaskProvider(workspaceFolder)); + for (const project of foundProjects) { + ctx.pushCleanup(activateTaskProvider(createWorkspaceWithNewLocation(workspaceFolder, vscode.Uri.file(project)))); + } activateStatusDisplay(ctx); 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..be4624de4d1e 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,49 @@ 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; + // algorithm that will strip one folder at a time and check if that folder contains "Cargo.toml" + let current = fileLoc.fsPath; + if (fileLoc.fsPath.substring(0, workspaceRoot.length) !== workspaceRoot) { + return null; + } + 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; +}