about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--crates/rust-analyzer/src/config.rs10
-rw-r--r--crates/rust-analyzer/src/handlers.rs1
-rw-r--r--editors/code/package.json16
-rw-r--r--editors/code/src/client.ts13
-rw-r--r--editors/code/src/commands.ts29
-rw-r--r--editors/code/src/config.ts29
-rw-r--r--editors/code/src/ctx.ts61
-rw-r--r--editors/code/src/lsp_ext.ts1
-rw-r--r--editors/code/src/main.ts1
-rw-r--r--editors/code/src/rust_project.ts91
-rw-r--r--editors/code/src/util.ts17
11 files changed, 257 insertions, 12 deletions
diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs
index 75233dbb2ab..05ad7ab4c4a 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<String> = "[]",
-
         /// 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<ProjectJsonData>) {
+        let mut linked_projects = linked_projects
+            .into_iter()
+            .map(ManifestOrProjectJson::ProjectJson)
+            .collect::<Vec<ManifestOrProjectJson>>();
+
+        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 e95d68b70a3..2fca2ab851d 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 a3b1a3107d0..c5eb08748bf 100644
--- a/editors/code/package.json
+++ b/editors/code/package.json
@@ -200,6 +200,11 @@
                 "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",
                 "category": "rust-analyzer"
@@ -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 62980ca0464..565cb9c6432 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 f4a4579a92c..8a953577e99 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<JsonProject> => {
+                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 1faa0ad9106..da7c74c28ba 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<T>(path: string): T | undefined {
-        return substituteVSCodeVariables(this.cfg.get<T>(path));
+        return prepareVSCodeConfig(this.cfg.get<T>(path));
     }
 
     get serverPath() {
@@ -214,6 +217,10 @@ export class Config {
         return this.get<boolean>("trace.extension");
     }
 
+    get discoverProjectCommand() {
+        return this.get<string[] | undefined>("discoverProjectCommand");
+    }
+
     get cargoRunner() {
         return this.get<string | undefined>("cargoRunner");
     }
@@ -280,18 +287,32 @@ export class Config {
     }
 }
 
-export function substituteVSCodeVariables<T>(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<T>(
+    resp: T,
+    cb?: (key: Extract<keyof T, string>, res: { [key: string]: any }) => void
+): T {
     if (Is.string(resp)) {
         return substituteVSCodeVariableInString(resp) as T;
     } else if (resp && Is.array<any>(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 02b665c7503..c2dca733df8 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<JsonProject> {
+    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<JsonProject> => {
+                        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 400cd207d41..872d7199b83 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<lc.TextDocumentPositionParams, Te
     "rust-analyzer/relatedTests"
 );
 export const reloadWorkspace = new lc.RequestType0<null, void>("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 8a2412af849..d5de00561b1 100644
--- a/editors/code/src/main.ts
+++ b/editors/code/src/main.ts
@@ -153,6 +153,7 @@ function createCommands(): Record<string, CommandFactory> {
         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 00000000000..187a1a96c10
--- /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 d93b9caeb16..922fbcbcf35 100644
--- a/editors/code/src/util.ts
+++ b/editors/code/src/util.ts
@@ -150,9 +150,11 @@ export function memoizeAsync<Ret, TThis, Param extends string>(
 
 /** Awaitable wrapper around `child_process.exec` */
 export function execute(command: string, options: ExecOptions): Promise<string> {
+    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<string>
     });
 }
 
+export function executeDiscoverProject(command: string, options: ExecOptions): Promise<string> {
+    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;