Skip to content

Commit 8af3d63

Browse files
committed
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.
1 parent 9549753 commit 8af3d63

File tree

14 files changed

+258
-25
lines changed

14 files changed

+258
-25
lines changed

crates/project-model/src/cfg_flag.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
use std::{fmt, str::FromStr};
55

66
use cfg::CfgOptions;
7+
use serde::Serialize;
78

89
#[derive(Clone, Eq, PartialEq, Debug)]
910
pub enum CfgFlag {
@@ -38,6 +39,18 @@ impl<'de> serde::Deserialize<'de> for CfgFlag {
3839
}
3940
}
4041

42+
impl Serialize for CfgFlag {
43+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
44+
where
45+
S: serde::Serializer,
46+
{
47+
match self {
48+
CfgFlag::Atom(s) => serializer.serialize_str(s),
49+
CfgFlag::KeyValue { .. } => serializer.serialize_str(&format!("{}", &self)),
50+
}
51+
}
52+
}
53+
4154
impl Extend<CfgFlag> for CfgOptions {
4255
fn extend<T: IntoIterator<Item = CfgFlag>>(&mut self, iter: T) {
4356
for cfg_flag in iter {

crates/project-model/src/project_json.rs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ use std::path::PathBuf;
5454
use base_db::{CrateDisplayName, CrateId, CrateName, Dependency, Edition};
5555
use paths::{AbsPath, AbsPathBuf};
5656
use rustc_hash::FxHashMap;
57-
use serde::{de, Deserialize};
57+
use serde::{de, ser, Deserialize, Serialize};
5858

5959
use crate::cfg_flag::CfgFlag;
6060

@@ -171,14 +171,14 @@ impl ProjectJson {
171171
}
172172
}
173173

174-
#[derive(Deserialize, Debug, Clone)]
174+
#[derive(Serialize, Deserialize, Debug, Clone)]
175175
pub struct ProjectJsonData {
176176
sysroot: Option<PathBuf>,
177177
sysroot_src: Option<PathBuf>,
178178
crates: Vec<CrateData>,
179179
}
180180

181-
#[derive(Deserialize, Debug, Clone)]
181+
#[derive(Serialize, Deserialize, Debug, Clone)]
182182
struct CrateData {
183183
display_name: Option<String>,
184184
root_module: PathBuf,
@@ -200,7 +200,7 @@ struct CrateData {
200200
repository: Option<String>,
201201
}
202202

203-
#[derive(Deserialize, Debug, Clone)]
203+
#[derive(Serialize, Deserialize, Debug, Clone)]
204204
#[serde(rename = "edition")]
205205
enum EditionData {
206206
#[serde(rename = "2015")]
@@ -221,16 +221,16 @@ impl From<EditionData> for Edition {
221221
}
222222
}
223223

224-
#[derive(Deserialize, Debug, Clone)]
224+
#[derive(Serialize, Deserialize, Debug, Clone)]
225225
struct DepData {
226226
/// Identifies a crate by position in the crates array.
227227
#[serde(rename = "crate")]
228228
krate: usize,
229-
#[serde(deserialize_with = "deserialize_crate_name")]
229+
#[serde(deserialize_with = "deserialize_crate_name", serialize_with = "serialize_crate_name")]
230230
name: CrateName,
231231
}
232232

233-
#[derive(Deserialize, Debug, Clone)]
233+
#[derive(Serialize, Deserialize, Debug, Clone)]
234234
struct CrateSource {
235235
include_dirs: Vec<PathBuf>,
236236
exclude_dirs: Vec<PathBuf>,
@@ -243,3 +243,10 @@ where
243243
let name = String::deserialize(de)?;
244244
CrateName::new(&name).map_err(|err| de::Error::custom(format!("invalid crate name: {err:?}")))
245245
}
246+
247+
fn serialize_crate_name<S>(crate_name: &CrateName, serializer: S) -> Result<S::Ok, S::Error>
248+
where
249+
S: ser::Serializer,
250+
{
251+
crate_name.serialize(serializer)
252+
}

crates/rust-analyzer/src/config.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,6 @@ config_data! {
272272
/// The warnings will be indicated by a blue squiggly underline in code
273273
/// and a blue icon in the `Problems Panel`.
274274
diagnostics_warningsAsInfo: Vec<String> = "[]",
275-
276275
/// These directories will be ignored by rust-analyzer. They are
277276
/// relative to the workspace root, and globs are not supported. You may
278277
/// also need to add the folders to Code's `files.watcherExclude`.
@@ -895,6 +894,15 @@ impl Config {
895894
}
896895
}
897896

897+
pub fn add_linked_projects(&mut self, linked_projects: Vec<ProjectJsonData>) {
898+
let mut linked_projects = linked_projects
899+
.into_iter()
900+
.map(ManifestOrProjectJson::ProjectJson)
901+
.collect::<Vec<ManifestOrProjectJson>>();
902+
903+
self.data.linkedProjects.append(&mut linked_projects);
904+
}
905+
898906
pub fn did_save_text_document_dynamic_registration(&self) -> bool {
899907
let caps = try_or_def!(self.caps.text_document.as_ref()?.synchronization.clone()?);
900908
caps.did_save == Some(true) && caps.dynamic_registration == Some(true)

crates/rust-analyzer/src/handlers.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use std::{
66
io::Write as _,
77
process::{self, Stdio},
8+
sync::Arc,
89
};
910

1011
use anyhow::Context;
@@ -46,6 +47,22 @@ use crate::{
4647
pub(crate) fn handle_workspace_reload(state: &mut GlobalState, _: ()) -> Result<()> {
4748
state.proc_macro_clients.clear();
4849
state.proc_macro_changed = false;
50+
51+
state.fetch_workspaces_queue.request_op("reload workspace request".to_string());
52+
state.fetch_build_data_queue.request_op("reload workspace request".to_string());
53+
Ok(())
54+
}
55+
56+
pub(crate) fn handle_add_project(
57+
state: &mut GlobalState,
58+
params: lsp_ext::AddProjectParams,
59+
) -> Result<()> {
60+
state.proc_macro_clients.clear();
61+
state.proc_macro_changed = false;
62+
63+
let config = Arc::make_mut(&mut state.config);
64+
config.add_linked_projects(params.project);
65+
4966
state.fetch_workspaces_queue.request_op("reload workspace request".to_string());
5067
state.fetch_build_data_queue.request_op("reload workspace request".to_string());
5168
Ok(())

crates/rust-analyzer/src/lsp_ext.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use lsp_types::{
99
notification::Notification, CodeActionKind, DocumentOnTypeFormattingParams,
1010
PartialResultParams, Position, Range, TextDocumentIdentifier, WorkDoneProgressParams,
1111
};
12+
use project_model::ProjectJsonData;
1213
use serde::{Deserialize, Serialize};
1314

1415
use crate::line_index::PositionEncoding;
@@ -51,6 +52,20 @@ impl Request for ReloadWorkspace {
5152
const METHOD: &'static str = "rust-analyzer/reloadWorkspace";
5253
}
5354

55+
pub enum AddProject {}
56+
57+
impl Request for AddProject {
58+
type Params = AddProjectParams;
59+
type Result = ();
60+
const METHOD: &'static str = "rust-analyzer/addProject";
61+
}
62+
63+
#[derive(Serialize, Deserialize, Debug)]
64+
#[serde(rename_all = "camelCase")]
65+
pub struct AddProjectParams {
66+
pub project: Vec<ProjectJsonData>,
67+
}
68+
5469
pub enum SyntaxTree {}
5570

5671
impl Request for SyntaxTree {

crates/rust-analyzer/src/main_loop.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,7 @@ impl GlobalState {
625625
.on_sync_mut::<lsp_ext::ReloadWorkspace>(handlers::handle_workspace_reload)
626626
.on_sync_mut::<lsp_ext::MemoryUsage>(handlers::handle_memory_usage)
627627
.on_sync_mut::<lsp_ext::ShuffleCrateGraph>(handlers::handle_shuffle_crate_graph)
628+
.on_sync_mut::<lsp_ext::AddProject>(handlers::handle_add_project)
628629
.on_sync::<lsp_ext::JoinLines>(handlers::handle_join_lines)
629630
.on_sync::<lsp_ext::OnEnter>(handlers::handle_on_enter)
630631
.on_sync::<lsp_types::request::SelectionRangeRequest>(handlers::handle_selection_range)

editors/code/package.json

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,11 @@
199199
"title": "Reload workspace",
200200
"category": "rust-analyzer"
201201
},
202+
{
203+
"command": "rust-analyzer.addProject",
204+
"title": "Add current file to workspace",
205+
"category": "rust-analyzer"
206+
},
202207
{
203208
"command": "rust-analyzer.reload",
204209
"title": "Restart server",
@@ -447,6 +452,17 @@
447452
"Fill missing expressions with reasonable defaults, `new` or `default` constructors."
448453
]
449454
},
455+
"rust-analyzer.discoverProjectCommand": {
456+
"markdownDescription": "Sets the command that rust-analyzer uses to generate `rust-project.json` files. This command is\n only suggested if a build system like Buck or Bazel is used. The command must accept files as arguements and return \n a rust-project.json over stdout.",
457+
"default": null,
458+
"type": [
459+
"null",
460+
"array"
461+
],
462+
"items": {
463+
"type": "string"
464+
}
465+
},
450466
"rust-analyzer.cachePriming.enable": {
451467
"markdownDescription": "Warm up caches on project load.",
452468
"default": true,
@@ -1904,4 +1920,4 @@
19041920
}
19051921
]
19061922
}
1907-
}
1923+
}

editors/code/src/commands.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as lc from "vscode-languageclient";
33
import * as ra from "./lsp_ext";
44
import * as path from "path";
55

6-
import { Ctx, Cmd, CtxInit } from "./ctx";
6+
import { Ctx, Cmd, CtxInit, discoverWorkspace } from "./ctx";
77
import { applySnippetWorkspaceEdit, applySnippetTextEdits } from "./snippets";
88
import { spawnSync } from "child_process";
99
import { RunnableQuickPick, selectRunnable, createTask, createArgs } from "./run";
@@ -749,6 +749,23 @@ export function reloadWorkspace(ctx: CtxInit): Cmd {
749749
return async () => ctx.client.sendRequest(ra.reloadWorkspace);
750750
}
751751

752+
export function addProject(ctx: CtxInit): Cmd {
753+
return async () => {
754+
const discoverProjectCommand = ctx.config.discoverProjectCommand;
755+
if (!discoverProjectCommand) {
756+
return;
757+
}
758+
759+
let workspaces: JsonProject[] = await Promise.all(vscode.workspace.workspaceFolders!.map(async (folder): Promise<JsonProject> => {
760+
return discoverWorkspace(vscode.workspace.textDocuments, discoverProjectCommand, { cwd: folder.uri.fsPath });
761+
}));
762+
763+
await ctx.client.sendRequest(ra.addProject, {
764+
project: workspaces
765+
});
766+
}
767+
}
768+
752769
async function showReferencesImpl(
753770
client: LanguageClient | undefined,
754771
uri: string,

editors/code/src/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,10 @@ export class Config {
214214
return this.get<boolean>("trace.extension");
215215
}
216216

217+
get discoverProjectCommand() {
218+
return this.get<string[] | undefined>("discoverProjectCommand")
219+
}
220+
217221
get cargoRunner() {
218222
return this.get<string | undefined>("cargoRunner");
219223
}

editors/code/src/ctx.ts

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import * as ra from "./lsp_ext";
44

55
import { Config, substituteVSCodeVariables } from "./config";
66
import { createClient } from "./client";
7-
import { isRustDocument, isRustEditor, LazyOutputChannel, log, RustEditor } from "./util";
7+
import { executeDiscoverProject, isRustDocument, isRustEditor, LazyOutputChannel, log, RustEditor } from "./util";
88
import { ServerStatusParams } from "./lsp_ext";
99
import { PersistentState } from "./persistent_state";
1010
import { bootstrap } from "./bootstrap";
11+
import { ExecOptions } from "child_process";
1112

1213
// We only support local folders, not eg. Live Share (`vlsl:` scheme), so don't activate if
1314
// only those are in use. We use "Empty" to represent these scenarios
@@ -16,12 +17,12 @@ import { bootstrap } from "./bootstrap";
1617
export type Workspace =
1718
| { kind: "Empty" }
1819
| {
19-
kind: "Workspace Folder";
20-
}
20+
kind: "Workspace Folder";
21+
}
2122
| {
22-
kind: "Detached Files";
23-
files: vscode.TextDocument[];
24-
};
23+
kind: "Detached Files";
24+
files: vscode.TextDocument[];
25+
};
2526

2627
export function fetchWorkspace(): Workspace {
2728
const folders = (vscode.workspace.workspaceFolders || []).filter(
@@ -35,12 +36,19 @@ export function fetchWorkspace(): Workspace {
3536
? rustDocuments.length === 0
3637
? { kind: "Empty" }
3738
: {
38-
kind: "Detached Files",
39-
files: rustDocuments,
40-
}
39+
kind: "Detached Files",
40+
files: rustDocuments,
41+
}
4142
: { kind: "Workspace Folder" };
4243
}
4344

45+
export async function discoverWorkspace(files: readonly vscode.TextDocument[], command: string[], options: ExecOptions): Promise<JsonProject> {
46+
const paths = files.map((f) => f.uri.fsPath).join(" ");
47+
const joinedCommand = command.join(" ");
48+
const data = await executeDiscoverProject(`${joinedCommand} -- ${paths}`, options);
49+
return JSON.parse(data) as JsonProject;
50+
}
51+
4452
export type CommandFactory = {
4553
enabled: (ctx: CtxInit) => Cmd;
4654
disabled?: (ctx: Ctx) => Cmd;
@@ -63,6 +71,7 @@ export class Ctx {
6371
private state: PersistentState;
6472
private commandFactories: Record<string, CommandFactory>;
6573
private commandDisposables: Disposable[];
74+
private discoveredWorkspaces: JsonProject[] | undefined;
6675

6776
get client() {
6877
return this._client;
@@ -71,7 +80,7 @@ export class Ctx {
7180
constructor(
7281
readonly extCtx: vscode.ExtensionContext,
7382
commandFactories: Record<string, CommandFactory>,
74-
workspace: Workspace
83+
workspace: Workspace,
7584
) {
7685
extCtx.subscriptions.push(this);
7786
this.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
@@ -169,7 +178,18 @@ export class Ctx {
169178
};
170179
}
171180

172-
const initializationOptions = substituteVSCodeVariables(rawInitializationOptions);
181+
const discoverProjectCommand = this.config.discoverProjectCommand;
182+
if (discoverProjectCommand) {
183+
let workspaces: JsonProject[] = await Promise.all(vscode.workspace.workspaceFolders!.map(async (folder): Promise<JsonProject> => {
184+
return discoverWorkspace(vscode.workspace.textDocuments, discoverProjectCommand, { cwd: folder.uri.fsPath });
185+
}));
186+
187+
this.discoveredWorkspaces = workspaces;
188+
}
189+
190+
let initializationOptions = substituteVSCodeVariables(rawInitializationOptions);
191+
// this appears to be load-bearing, for better or worse.
192+
await initializationOptions.update('linkedProjects', this.discoveredWorkspaces)
173193

174194
this._client = await createClient(
175195
this.traceOutputChannel,

0 commit comments

Comments
 (0)