diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index 75233dbb2a..05ad7ab4c4 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -272,7 +272,6 @@ config_data! { /// The warnings will be indicated by a blue squiggly underline in code /// and a blue icon in the `Problems Panel`. diagnostics_warningsAsInfo: Vec = "[]", - /// These directories will be ignored by rust-analyzer. They are /// relative to the workspace root, and globs are not supported. You may /// also need to add the folders to Code's `files.watcherExclude`. @@ -895,6 +894,15 @@ impl Config { } } + pub fn add_linked_projects(&mut self, linked_projects: Vec) { + let mut linked_projects = linked_projects + .into_iter() + .map(ManifestOrProjectJson::ProjectJson) + .collect::>(); + + self.data.linkedProjects.append(&mut linked_projects); + } + pub fn did_save_text_document_dynamic_registration(&self) -> bool { let caps = try_or_def!(self.caps.text_document.as_ref()?.synchronization.clone()?); caps.did_save == Some(true) && caps.dynamic_registration == Some(true) diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs index e95d68b70a..2fca2ab851 100644 --- a/crates/rust-analyzer/src/handlers.rs +++ b/crates/rust-analyzer/src/handlers.rs @@ -46,6 +46,7 @@ use crate::{ pub(crate) fn handle_workspace_reload(state: &mut GlobalState, _: ()) -> Result<()> { state.proc_macro_clients.clear(); state.proc_macro_changed = false; + state.fetch_workspaces_queue.request_op("reload workspace request".to_string()); state.fetch_build_data_queue.request_op("reload workspace request".to_string()); Ok(()) diff --git a/editors/code/package.json b/editors/code/package.json index a3b1a3107d..c5eb08748b 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -199,6 +199,11 @@ "title": "Reload workspace", "category": "rust-analyzer" }, + { + "command": "rust-analyzer.addProject", + "title": "Add current file's crate to workspace", + "category": "rust-analyzer" + }, { "command": "rust-analyzer.reload", "title": "Restart server", @@ -428,6 +433,17 @@ "default": false, "type": "boolean" }, + "rust-analyzer.discoverProjectCommand": { + "markdownDescription": "Sets the command that rust-analyzer uses to generate `rust-project.json` files. This command should only be used\n if a build system like Buck or Bazel is also in use. The command must accept files as arguments and return \n a rust-project.json over stdout.", + "default": null, + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + } + }, "$generated-start": {}, "rust-analyzer.assist.emitMustUse": { "markdownDescription": "Whether to insert #[must_use] when generating `as_` methods\nfor enum variants.", diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts index 62980ca046..565cb9c643 100644 --- a/editors/code/src/client.ts +++ b/editors/code/src/client.ts @@ -6,7 +6,7 @@ import * as Is from "vscode-languageclient/lib/common/utils/is"; import { assert } from "./util"; import * as diagnostics from "./diagnostics"; import { WorkspaceEdit } from "vscode"; -import { Config, substituteVSCodeVariables } from "./config"; +import { Config, prepareVSCodeConfig } from "./config"; import { randomUUID } from "crypto"; export interface Env { @@ -95,7 +95,16 @@ export async function createClient( const resp = await next(params, token); if (resp && Array.isArray(resp)) { return resp.map((val) => { - return substituteVSCodeVariables(val); + return prepareVSCodeConfig(val, (key, cfg) => { + // we only want to set discovered workspaces on the right key + // and if a workspace has been discovered. + if ( + key === "linkedProjects" && + config.discoveredWorkspaces.length > 0 + ) { + cfg[key] = config.discoveredWorkspaces; + } + }); }); } else { return resp; diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts index f4a4579a92..8a953577e9 100644 --- a/editors/code/src/commands.ts +++ b/editors/code/src/commands.ts @@ -3,7 +3,7 @@ import * as lc from "vscode-languageclient"; import * as ra from "./lsp_ext"; import * as path from "path"; -import { Ctx, Cmd, CtxInit } from "./ctx"; +import { Ctx, Cmd, CtxInit, discoverWorkspace } from "./ctx"; import { applySnippetWorkspaceEdit, applySnippetTextEdits } from "./snippets"; import { spawnSync } from "child_process"; import { RunnableQuickPick, selectRunnable, createTask, createArgs } from "./run"; @@ -749,6 +749,33 @@ export function reloadWorkspace(ctx: CtxInit): Cmd { return async () => ctx.client.sendRequest(ra.reloadWorkspace); } +export function addProject(ctx: CtxInit): Cmd { + return async () => { + const discoverProjectCommand = ctx.config.discoverProjectCommand; + if (!discoverProjectCommand) { + return; + } + + const workspaces: JsonProject[] = await Promise.all( + vscode.workspace.workspaceFolders!.map(async (folder): Promise => { + const rustDocuments = vscode.workspace.textDocuments.filter(isRustDocument); + return discoverWorkspace(rustDocuments, discoverProjectCommand, { + cwd: folder.uri.fsPath, + }); + }) + ); + + ctx.addToDiscoveredWorkspaces(workspaces); + + // this is a workaround to avoid needing writing the `rust-project.json` into + // a workspace-level VS Code-specific settings folder. We'd like to keep the + // `rust-project.json` entirely in-memory. + await ctx.client?.sendNotification(lc.DidChangeConfigurationNotification.type, { + settings: "", + }); + }; +} + async function showReferencesImpl( client: LanguageClient | undefined, uri: string, diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts index 1faa0ad910..da7c74c28b 100644 --- a/editors/code/src/config.ts +++ b/editors/code/src/config.ts @@ -34,6 +34,7 @@ export class Config { constructor(ctx: vscode.ExtensionContext) { this.globalStorageUri = ctx.globalStorageUri; + this.discoveredWorkspaces = []; vscode.workspace.onDidChangeConfiguration( this.onDidChangeConfiguration, this, @@ -55,6 +56,8 @@ export class Config { log.info("Using configuration", Object.fromEntries(cfg)); } + public discoveredWorkspaces: JsonProject[]; + private async onDidChangeConfiguration(event: vscode.ConfigurationChangeEvent) { this.refreshLogging(); @@ -191,7 +194,7 @@ export class Config { * So this getter handles this quirk by not requiring the caller to use postfix `!` */ private get(path: string): T | undefined { - return substituteVSCodeVariables(this.cfg.get(path)); + return prepareVSCodeConfig(this.cfg.get(path)); } get serverPath() { @@ -214,6 +217,10 @@ export class Config { return this.get("trace.extension"); } + get discoverProjectCommand() { + return this.get("discoverProjectCommand"); + } + get cargoRunner() { return this.get("cargoRunner"); } @@ -280,18 +287,32 @@ export class Config { } } -export function substituteVSCodeVariables(resp: T): T { +// the optional `cb?` parameter is meant to be used to add additional +// key/value pairs to the VS Code configuration. This needed for, e.g., +// including a `rust-project.json` into the `linkedProjects` key as part +// of the configuration/InitializationParams _without_ causing VS Code +// configuration to be written out to workspace-level settings. This is +// undesirable behavior because rust-project.json files can be tens of +// thousands of lines of JSON, most of which is not meant for humans +// to interact with. +export function prepareVSCodeConfig( + resp: T, + cb?: (key: Extract, res: { [key: string]: any }) => void +): T { if (Is.string(resp)) { return substituteVSCodeVariableInString(resp) as T; } else if (resp && Is.array(resp)) { return resp.map((val) => { - return substituteVSCodeVariables(val); + return prepareVSCodeConfig(val); }) as T; } else if (resp && typeof resp === "object") { const res: { [key: string]: any } = {}; for (const key in resp) { const val = resp[key]; - res[key] = substituteVSCodeVariables(val); + res[key] = prepareVSCodeConfig(val); + if (cb) { + cb(key, res); + } } return res as T; } diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts index 02b665c750..c2dca733df 100644 --- a/editors/code/src/ctx.ts +++ b/editors/code/src/ctx.ts @@ -2,12 +2,20 @@ import * as vscode from "vscode"; import * as lc from "vscode-languageclient/node"; import * as ra from "./lsp_ext"; -import { Config, substituteVSCodeVariables } from "./config"; +import { Config, prepareVSCodeConfig } from "./config"; import { createClient } from "./client"; -import { isRustDocument, isRustEditor, LazyOutputChannel, log, RustEditor } from "./util"; +import { + executeDiscoverProject, + isRustDocument, + isRustEditor, + LazyOutputChannel, + log, + RustEditor, +} from "./util"; import { ServerStatusParams } from "./lsp_ext"; import { PersistentState } from "./persistent_state"; import { bootstrap } from "./bootstrap"; +import { ExecOptions } from "child_process"; // We only support local folders, not eg. Live Share (`vlsl:` scheme), so don't activate if // only those are in use. We use "Empty" to represent these scenarios @@ -41,6 +49,17 @@ export function fetchWorkspace(): Workspace { : { kind: "Workspace Folder" }; } +export async function discoverWorkspace( + files: readonly vscode.TextDocument[], + command: string[], + options: ExecOptions +): Promise { + const paths = files.map((f) => `"${f.uri.fsPath}"`).join(" "); + const joinedCommand = command.join(" "); + const data = await executeDiscoverProject(`${joinedCommand} ${paths}`, options); + return JSON.parse(data) as JsonProject; +} + export type CommandFactory = { enabled: (ctx: CtxInit) => Cmd; disabled?: (ctx: Ctx) => Cmd; @@ -52,7 +71,7 @@ export type CtxInit = Ctx & { export class Ctx { readonly statusBar: vscode.StatusBarItem; - readonly config: Config; + config: Config; readonly workspace: Workspace; private _client: lc.LanguageClient | undefined; @@ -169,7 +188,30 @@ export class Ctx { }; } - const initializationOptions = substituteVSCodeVariables(rawInitializationOptions); + const discoverProjectCommand = this.config.discoverProjectCommand; + if (discoverProjectCommand) { + const workspaces: JsonProject[] = await Promise.all( + vscode.workspace.workspaceFolders!.map(async (folder): Promise => { + const rustDocuments = vscode.workspace.textDocuments.filter(isRustDocument); + return discoverWorkspace(rustDocuments, discoverProjectCommand, { + cwd: folder.uri.fsPath, + }); + }) + ); + + this.addToDiscoveredWorkspaces(workspaces); + } + + const initializationOptions = prepareVSCodeConfig( + rawInitializationOptions, + (key, obj) => { + // we only want to set discovered workspaces on the right key + // and if a workspace has been discovered. + if (key === "linkedProjects" && this.config.discoveredWorkspaces.length > 0) { + obj["linkedProjects"] = this.config.discoveredWorkspaces; + } + } + ); this._client = await createClient( this.traceOutputChannel, @@ -251,6 +293,17 @@ export class Ctx { return this._serverPath; } + addToDiscoveredWorkspaces(workspaces: JsonProject[]) { + for (const workspace of workspaces) { + const index = this.config.discoveredWorkspaces.indexOf(workspace); + if (~index) { + this.config.discoveredWorkspaces[index] = workspace; + } else { + this.config.discoveredWorkspaces.push(workspace); + } + } + } + private updateCommands(forceDisable?: "disable") { this.commandDisposables.forEach((disposable) => disposable.dispose()); this.commandDisposables = []; diff --git a/editors/code/src/lsp_ext.ts b/editors/code/src/lsp_ext.ts index 400cd207d4..872d7199b8 100644 --- a/editors/code/src/lsp_ext.ts +++ b/editors/code/src/lsp_ext.ts @@ -43,6 +43,7 @@ export const relatedTests = new lc.RequestType("rust-analyzer/reloadWorkspace"); + export const runFlycheck = new lc.NotificationType<{ textDocument: lc.TextDocumentIdentifier | null; }>("rust-analyzer/runFlycheck"); diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index 8a2412af84..d5de00561b 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts @@ -153,6 +153,7 @@ function createCommands(): Record { memoryUsage: { enabled: commands.memoryUsage }, shuffleCrateGraph: { enabled: commands.shuffleCrateGraph }, reloadWorkspace: { enabled: commands.reloadWorkspace }, + addProject: { enabled: commands.addProject }, matchingBrace: { enabled: commands.matchingBrace }, joinLines: { enabled: commands.joinLines }, parentModule: { enabled: commands.parentModule }, diff --git a/editors/code/src/rust_project.ts b/editors/code/src/rust_project.ts new file mode 100644 index 0000000000..187a1a96c1 --- /dev/null +++ b/editors/code/src/rust_project.ts @@ -0,0 +1,91 @@ +interface JsonProject { + /// Path to the directory with *source code* of + /// sysroot crates. + /// + /// It should point to the directory where std, + /// core, and friends can be found: + /// + /// https://github.com/rust-lang/rust/tree/master/library. + /// + /// If provided, rust-analyzer automatically adds + /// dependencies on sysroot crates. Conversely, + /// if you omit this path, you can specify sysroot + /// dependencies yourself and, for example, have + /// several different "sysroots" in one graph of + /// crates. + sysroot_src?: string; + /// The set of crates comprising the current + /// project. Must include all transitive + /// dependencies as well as sysroot crate (libstd, + /// libcore and such). + crates: Crate[]; +} + +interface Crate { + /// Optional crate name used for display purposes, + /// without affecting semantics. See the `deps` + /// key for semantically-significant crate names. + display_name?: string; + /// Path to the root module of the crate. + root_module: string; + /// Edition of the crate. + edition: "2015" | "2018" | "2021"; + /// Dependencies + deps: Dep[]; + /// Should this crate be treated as a member of + /// current "workspace". + /// + /// By default, inferred from the `root_module` + /// (members are the crates which reside inside + /// the directory opened in the editor). + /// + /// Set this to `false` for things like standard + /// library and 3rd party crates to enable + /// performance optimizations (rust-analyzer + /// assumes that non-member crates don't change). + is_workspace_member?: boolean; + /// Optionally specify the (super)set of `.rs` + /// files comprising this crate. + /// + /// By default, rust-analyzer assumes that only + /// files under `root_module.parent` can belong + /// to a crate. `include_dirs` are included + /// recursively, unless a subdirectory is in + /// `exclude_dirs`. + /// + /// Different crates can share the same `source`. + /// + /// If two crates share an `.rs` file in common, + /// they *must* have the same `source`. + /// rust-analyzer assumes that files from one + /// source can't refer to files in another source. + source?: { + include_dirs: string[]; + exclude_dirs: string[]; + }; + /// The set of cfgs activated for a given crate, like + /// `["unix", "feature=\"foo\"", "feature=\"bar\""]`. + cfg: string[]; + /// Target triple for this Crate. + /// + /// Used when running `rustc --print cfg` + /// to get target-specific cfgs. + target?: string; + /// Environment variables, used for + /// the `env!` macro + env: { [key: string]: string }; + + /// Whether the crate is a proc-macro crate. + is_proc_macro: boolean; + /// For proc-macro crates, path to compiled + /// proc-macro (.so file). + proc_macro_dylib_path?: string; +} + +interface Dep { + /// Index of a crate in the `crates` array. + crate: number; + /// Name as should appear in the (implicit) + /// `extern crate name` declaration. + name: string; +} diff --git a/editors/code/src/util.ts b/editors/code/src/util.ts index d93b9caeb1..922fbcbcf3 100644 --- a/editors/code/src/util.ts +++ b/editors/code/src/util.ts @@ -150,9 +150,11 @@ export function memoizeAsync( /** Awaitable wrapper around `child_process.exec` */ export function execute(command: string, options: ExecOptions): Promise { + log.info(`running command: ${command}`); return new Promise((resolve, reject) => { exec(command, options, (err, stdout, stderr) => { if (err) { + log.error(err); reject(err); return; } @@ -167,6 +169,21 @@ export function execute(command: string, options: ExecOptions): Promise }); } +export function executeDiscoverProject(command: string, options: ExecOptions): Promise { + log.info(`running command: ${command}`); + return new Promise((resolve, reject) => { + exec(command, options, (err, stdout, _) => { + if (err) { + log.error(err); + reject(err); + return; + } + + resolve(stdout.trimEnd()); + }); + }); +} + export class LazyOutputChannel implements vscode.OutputChannel { constructor(name: string) { this.name = name;