Skip to content

Commit ce2c55c

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

File tree

26 files changed

+328
-436
lines changed

26 files changed

+328
-436
lines changed

Cargo.lock

Lines changed: 1 addition & 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: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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+
Error { message: String, context: Option<String> },
50+
Progress { message: String },
51+
Finished { project_json: Vec<ProjectJsonData> },
52+
}
53+
54+
impl ParseFromLine for DiscoverProjectMessage {
55+
fn from_line(line: &str, _error: &mut String) -> Option<Self> {
56+
let Ok(value) = serde_json::from_str::<serde_json::Value>(line) else {
57+
return Some(DiscoverProjectMessage::Error { message: line.to_owned(), context: None });
58+
};
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+
if let Some(error) = value.pointer("/fields/error") {
71+
if let Some(source) = value.pointer("/fields/source") {
72+
return Some(DiscoverProjectMessage::Error {
73+
message: error.as_str().unwrap().to_owned(),
74+
context: Some(source.as_str().unwrap().to_owned()),
75+
});
76+
}
77+
}
78+
79+
None
80+
}
81+
82+
fn from_eof() -> Option<Self> {
83+
None
84+
}
85+
}

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/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 & 1 deletion
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,7 +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() {
260+
if config.discover_command().is_none()
261+
&& !config.has_linked_projects()
262+
&& config.detached_files().is_empty()
263+
{
259264
config.rediscover_workspaces();
260265
}
261266

crates/rust-analyzer/src/config.rs

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -370,9 +370,6 @@ config_data! {
370370
/// Whether to show `can't find Cargo.toml` error message.
371371
notifications_cargoTomlNotFound: bool = true,
372372

373-
/// Whether to send an UnindexedProject notification to the client.
374-
notifications_unindexedProject: bool = false,
375-
376373
/// How many worker threads in the main loop. The default `null` means to pick automatically.
377374
numThreads: Option<usize> = None,
378375

@@ -439,6 +436,12 @@ config_data! {
439436
/// Whether to insert closing angle brackets when typing an opening angle bracket of a generic argument list.
440437
typing_autoClosingAngleBrackets_enable: bool = false,
441438

439+
/// Enables automatic discovery of projects using the discoverCommand.
440+
///
441+
/// Setting this command will result in rust-analyzer starting indexing
442+
/// only once a Rust file has been opened.
443+
workspace_discoverCommand: Option<Vec<String>> = None,
444+
442445
/// Workspace symbol search kind.
443446
workspace_symbol_search_kind: WorkspaceSymbolSearchKindDef = WorkspaceSymbolSearchKindDef::OnlyTypes,
444447
/// Limits the number of items returned from a workspace symbol search (Defaults to 128).
@@ -826,7 +829,6 @@ pub enum FilesWatcher {
826829
#[derive(Debug, Clone)]
827830
pub struct NotificationsConfig {
828831
pub cargo_toml_not_found: bool,
829-
pub unindexed_project: bool,
830832
}
831833

832834
#[derive(Debug, Clone)]
@@ -929,6 +931,16 @@ impl Config {
929931
self.workspace_roots.extend(paths);
930932
}
931933

934+
pub fn add_linked_projects(&mut self, projects: Vec<ProjectJsonData>) {
935+
let linked_projects = &mut self.client_config.global.linkedProjects;
936+
937+
let new_projects = projects.into_iter().map(ManifestOrProjectJson::ProjectJson);
938+
match linked_projects {
939+
Some(projects) => projects.append(&mut new_projects.collect::<Vec<_>>()),
940+
None => *linked_projects = Some(new_projects.collect::<Vec<_>>()),
941+
}
942+
}
943+
932944
pub fn update(&mut self, mut json: serde_json::Value) -> Result<(), ConfigError> {
933945
tracing::info!("updating config from JSON: {:#}", json);
934946
if json.is_null() || json.as_object().map_or(false, |it| it.is_empty()) {
@@ -1297,6 +1309,7 @@ impl Config {
12971309
pub fn has_linked_projects(&self) -> bool {
12981310
!self.linkedProjects().is_empty()
12991311
}
1312+
13001313
pub fn linked_manifests(&self) -> impl Iterator<Item = &Utf8Path> + '_ {
13011314
self.linkedProjects().iter().filter_map(|it| match it {
13021315
ManifestOrProjectJson::Manifest(p) => Some(&**p),
@@ -1306,6 +1319,11 @@ impl Config {
13061319
pub fn has_linked_project_jsons(&self) -> bool {
13071320
self.linkedProjects().iter().any(|it| matches!(it, ManifestOrProjectJson::ProjectJson(_)))
13081321
}
1322+
1323+
pub fn discover_command(&self) -> Option<Vec<String>> {
1324+
self.workspace_discoverCommand().clone()
1325+
}
1326+
13091327
pub fn linked_or_discovered_projects(&self) -> Vec<LinkedProject> {
13101328
match self.linkedProjects().as_slice() {
13111329
[] => {
@@ -1566,12 +1584,11 @@ impl Config {
15661584
pub fn notifications(&self) -> NotificationsConfig {
15671585
NotificationsConfig {
15681586
cargo_toml_not_found: self.notifications_cargoTomlNotFound().to_owned(),
1569-
unindexed_project: self.notifications_unindexedProject().to_owned(),
15701587
}
15711588
}
15721589

15731590
pub fn cargo_autoreload_config(&self) -> bool {
1574-
self.cargo_autoreload().to_owned()
1591+
self.cargo_autoreload().to_owned() && self.workspace_discoverCommand().is_none()
15751592
}
15761593

15771594
pub fn run_build_scripts(&self) -> bool {
@@ -3186,6 +3203,34 @@ mod tests {
31863203
);
31873204
}
31883205

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

0 commit comments

Comments
 (0)