Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

internal: Add Cargo-style project discovery for Buck and Bazel Users #14307

Merged
10 changes: 9 additions & 1 deletion crates/rust-analyzer/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions crates/rust-analyzer/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
Expand Down
16 changes: 16 additions & 0 deletions editors/code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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.",
Expand Down
6 changes: 4 additions & 2 deletions editors/code/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -95,7 +95,9 @@ 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) => {
cfg[key] = config.discoveredWorkspaces;
});
});
} else {
return resp;
Expand Down
29 changes: 28 additions & 1 deletion editors/code/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down
21 changes: 17 additions & 4 deletions editors/code/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export class Config {

constructor(ctx: vscode.ExtensionContext) {
this.globalStorageUri = ctx.globalStorageUri;
this.discoveredWorkspaces = [];
vscode.workspace.onDidChangeConfiguration(
this.onDidChangeConfiguration,
this,
Expand All @@ -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();

Expand Down Expand Up @@ -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() {
Expand All @@ -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");
}
Expand Down Expand Up @@ -280,18 +287,24 @@ export class Config {
}
}

export function substituteVSCodeVariables<T>(resp: T): T {
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;
}
Expand Down
59 changes: 55 additions & 4 deletions editors/code/src/ctx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -169,7 +188,28 @@ 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) => {
if (key === "linkedProjects") {
obj["linkedProjects"] = this.config.discoveredWorkspaces;
}
}
);

this._client = await createClient(
this.traceOutputChannel,
Expand Down Expand Up @@ -251,6 +291,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 = [];
Expand Down
1 change: 1 addition & 0 deletions editors/code/src/lsp_ext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
1 change: 1 addition & 0 deletions editors/code/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
91 changes: 91 additions & 0 deletions editors/code/src/rust_project.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading