From da8099b23aac07d3fa49e65e6e0ac6ed35ca8f00 Mon Sep 17 00:00:00 2001 From: Jannick Johnsen Date: Tue, 14 Apr 2020 04:57:20 +0200 Subject: [PATCH 1/7] create helper to find the location of Cargo.toml root folder --- editors/code/src/util.ts | 52 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/editors/code/src/util.ts b/editors/code/src/util.ts index 6f91f81d63ed..d353ad482e88 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 file_exists: (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 file_exists(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 file_exists(cargoPath)) { + // ghetto change the uri on Workspace folder to make vscode think it's located elsewhere + return vscode.Uri.file(current); + } + } + + return null; + } From 53ed3831dd296edd660aeea7d6508e53f263c650 Mon Sep 17 00:00:00 2001 From: Jannick Johnsen Date: Wed, 15 Apr 2020 23:11:00 +0200 Subject: [PATCH 2/7] add an "override" roots flag to RA-server, shouldn't be necessary but I was unable to set workspace folders in vscode. can be removed if solution is found. --- crates/rust-analyzer/src/bin/args.rs | 25 +++++++++++++++++++++---- crates/rust-analyzer/src/bin/main.rs | 25 ++++++++++++++++--------- editors/code/src/client.ts | 1 + 3 files changed, 38 insertions(+), 13 deletions(-) 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/src/client.ts b/editors/code/src/client.ts index 0ad4b63aeb18..57d65cfc3bcd 100644 --- a/editors/code/src/client.ts +++ b/editors/code/src/client.ts @@ -11,6 +11,7 @@ export async function createClient(serverPath: string, cwd: string): Promise Date: Wed, 15 Apr 2020 22:53:17 +0200 Subject: [PATCH 3/7] instead of returning root folder if it contains a Cargo.toml, check that the folder actually contains the file. --- editors/code/src/util.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/editors/code/src/util.ts b/editors/code/src/util.ts index d353ad482e88..8b709800eaf2 100644 --- a/editors/code/src/util.ts +++ b/editors/code/src/util.ts @@ -103,13 +103,11 @@ export async function nearestParentWithCargoToml( const file_exists: (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 file_exists(rootManifest)) { - return workspaceRootUri; - } - // 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); From 22d1495dd22e490d67b82058db19d1ea65feebdb Mon Sep 17 00:00:00 2001 From: Jannick Johnsen Date: Wed, 15 Apr 2020 23:01:01 +0200 Subject: [PATCH 4/7] add functionally to locate rust projects on startup and every time a new document is opened. One caveat is vscode.workspace.textDocuments only provides the documents the user have clicked on: Issue: https://github.com/microsoft/vscode/issues/15178 --- editors/code/src/client.ts | 2 +- editors/code/src/ctx.ts | 3 ++- editors/code/src/main.ts | 46 ++++++++++++++++++++++++++++++++++---- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts index 57d65cfc3bcd..1e0ef5d288d4 100644 --- a/editors/code/src/client.ts +++ b/editors/code/src/client.ts @@ -4,7 +4,7 @@ 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. 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..9c3d0bd4d67a 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts @@ -8,15 +8,53 @@ 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; +let foundProjects: Set = new Set(); +let config: Config | undefined = undefined; + +async function locate_rust_projects(root: vscode.Uri) { + let 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 locate_rust_projects(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) { + vscode.window.showInformationMessage( + `Found a new project at ${cargoRoot.fsPath}.` + + " Manually run: \"Rust Analyzer: Restart server\"" + + " or set rust-analyzer.server.autoRestartOnNew=true" + ); + } + } + } + }); + // Register a "dumb" onEnter command for the case where server fails to // start. // @@ -37,7 +75,8 @@ export async function activate(context: vscode.ExtensionContext) { ); 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,8 +91,7 @@ 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 From 91e552bfab88a331939bcee26b930a408deb3662 Mon Sep 17 00:00:00 2001 From: Jannick Johnsen Date: Wed, 15 Apr 2020 23:06:17 +0200 Subject: [PATCH 5/7] add option for automatically restart the rust-analyzer server --- editors/code/package.json | 5 +++++ editors/code/src/config.ts | 2 ++ editors/code/src/main.ts | 37 +++++++++++++++++++++++++------------ 3 files changed, 32 insertions(+), 12 deletions(-) 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/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/main.ts b/editors/code/src/main.ts index 9c3d0bd4d67a..609e263f4915 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts @@ -45,16 +45,39 @@ export async function activate(context: vscode.ExtensionContext) { 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. // @@ -69,6 +92,7 @@ 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' }), @@ -95,18 +119,7 @@ export async function activate(context: vscode.ExtensionContext) { // 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); From d328a011da1eec314ed1587b13bd39d5e473cef6 Mon Sep 17 00:00:00 2001 From: Jannick Johnsen Date: Wed, 15 Apr 2020 23:06:42 +0200 Subject: [PATCH 6/7] fix tasks to work for all projects --- editors/code/src/main.ts | 4 +++- editors/code/src/tasks.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index 609e263f4915..2d9d80ed8df6 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts @@ -143,7 +143,9 @@ export async function startRA(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..937909286d17 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'], ); From b545a229320cf298847e55ff1d42461780e82c4d Mon Sep 17 00:00:00 2001 From: Jannick Johnsen Date: Wed, 15 Apr 2020 23:16:36 +0200 Subject: [PATCH 7/7] apply linting --- editors/code/src/main.ts | 8 +++---- editors/code/src/tasks.ts | 2 +- editors/code/src/util.ts | 48 +++++++++++++++++++-------------------- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index 2d9d80ed8df6..1119b844ef2a 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts @@ -15,11 +15,11 @@ import { spawnSync } from 'child_process'; import { activateTaskProvider } from './tasks'; let ctx: Ctx | undefined; -let foundProjects: Set = new Set(); +const foundProjects: Set = new Set(); let config: Config | undefined = undefined; -async function locate_rust_projects(root: vscode.Uri) { - let cargoRoots = await Promise.all(vscode.workspace.textDocuments.map((doc) => nearestParentWithCargoToml(root, doc.uri))); +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); @@ -33,7 +33,7 @@ export async function activate(context: vscode.ExtensionContext) { const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; if (workspaceFolder !== undefined) { - await locate_rust_projects(workspaceFolder.uri) + await locateRustProjects(workspaceFolder.uri); } vscode.workspace.onDidOpenTextDocument(async (doc) => { diff --git a/editors/code/src/tasks.ts b/editors/code/src/tasks.ts index 937909286d17..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], { cwd: target.uri.fsPath}), + 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 8b709800eaf2..be4624de4d1e 100644 --- a/editors/code/src/util.ts +++ b/editors/code/src/util.ts @@ -92,43 +92,43 @@ export function createWorkspaceWithNewLocation(workspace: vscode.WorkspaceFolder ...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 file_exists: (path: fs.PathLike) => Promise = util.promisify(fs.exists); +): 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) { + 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 file_exists(cargoPath)) { - // ghetto change the uri on Workspace folder to make vscode think it's located elsewhere - return vscode.Uri.file(current); - } + 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; - } +}