Skip to content

Commit ba28ffc

Browse files
committed
feature: move linked_projects discovery to the rust-analyzer server
1 parent f6fc109 commit ba28ffc

File tree

18 files changed

+286
-70
lines changed

18 files changed

+286
-70
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/flycheck/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ command-group.workspace = true
2424
paths.workspace = true
2525
stdx.workspace = true
2626
toolchain.workspace = true
27+
project-model.workspace = true
2728

2829
[lints]
2930
workspace = true

crates/flycheck/src/json_workspace.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//! A `cargo-metadata`-equivalent for non-Cargo build systems.
2+
use std::{io, process::Command};
3+
4+
use crossbeam_channel::Sender;
5+
use paths::AbsPathBuf;
6+
use project_model::ProjectJsonData;
7+
use serde::{Deserialize, Serialize};
8+
9+
use crate::command::{CommandHandle, ParseFromLine};
10+
11+
/// A command wrapper for getting a `rust-project.json`.
12+
///
13+
/// This is analogous to `cargo-metadata`, but for non-Cargo build systems.
14+
pub struct JsonWorkspace {
15+
command: Vec<String>,
16+
sender: Sender<DiscoverProjectMessage>,
17+
}
18+
19+
impl JsonWorkspace {
20+
/// Create a new [JsonWorkspace].
21+
pub fn new(sender: Sender<DiscoverProjectMessage>, command: Vec<String>) -> Self {
22+
Self { sender, command }
23+
}
24+
25+
/// Spawn the command inside [JsonWorkspace] and report progress, if any.
26+
pub fn spawn(&self, files: Vec<AbsPathBuf>) -> io::Result<JsonWorkspaceHandle> {
27+
let command = &self.command[0];
28+
let args = &self.command[1..];
29+
let first_file: &AbsPathBuf = &files[0];
30+
31+
let mut cmd = Command::new(command);
32+
cmd.args(args);
33+
cmd.arg(first_file);
34+
35+
Ok(JsonWorkspaceHandle { _handle: CommandHandle::spawn(cmd, self.sender.clone())? })
36+
}
37+
}
38+
39+
/// A handle to a spawned [JsonWorkspace].
40+
#[derive(Debug)]
41+
pub struct JsonWorkspaceHandle {
42+
_handle: CommandHandle<DiscoverProjectMessage>,
43+
}
44+
45+
/// An enum containing either progress messages or the materialized rust-project.
46+
#[derive(Debug, Clone, Deserialize, Serialize)]
47+
#[serde(tag = "type")]
48+
pub enum DiscoverProjectMessage {
49+
Progress { message: String },
50+
Finished { project_json: Vec<ProjectJsonData> },
51+
}
52+
53+
impl ParseFromLine for DiscoverProjectMessage {
54+
fn from_line(line: &str, _error: &mut String) -> Option<Self> {
55+
let value = serde_json::from_str::<serde_json::Value>(&line).unwrap();
56+
57+
// let mut deserializer = serde_json::Deserializer::from_str(line);
58+
// deserializer.disable_recursion_limit();
59+
60+
if let Ok(project) = serde_json::from_value::<ProjectJsonData>(value.clone()) {
61+
return Some(DiscoverProjectMessage::Finished { project_json: vec![project] });
62+
}
63+
64+
if let Some(message) = value.pointer("/fields/message") {
65+
return Some(DiscoverProjectMessage::Progress {
66+
message: message.as_str().unwrap().to_owned(),
67+
});
68+
}
69+
70+
None
71+
}
72+
73+
fn from_eof() -> Option<Self> {
74+
None
75+
}
76+
}

