This commit add Cargo-style project discovery for Buck and Bazel users.

This feature requires the user to add a command that generates a
`rust-project.json` from a set of files. Project discovery can be invoked
in two ways:

1. At extension activation time, which includes the generated
   `rust-project.json` as part of the linkedProjects argument in
    InitializeParams
2. Through a new command titled "Add current file to workspace", which
   makes use of a new, rust-analyzer specific LSP request that adds
   the workspace without erasing any existing workspaces.

I think that the command-running functionality _could_ merit being
placed into its own extension (and expose it via extension contribution
points), if only provide build-system idiomatic progress reporting and
status handling, but I haven't (yet) made an extension that does this.
This commit is contained in:
David Barsky 2023-03-09 15:06:26 -05:00
parent 9549753352
commit 8af3d6367e
14 changed files with 258 additions and 25 deletions

View file

@ -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,23 @@ 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;
}
let workspaces: JsonProject[] = await Promise.all(vscode.workspace.workspaceFolders!.map(async (folder): Promise<JsonProject> => {
return discoverWorkspace(vscode.workspace.textDocuments, discoverProjectCommand, { cwd: folder.uri.fsPath });
}));
await ctx.client.sendRequest(ra.addProject, {
project: workspaces
});
}
}
async function showReferencesImpl(
client: LanguageClient | undefined,
uri: string,

View file

@ -214,6 +214,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");
}

View file

@ -4,10 +4,11 @@ import * as ra from "./lsp_ext";
import { Config, substituteVSCodeVariables } 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
@ -16,12 +17,12 @@ import { bootstrap } from "./bootstrap";
export type Workspace =
| { kind: "Empty" }
| {
kind: "Workspace Folder";
}
kind: "Workspace Folder";
}
| {
kind: "Detached Files";
files: vscode.TextDocument[];
};
kind: "Detached Files";
files: vscode.TextDocument[];
};
export function fetchWorkspace(): Workspace {
const folders = (vscode.workspace.workspaceFolders || []).filter(
@ -35,12 +36,19 @@ export function fetchWorkspace(): Workspace {
? rustDocuments.length === 0
? { kind: "Empty" }
: {
kind: "Detached Files",
files: rustDocuments,
}
kind: "Detached Files",
files: rustDocuments,
}
: { 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;
@ -63,6 +71,7 @@ export class Ctx {
private state: PersistentState;
private commandFactories: Record<string, CommandFactory>;
private commandDisposables: Disposable[];
private discoveredWorkspaces: JsonProject[] | undefined;
get client() {
return this._client;
@ -71,7 +80,7 @@ export class Ctx {
constructor(
readonly extCtx: vscode.ExtensionContext,
commandFactories: Record<string, CommandFactory>,
workspace: Workspace
workspace: Workspace,
) {
extCtx.subscriptions.push(this);
this.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
@ -169,7 +178,18 @@ export class Ctx {
};
}
const initializationOptions = substituteVSCodeVariables(rawInitializationOptions);
const discoverProjectCommand = this.config.discoverProjectCommand;
if (discoverProjectCommand) {
let workspaces: JsonProject[] = await Promise.all(vscode.workspace.workspaceFolders!.map(async (folder): Promise<JsonProject> => {
return discoverWorkspace(vscode.workspace.textDocuments, discoverProjectCommand, { cwd: folder.uri.fsPath });
}));
this.discoveredWorkspaces = workspaces;
}
let initializationOptions = substituteVSCodeVariables(rawInitializationOptions);
// this appears to be load-bearing, for better or worse.
await initializationOptions.update('linkedProjects', this.discoveredWorkspaces)
this._client = await createClient(
this.traceOutputChannel,

View file

@ -43,6 +43,10 @@ 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 addProject = new lc.RequestType<AddProjectParams, string, void>(
"rust-analyzer/addProject"
)
export const runFlycheck = new lc.NotificationType<{
textDocument: lc.TextDocumentIdentifier | null;
}>("rust-analyzer/runFlycheck");
@ -68,6 +72,8 @@ export const viewItemTree = new lc.RequestType<ViewItemTreeParams, string, void>
export type AnalyzerStatusParams = { textDocument?: lc.TextDocumentIdentifier };
export type AddProjectParams = { project: JsonProject[] };
export type ExpandMacroParams = {
textDocument: lc.TextDocumentIdentifier;
position: lc.Position;

View file

@ -24,11 +24,11 @@ export async function activate(
vscode.window
.showWarningMessage(
`You have both the rust-analyzer (rust-lang.rust-analyzer) and Rust (rust-lang.rust) ` +
"plugins enabled. These are known to conflict and cause various functions of " +
"both plugins to not work correctly. You should disable one of them.",
"plugins enabled. These are known to conflict and cause various functions of " +
"both plugins to not work correctly. You should disable one of them.",
"Got it"
)
.then(() => {}, console.error);
.then(() => { }, console.error);
}
const ctx = new Ctx(context, createCommands(), fetchWorkspace());
@ -146,13 +146,14 @@ function createCommands(): Record<string, CommandFactory> {
health: "stopped",
});
},
disabled: (_) => async () => {},
disabled: (_) => async () => { },
},
analyzerStatus: { enabled: commands.analyzerStatus },
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 },

View file

@ -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,
}

View file

@ -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;