crates/flycheck/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@ pub use cargo_metadata::diagnostic::{
2222
use toolchain::Tool;
2323

2424
mod command;
25+
mod json_workspace;
2526
mod test_runner;
2627

2728
use command::{CommandHandle, ParseFromLine};
29+
pub use json_workspace::{DiscoverProjectMessage, JsonWorkspace, JsonWorkspaceHandle};
2830
pub use test_runner::{CargoTestHandle, CargoTestMessage, TestState};
2931

3032
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
@@ -227,7 +229,7 @@ enum Event {
227229
CheckEvent(Option<CargoCheckMessage>),
228230
}
229231

230-
const SAVED_FILE_PLACEHOLDER: &str = "$saved_file";
232+
pub const SAVED_FILE_PLACEHOLDER: &str = "$saved_file";
231233

232234
impl FlycheckActor {
233235
fn new(

crates/load-cargo/src/lib.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use itertools::Itertools;
1717
use proc_macro_api::{MacroDylib, ProcMacroServer};
1818
use project_model::{CargoConfig, PackageRoot, ProjectManifest, ProjectWorkspace};
1919
use span::Span;
20-
use tracing::{instrument, Level};
20+
use tracing::Level;
2121
use vfs::{file_set::FileSetConfig, loader::Handle, AbsPath, AbsPathBuf, VfsPath};
2222

2323
pub struct LoadCargoConfig {
@@ -51,7 +51,6 @@ pub fn load_workspace_at(
5151
load_workspace(workspace, &cargo_config.extra_env, load_config)
5252
}
5353

54-
#[instrument(skip_all)]
5554
pub fn load_workspace(
5655
ws: ProjectWorkspace,
5756
extra_env: &FxHashMap<String, String>,

crates/project-model/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ doctest = false
1414
[dependencies]
1515
anyhow.workspace = true
1616
cargo_metadata.workspace = true
17+
crossbeam-channel.workspace = true
1718
rustc-hash.workspace = true
1819
semver.workspace = true
1920
serde_json.workspace = true

crates/project-model/src/project_json.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ impl ProjectJson {
175175

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

crates/project-model/src/workspace.rs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ use crate::{
3030
utf8_stdout, CargoConfig, CargoWorkspace, InvocationStrategy, ManifestPath, Package,
3131
ProjectJson, ProjectManifest, Sysroot, TargetData, TargetKind, WorkspaceBuildScripts,
3232
};
33+
use tracing::{debug, error, info, Level};
3334

3435
pub type FileLoader<'a> = &'a mut dyn for<'b> FnMut(&'b AbsPath) -> Option<FileId>;
3536

@@ -249,7 +250,7 @@ impl ProjectWorkspace {
249250
};
250251

251252
let rustc = rustc_dir.and_then(|rustc_dir| {
252-
tracing::info!(workspace = %cargo_toml, rustc_dir = %rustc_dir, "Using rustc source");
253+
info!(workspace = %cargo_toml, rustc_dir = %rustc_dir, "Using rustc source");
253254
match CargoWorkspace::fetch_metadata(
254255
&rustc_dir,
255256
cargo_toml.parent(),
@@ -705,7 +706,7 @@ impl ProjectWorkspace {
705706
load: FileLoader<'_>,
706707
extra_env: &FxHashMap<String, String>,
707708
) -> (CrateGraph, ProcMacroPaths) {
708-
let _p = tracing::span!(tracing::Level::INFO, "ProjectWorkspace::to_crate_graph").entered();
709+
let _p = tracing::span!(tracing::Level::INFO, "to_crate_graph").entered();
709710

710711
let Self { kind, sysroot, cfg_overrides, rustc_cfg, .. } = self;
711712
let ((mut crate_graph, proc_macros), sysroot) = match kind {
@@ -762,9 +763,9 @@ impl ProjectWorkspace {
762763
};
763764

764765
if matches!(sysroot.mode(), SysrootMode::Stitched(_)) && crate_graph.patch_cfg_if() {
765-
tracing::debug!("Patched std to depend on cfg-if")
766+
debug!("Patched std to depend on cfg-if")
766767
} else {
767-
tracing::debug!("Did not patch std to depend on cfg-if")
768+
debug!("Did not patch std to depend on cfg-if")
768769
}
769770
(crate_graph, proc_macros)
770771
}
@@ -909,6 +910,11 @@ fn project_json_to_crate_graph(
909910
CrateOrigin::Local { repo: None, name: None }
910911
},
911912
);
913+
debug!(
914+
?crate_graph_crate_id,
915+
crate = display_name.as_ref().map(|name| name.canonical_name()),
916+
"added root to crate graph"
917+
);
912918
if *is_proc_macro {
913919
if let Some(path) = proc_macro_dylib_path.clone() {
914920
let node = Ok((
@@ -923,6 +929,7 @@ fn project_json_to_crate_graph(
923929
)
924930
.collect();
925931

932+
debug!(map = ?idx_to_crate_id);
926933
for (from_idx, krate) in project.crates() {
927934
if let Some(&from) = idx_to_crate_id.get(&from_idx) {
928935
public_deps.add_to_crate_graph(crate_graph, from);
@@ -949,7 +956,7 @@ fn cargo_to_crate_graph(
949956
override_cfg: &CfgOverrides,
950957
build_scripts: &WorkspaceBuildScripts,
951958
) -> (CrateGraph, ProcMacroPaths) {
952-
let _p = tracing::span!(tracing::Level::INFO, "cargo_to_crate_graph").entered();
959+
let _p = tracing::span!(Level::INFO, "cargo_to_crate_graph").entered();
953960
let mut res = (CrateGraph::default(), ProcMacroPaths::default());
954961
let crate_graph = &mut res.0;
955962
let proc_macros = &mut res.1;
@@ -1134,7 +1141,7 @@ fn detached_file_to_crate_graph(
11341141
sysroot: &Sysroot,
11351142
override_cfg: &CfgOverrides,
11361143
) -> (CrateGraph, ProcMacroPaths) {
1137-
let _p = tracing::span!(tracing::Level::INFO, "detached_file_to_crate_graph").entered();
1144+
let _p = tracing::span!(Level::INFO, "detached_file_to_crate_graph").entered();
11381145
let mut crate_graph = CrateGraph::default();
11391146
let (public_deps, _libproc_macro) =
11401147
sysroot_to_crate_graph(&mut crate_graph, sysroot, rustc_cfg.clone(), load);
@@ -1148,7 +1155,7 @@ fn detached_file_to_crate_graph(
11481155
let file_id = match load(detached_file) {
11491156
Some(file_id) => file_id,
11501157
None => {
1151-
tracing::error!("Failed to load detached file {:?}", detached_file);
1158+
error!("Failed to load detached file {:?}", detached_file);
11521159
return (crate_graph, FxHashMap::default());
11531160
}
11541161
};
@@ -1345,7 +1352,7 @@ fn add_target_crate_root(
13451352
crate_id
13461353
}
13471354

1348-
#[derive(Default)]
1355+
#[derive(Default, Debug)]
13491356
struct SysrootPublicDeps {
13501357
deps: Vec<(CrateName, CrateId, bool)>,
13511358
}

crates/rust-analyzer/src/bin/main.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ fn run_server() -> anyhow::Result<()> {
172172
return Err(e.into());
173173
}
174174
};
175+
175176
tracing::info!("InitializeParams: {}", initialize_params);
176177
let lsp_types::InitializeParams {
177178
root_uri,
@@ -221,6 +222,7 @@ fn run_server() -> anyhow::Result<()> {
221222
.unwrap_or_else(|| vec![root_path.clone()]);
222223
let mut config =
223224
Config::new(root_path, capabilities, workspace_roots, visual_studio_code_version);
225+
224226
if let Some(json) = initialization_options {
225227
if let Err(e) = config.update(json) {
226228
use lsp_types::{
@@ -255,8 +257,10 @@ fn run_server() -> anyhow::Result<()> {
255257
return Err(e.into());
256258
}
257259

258-
if !config.has_linked_projects() && config.detached_files().is_empty() {
259-
config.rediscover_workspaces();
260+
if config.discover_command().is_none() {
261+
if !config.has_linked_projects() && config.detached_files().is_empty() {
262+
config.rediscover_workspaces();
263+
}
260264
}
261265

262266
// If the io_threads have an error, there's usually an error on the main

crates/rust-analyzer/src/config.rs

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,11 @@ 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> = vec![],
275+
/// Enables automatic discovery of projects using the discoverCommand.
276+
///
277+
/// Setting this command will result in rust-analyzer starting indexing
278+
/// only once a Rust file has been opened.
279+
discoverCommand: Option<Vec<String>> = None,
275280
/// These directories will be ignored by rust-analyzer. They are
276281
/// relative to the workspace root, and globs are not supported. You may
277282
/// also need to add the folders to Code's `files.watcherExclude`.
@@ -654,7 +659,7 @@ config_data! {
654659

655660
#[derive(Debug, Clone)]
656661
pub struct Config {
657-
discovered_projects: Vec<ProjectManifest>,
662+
pub(crate) discovered_projects: Vec<ProjectManifest>,
658663
/// The workspace roots as registered by the LSP client
659664
workspace_roots: Vec<AbsPathBuf>,
660665
caps: lsp_types::ClientCapabilities,
@@ -929,6 +934,18 @@ impl Config {
929934
self.workspace_roots.extend(paths);
930935
}
931936

937+
pub fn add_linked_projects(&mut self, projects: Vec<ProjectJsonData>) {
938+
let linked_projects = &mut self.client_config.global.linkedProjects;
939+
940+
let mut new_projects: Vec<_> =
941+
projects.into_iter().map(ManifestOrProjectJson::ProjectJson).collect();
942+
943+
match linked_projects {
944+
Some(projects) => projects.append(&mut new_projects),
945+
None => *linked_projects = Some(new_projects),
946+
}
947+
}
948+
932949
pub fn update(&mut self, mut json: serde_json::Value) -> Result<(), ConfigError> {
933950
tracing::info!("updating config from JSON: {:#}", json);
934951
if json.is_null() || json.as_object().map_or(false, |it| it.is_empty()) {
@@ -1297,6 +1314,7 @@ impl Config {
12971314
pub fn has_linked_projects(&self) -> bool {
12981315
!self.linkedProjects().is_empty()
12991316
}
1317+
13001318
pub fn linked_manifests(&self) -> impl Iterator<Item = &Utf8Path> + '_ {
13011319
self.linkedProjects().iter().filter_map(|it| match it {
13021320
ManifestOrProjectJson::Manifest(p) => Some(&**p),
@@ -1306,6 +1324,11 @@ impl Config {
13061324
pub fn has_linked_project_jsons(&self) -> bool {
13071325
self.linkedProjects().iter().any(|it| matches!(it, ManifestOrProjectJson::ProjectJson(_)))
13081326
}
1327+
1328+
pub fn discover_command(&self) -> Option<Vec<String>> {
1329+
self.discoverCommand().clone()
1330+
}
1331+
13091332
pub fn linked_or_discovered_projects(&self) -> Vec<LinkedProject> {
13101333
match self.linkedProjects().as_slice() {
13111334
[] => {
@@ -1571,7 +1594,7 @@ impl Config {
15711594
}
15721595

15731596
pub fn cargo_autoreload_config(&self) -> bool {
1574-
self.cargo_autoreload().to_owned()
1597+
self.cargo_autoreload().to_owned() && self.discoverCommand().is_none()
15751598
}
15761599

15771600
pub fn run_build_scripts(&self) -> bool {
@@ -3026,7 +3049,7 @@ fn doc_comment_to_string(doc: &[&str]) -> String {
30263049

30273050
#[cfg(test)]
30283051
mod tests {
3029-
use std::fs;
3052+
use std::{env::temp_dir, fs};
30303053

30313054
use test_utils::{ensure_file_contents, project_root};
30323055

@@ -3186,6 +3209,34 @@ mod tests {
31863209
);
31873210
}
31883211

3212+
#[test]
3213+
fn linked_targets_updates() {
3214+
let mut config = Config::new(
3215+
AbsPathBuf::try_from(project_root()).unwrap(),
3216+
Default::default(),
3217+
vec![],
3218+
None,
3219+
);
3220+
assert!(config.linkedProjects().is_empty());
3221+
let project_json = serde_json::json!({
3222+
"sysroot_src": null,
3223+
"crates": [
3224+
{
3225+
"display_name": "hello_world",
3226+
"root_module": "$ROOT$src/lib.rs",
3227+
"edition": "2018",
3228+
"deps": [],
3229+
"is_workspace_member": true
3230+
}
3231+
]
3232+
});
3233+
3234+
let project_json = serde_json::from_value(project_json).expect("unable to deserialize");
3235+
config.add_linked_projects(vec![project_json]);
3236+
3237+
assert!(!config.linkedProjects().is_empty());
3238+
}
3239+
31893240
#[test]
31903241
fn cargo_target_dir_relative_dir() {
31913242
let mut config = Config::new(

0 commit comments

Comments
 (0)