From f4fe143b4d4d162edbb75f44744eb150def43ed1 Mon Sep 17 00:00:00 2001 From: David Barsky Date: Mon, 1 Jul 2024 12:59:39 -0400 Subject: [PATCH] feature: move `linked_projects` discovery to the rust-analyzer server --- Cargo.lock | 127 ++++++------- crates/flycheck/Cargo.toml | 1 + crates/flycheck/src/json_workspace.rs | 124 +++++++++++++ crates/flycheck/src/lib.rs | 6 +- crates/load-cargo/src/lib.rs | 2 - crates/project-model/src/project_json.rs | 20 ++- crates/project-model/src/workspace.rs | 17 +- crates/rust-analyzer/src/bin/main.rs | 6 +- crates/rust-analyzer/src/config.rs | 120 ++++++++++++- crates/rust-analyzer/src/global_state.rs | 44 ++++- .../src/handlers/notification.rs | 42 +++-- crates/rust-analyzer/src/handlers/request.rs | 5 +- crates/rust-analyzer/src/lsp/ext.rs | 2 +- crates/rust-analyzer/src/lsp/utils.rs | 1 - crates/rust-analyzer/src/main_loop.rs | 169 ++++++++++++++---- crates/rust-analyzer/src/op_queue.rs | 1 + crates/rust-analyzer/src/reload.rs | 98 +++++++--- crates/rust-analyzer/src/tracing/config.rs | 5 +- crates/rust-analyzer/tests/slow-tests/main.rs | 1 - docs/dev/lsp-extensions.md | 2 +- docs/user/generated_config.adoc | 10 ++ editors/code/package.json | 34 ++++ 22 files changed, 669 insertions(+), 168 deletions(-) create mode 100644 crates/flycheck/src/json_workspace.rs diff --git a/Cargo.lock b/Cargo.lock index 57d43dad3fd8..1e6ef7517725 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -28,9 +28,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.83" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "arbitrary" @@ -52,16 +52,16 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backtrace" -version = "0.3.71" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11" dependencies = [ "addr2line", "cc", "cfg-if", "libc", "miniz_oxide", - "object 0.32.2", + "object 0.35.0", "rustc-demangle", ] @@ -103,9 +103,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "camino" -version = "1.1.6" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239" dependencies = [ "serde", ] @@ -135,9 +135,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.97" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" +checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" [[package]] name = "cfg" @@ -235,18 +235,18 @@ checksum = "0d48d8f76bd9331f19fe2aaf3821a9f9fb32c3963e1e3d6ce82a8c09cef7444a" [[package]] name = "crc32fast" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ "crossbeam-utils", ] @@ -272,9 +272,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "ctrlc" @@ -369,9 +369,9 @@ checksum = "9bda8e21c04aca2ae33ffc2fd8c23134f3cac46db123ba97bd9d3f3b8a4a85e1" [[package]] name = "either" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" [[package]] name = "ena" @@ -434,6 +434,7 @@ dependencies = [ "crossbeam-channel", "paths", "process-wrap", + "project-model", "rustc-hash", "serde", "serde_json", @@ -479,9 +480,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "hashbrown" @@ -917,9 +918,9 @@ dependencies = [ [[package]] name = "libmimalloc-sys" -version = "0.1.37" +version = "0.1.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81eb4061c0582dedea1cbc7aff2240300dd6982e0239d1c99e65c1dbf4a30ba7" +checksum = "0e7bb23d733dfcc8af652a78b7bf232f0e967710d044732185e561e47c0336b6" dependencies = [ "cc", "libc", @@ -1084,18 +1085,18 @@ dependencies = [ [[package]] name = "mimalloc" -version = "0.1.41" +version = "0.1.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f41a2280ded0da56c8cf898babb86e8f10651a34adcfff190ae9a1159c6908d" +checksum = "e9186d86b79b52f4a77af65604b51225e8db1d6ee7e3f41aec1e40829c71a176" dependencies = [ "libmimalloc-sys", ] [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" dependencies = [ "adler", ] @@ -1160,9 +1161,9 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.49.0" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c073d3c1930d0751774acf49e66653acecb416c3a54c6ec095a9b11caddb5a68" +checksum = "dd2800e1520bdc966782168a627aa5d1ad92e33b984bf7c7615d31280c83ff14" dependencies = [ "windows-sys 0.48.0", ] @@ -1185,18 +1186,18 @@ dependencies = [ [[package]] name = "object" -version = "0.32.2" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "d8dd6c0cdf9429bce006e1362bfce61fa1bfd8c898a643ed8d2b471934701d3d" dependencies = [ "memchr", ] [[package]] name = "object" -version = "0.33.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8dd6c0cdf9429bce006e1362bfce61fa1bfd8c898a643ed8d2b471934701d3d" +checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e" dependencies = [ "memchr", ] @@ -1221,9 +1222,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "parking_lot" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -1379,9 +1380,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.82" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" dependencies = [ "unicode-ident", ] @@ -1827,18 +1828,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.201" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.201" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", @@ -1870,9 +1871,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" dependencies = [ "serde", ] @@ -1894,9 +1895,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "smol_str" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6845563ada680337a52d43bb0b29f396f2d911616f6573012645b9e3d048a49" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" dependencies = [ "serde", ] @@ -1950,9 +1951,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.63" +version = "2.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" dependencies = [ "proc-macro2", "quote", @@ -2035,18 +2036,18 @@ checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" [[package]] name = "thiserror" -version = "1.0.60" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.60" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", @@ -2130,9 +2131,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "toml" -version = "0.8.12" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" dependencies = [ "serde", "serde_spanned", @@ -2142,18 +2143,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.12" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" +checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" dependencies = [ "indexmap", "serde", @@ -2227,9 +2228,9 @@ dependencies = [ [[package]] name = "tracing-tree" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65139ecd2c3f6484c3b99bc01c77afe21e95473630747c7aca525e78b0666675" +checksum = "b56c62d2c80033cb36fae448730a2f2ef99410fe3ecbffc916681a32f6807dbe" dependencies = [ "nu-ansi-term", "tracing-core", @@ -2239,9 +2240,9 @@ dependencies = [ [[package]] name = "triomphe" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3" +checksum = "1b2cb4fbb9995eeb36ac86fadf24031ccd58f99d6b4b2d7b911db70bddb80d90" dependencies = [ "serde", "stable_deref_trait", @@ -2579,9 +2580,9 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d" +checksum = "56c52728401e1dc672a56e81e593e912aa54c78f40246869f78359a2bf24d29d" dependencies = [ "memchr", ] diff --git a/crates/flycheck/Cargo.toml b/crates/flycheck/Cargo.toml index d81a5fe34006..bb3a94c8da67 100644 --- a/crates/flycheck/Cargo.toml +++ b/crates/flycheck/Cargo.toml @@ -24,6 +24,7 @@ process-wrap.workspace = true paths.workspace = true stdx.workspace = true toolchain.workspace = true +project-model.workspace = true [lints] workspace = true \ No newline at end of file diff --git a/crates/flycheck/src/json_workspace.rs b/crates/flycheck/src/json_workspace.rs new file mode 100644 index 000000000000..b04e43fd56d0 --- /dev/null +++ b/crates/flycheck/src/json_workspace.rs @@ -0,0 +1,124 @@ +//! A `cargo-metadata`-equivalent for non-Cargo build systems. +use std::{io, process::Command}; + +use crossbeam_channel::Sender; +use paths::{AbsPathBuf, Utf8Path}; +use project_model::ProjectJsonData; +use serde::{Deserialize, Serialize}; + +use crate::command::{CommandHandle, ParseFromLine}; + +/// A command wrapper for getting a `rust-project.json`. +/// +/// This is analogous to `cargo-metadata`, but for non-Cargo build systems. +pub struct JsonWorkspace { + command: Vec, + sender: Sender, +} + +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum JsonArguments { + Path( + #[serde(serialize_with = "serialize_abs_pathbuf")] + #[serde(deserialize_with = "deserialize_abs_pathbuf")] + AbsPathBuf, + ), + Label(String), +} + +impl JsonWorkspace { + /// Create a new [JsonWorkspace]. + pub fn new(sender: Sender, command: Vec) -> Self { + Self { sender, command } + } + + /// Spawn the command inside [JsonWorkspace] and report progress, if any. + pub fn spawn(&self, arg: JsonArguments) -> io::Result { + let command = &self.command[0]; + let args = &self.command[1..]; + + let mut cmd = Command::new(command); + cmd.args(args); + + let arg = serde_json::to_string(&arg)?; + cmd.arg(arg); + + Ok(JsonWorkspaceHandle { _handle: CommandHandle::spawn(cmd, self.sender.clone())? }) + } +} + +/// A handle to a spawned [JsonWorkspace]. +#[derive(Debug)] +pub struct JsonWorkspaceHandle { + _handle: CommandHandle, +} + +/// An enum containing either progress messages or the materialized rust-project. +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(tag = "type")] +pub enum DiscoverProjectMessage { + Error { message: String, context: Option }, + Progress { message: String }, + Finished(FinishedOutput), +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +pub struct FinishedOutput { + pub project: ProjectJsonData, + #[serde(serialize_with = "serialize_abs_pathbuf")] + #[serde(deserialize_with = "deserialize_abs_pathbuf")] + pub buildfile: AbsPathBuf, +} + +fn deserialize_abs_pathbuf<'de, D>(de: D) -> std::result::Result +where + D: serde::de::Deserializer<'de>, +{ + let path = String::deserialize(de)?; + + AbsPathBuf::try_from(path.as_ref()) + .map_err(|err| serde::de::Error::custom(format!("invalid path name: {err:?}"))) +} + +fn serialize_abs_pathbuf(path: &AbsPathBuf, se: S) -> Result +where + S: serde::Serializer, +{ + let path: &Utf8Path = path.as_ref(); + se.serialize_str(path.as_str()) +} + +impl ParseFromLine for DiscoverProjectMessage { + fn from_line(line: &str, _error: &mut String) -> Option { + let Ok(value) = serde_json::from_str::(line) else { + return Some(DiscoverProjectMessage::Error { message: line.to_owned(), context: None }); + }; + + if let Ok(project) = serde_json::from_value::(value.clone()) { + return Some(DiscoverProjectMessage::Finished(project)); + } + + if let Some(message) = value.pointer("/fields/message") { + return Some(DiscoverProjectMessage::Progress { + message: message.as_str().unwrap().to_owned(), + }); + } + + if let Some(error) = value.pointer("/fields/error") { + let context = + value.pointer("/fields/source").map(|source| source.as_str().unwrap().to_owned()); + + return Some(DiscoverProjectMessage::Error { + message: error.as_str().unwrap().to_owned(), + context, + }); + } + + None + } + + fn from_eof() -> Option { + None + } +} diff --git a/crates/flycheck/src/lib.rs b/crates/flycheck/src/lib.rs index 36be51434757..972352ae02c3 100644 --- a/crates/flycheck/src/lib.rs +++ b/crates/flycheck/src/lib.rs @@ -22,9 +22,13 @@ pub use cargo_metadata::diagnostic::{ use toolchain::Tool; mod command; +mod json_workspace; mod test_runner; use command::{CommandHandle, ParseFromLine}; +pub use json_workspace::{ + DiscoverProjectMessage, JsonArguments, JsonWorkspace, JsonWorkspaceHandle, +}; pub use test_runner::{CargoTestHandle, CargoTestMessage, TestState}; #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] @@ -242,7 +246,7 @@ enum FlycheckStatus { Finished, } -const SAVED_FILE_PLACEHOLDER: &str = "$saved_file"; +pub const SAVED_FILE_PLACEHOLDER: &str = "$saved_file"; impl FlycheckActor { fn new( diff --git a/crates/load-cargo/src/lib.rs b/crates/load-cargo/src/lib.rs index cb785195a066..1e06f8ddf412 100644 --- a/crates/load-cargo/src/lib.rs +++ b/crates/load-cargo/src/lib.rs @@ -17,7 +17,6 @@ use itertools::Itertools; use proc_macro_api::{MacroDylib, ProcMacroServer}; use project_model::{CargoConfig, ManifestPath, PackageRoot, ProjectManifest, ProjectWorkspace}; use span::Span; -use tracing::instrument; use vfs::{file_set::FileSetConfig, loader::Handle, AbsPath, AbsPathBuf, VfsPath}; pub struct LoadCargoConfig { @@ -51,7 +50,6 @@ pub fn load_workspace_at( load_workspace(workspace, &cargo_config.extra_env, load_config) } -#[instrument(skip_all)] pub fn load_workspace( ws: ProjectWorkspace, extra_env: &FxHashMap, diff --git a/crates/project-model/src/project_json.rs b/crates/project-model/src/project_json.rs index 4a916e570be2..13bb7d2406a3 100644 --- a/crates/project-model/src/project_json.rs +++ b/crates/project-model/src/project_json.rs @@ -275,6 +275,16 @@ impl ProjectJson { self.manifest.as_ref() } + pub fn crate_by_buildfile(&self, path: &AbsPath) -> Option { + // this is fast enough for now, but it's unfortunate that this is O(crates). + let path: &std::path::Path = path.as_ref(); + self.crates + .iter() + .filter(|krate| krate.is_workspace_member) + .filter_map(|krate| krate.build.clone()) + .find(|build| build.build_file.as_std_path() == path) + } + /// Returns the path to the project's manifest or root folder, if no manifest exists. pub fn manifest_or_root(&self) -> &AbsPath { self.manifest.as_ref().map_or(&self.project_root, |manifest| manifest.as_ref()) @@ -285,7 +295,7 @@ impl ProjectJson { } } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] pub struct ProjectJsonData { sysroot: Option, sysroot_src: Option, @@ -294,7 +304,7 @@ pub struct ProjectJsonData { runnables: Vec, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] struct CrateData { display_name: Option, root_module: Utf8PathBuf, @@ -318,7 +328,7 @@ struct CrateData { build: Option, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] #[serde(rename = "edition")] enum EditionData { #[serde(rename = "2015")] @@ -331,7 +341,7 @@ enum EditionData { Edition2024, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] pub struct BuildData { label: String, build_file: Utf8PathBuf, @@ -418,7 +428,7 @@ pub(crate) struct Dep { pub(crate) name: CrateName, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] struct CrateSource { include_dirs: Vec, exclude_dirs: Vec, diff --git a/crates/project-model/src/workspace.rs b/crates/project-model/src/workspace.rs index 17e40e74de34..a7330f74935a 100644 --- a/crates/project-model/src/workspace.rs +++ b/crates/project-model/src/workspace.rs @@ -30,6 +30,7 @@ use crate::{ utf8_stdout, CargoConfig, CargoWorkspace, InvocationStrategy, ManifestPath, Package, ProjectJson, ProjectManifest, Sysroot, TargetData, TargetKind, WorkspaceBuildScripts, }; +use tracing::{debug, error, info}; pub type FileLoader<'a> = &'a mut dyn for<'b> FnMut(&'b AbsPath) -> Option; @@ -249,7 +250,7 @@ impl ProjectWorkspace { }; let rustc = rustc_dir.and_then(|rustc_dir| { - tracing::info!(workspace = %cargo_toml, rustc_dir = %rustc_dir, "Using rustc source"); + info!(workspace = %cargo_toml, rustc_dir = %rustc_dir, "Using rustc source"); match CargoWorkspace::fetch_metadata( &rustc_dir, cargo_toml.parent(), @@ -772,9 +773,9 @@ impl ProjectWorkspace { }; if matches!(sysroot.mode(), SysrootMode::Stitched(_)) && crate_graph.patch_cfg_if() { - tracing::debug!("Patched std to depend on cfg-if") + debug!("Patched std to depend on cfg-if") } else { - tracing::debug!("Did not patch std to depend on cfg-if") + debug!("Did not patch std to depend on cfg-if") } (crate_graph, proc_macros) } @@ -919,6 +920,11 @@ fn project_json_to_crate_graph( CrateOrigin::Local { repo: None, name: None } }, ); + debug!( + ?crate_graph_crate_id, + crate = display_name.as_ref().map(|name| name.canonical_name()), + "added root to crate graph" + ); if *is_proc_macro { if let Some(path) = proc_macro_dylib_path.clone() { let node = Ok(( @@ -933,6 +939,7 @@ fn project_json_to_crate_graph( ) .collect(); + debug!(map = ?idx_to_crate_id); for (from_idx, krate) in project.crates() { if let Some(&from) = idx_to_crate_id.get(&from_idx) { public_deps.add_to_crate_graph(crate_graph, from); @@ -1158,7 +1165,7 @@ fn detached_file_to_crate_graph( let file_id = match load(detached_file) { Some(file_id) => file_id, None => { - tracing::error!("Failed to load detached file {:?}", detached_file); + error!("Failed to load detached file {:?}", detached_file); return (crate_graph, FxHashMap::default()); } }; @@ -1355,7 +1362,7 @@ fn add_target_crate_root( crate_id } -#[derive(Default)] +#[derive(Default, Debug)] struct SysrootPublicDeps { deps: Vec<(CrateName, CrateId, bool)>, } diff --git a/crates/rust-analyzer/src/bin/main.rs b/crates/rust-analyzer/src/bin/main.rs index 774784f37b00..91145799d1e0 100644 --- a/crates/rust-analyzer/src/bin/main.rs +++ b/crates/rust-analyzer/src/bin/main.rs @@ -176,6 +176,7 @@ fn run_server() -> anyhow::Result<()> { return Err(e.into()); } }; + tracing::info!("InitializeParams: {}", initialize_params); let lsp_types::InitializeParams { root_uri, @@ -265,7 +266,10 @@ fn run_server() -> anyhow::Result<()> { return Err(e.into()); } - if !config.has_linked_projects() && config.detached_files().is_empty() { + if config.discover_workspace_config().is_none() + && !config.has_linked_projects() + && config.detached_files().is_empty() + { config.rediscover_workspaces(); } diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index ef890fa3e632..b9d6fe8801ca 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -330,6 +330,14 @@ config_data! { /// `textDocument/rangeFormatting` request. The rustfmt option is unstable and only /// available on a nightly build. rustfmt_rangeFormatting_enable: bool = false, + + /// Enables automatic discovery of projects using [`DiscoverWorkspaceConfig::command`]. + /// + /// [`DiscoverWorkspaceConfig`] also requires setting `progress_label` and `files_to_watch`. + /// `progress_label` is used for the title in progress indictators, whereas `files_to_watch` + /// is used to determine which build system-specific files should be watched in order to + /// reload rust-analyzer. + workspace_discoverConfig: Option = None, } } @@ -905,6 +913,21 @@ impl Config { ); (config, e, should_update) } + + pub fn add_linked_projects(&mut self, data: ProjectJsonData, buildfile: AbsPathBuf) { + let linked_projects = &mut self.client_config.0.global.linkedProjects; + + let new_project = ManifestOrProjectJson::DiscoveredProjectJson { data, buildfile }; + match linked_projects { + Some(projects) => { + match projects.iter_mut().find(|p| p.manifest() == new_project.manifest()) { + Some(p) => *p = new_project, + None => projects.push(new_project), + } + } + None => *linked_projects = Some(vec![new_project]), + } + } } #[derive(Default, Debug)] @@ -986,6 +1009,14 @@ impl From for LinkedProject { } } +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DiscoverWorkspaceConfig { + pub command: Vec, + pub progress_label: String, + pub files_to_watch: Vec, +} + pub struct CallInfoConfig { pub params_only: bool, pub docs: bool, @@ -1532,15 +1563,27 @@ impl Config { pub fn has_linked_projects(&self) -> bool { !self.linkedProjects().is_empty() } - pub fn linked_manifests(&self) -> impl Iterator + '_ { + + pub fn linked_manifests(&self) -> impl Iterator + '_ { self.linkedProjects().iter().filter_map(|it| match it { ManifestOrProjectJson::Manifest(p) => Some(&**p), - ManifestOrProjectJson::ProjectJson(_) => None, + // despite having a buildfile, using this variant as a manifest + // will fail. + ManifestOrProjectJson::DiscoveredProjectJson { .. } => None, + ManifestOrProjectJson::ProjectJson { .. } => None, }) } + pub fn has_linked_project_jsons(&self) -> bool { - self.linkedProjects().iter().any(|it| matches!(it, ManifestOrProjectJson::ProjectJson(_))) + self.linkedProjects() + .iter() + .any(|it| matches!(it, ManifestOrProjectJson::ProjectJson { .. })) + } + + pub fn discover_workspace_config(&self) -> Option<&DiscoverWorkspaceConfig> { + self.workspace_discoverConfig().as_ref() } + pub fn linked_or_discovered_projects(&self) -> Vec { match self.linkedProjects().as_slice() { [] => { @@ -1565,6 +1608,12 @@ impl Config { .ok() .map(Into::into) } + ManifestOrProjectJson::DiscoveredProjectJson { data, buildfile } => { + let root_path = + buildfile.parent().expect("Unable to get parent of buildfile"); + + Some(ProjectJson::new(None, root_path, data.clone()).into()) + } ManifestOrProjectJson::ProjectJson(it) => { Some(ProjectJson::new(None, &self.root_path, it.clone()).into()) } @@ -2284,11 +2333,49 @@ mod single_or_array { } } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(untagged)] enum ManifestOrProjectJson { - Manifest(Utf8PathBuf), + Manifest( + #[serde(serialize_with = "serialize_abs_pathbuf")] + #[serde(deserialize_with = "deserialize_abs_pathbuf")] + AbsPathBuf, + ), ProjectJson(ProjectJsonData), + DiscoveredProjectJson { + data: ProjectJsonData, + #[serde(serialize_with = "serialize_abs_pathbuf")] + #[serde(deserialize_with = "deserialize_abs_pathbuf")] + buildfile: AbsPathBuf, + }, +} + +fn deserialize_abs_pathbuf<'de, D>(de: D) -> std::result::Result +where + D: serde::de::Deserializer<'de>, +{ + let path = String::deserialize(de)?; + + AbsPathBuf::try_from(path.as_ref()) + .map_err(|err| serde::de::Error::custom(format!("invalid path name: {err:?}"))) +} + +fn serialize_abs_pathbuf(path: &AbsPathBuf, se: S) -> Result +where + S: serde::Serializer, +{ + let path: &Utf8Path = path.as_ref(); + se.serialize_str(path.as_str()) +} + +impl ManifestOrProjectJson { + fn manifest(&self) -> Option<&AbsPath> { + match self { + ManifestOrProjectJson::Manifest(manifest) => Some(manifest), + ManifestOrProjectJson::DiscoveredProjectJson { buildfile, .. } => Some(buildfile), + ManifestOrProjectJson::ProjectJson(_) => None, + } + } } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -3277,6 +3364,29 @@ fn field_props(field: &str, ty: &str, doc: &[&str], default: &str) -> serde_json }, ], }, + "Option" => set! { + "anyOf": [ + { + "type": "null" + }, + { + "type": "object", + "properties": { + "command": { + "type": "array", + "items": { "type": "string" } + }, + "progressLabel": { + "type": "string" + }, + "filesToWatch": { + "type": "array", + "items": { "type": "string" } + }, + } + } + ] + }, _ => panic!("missing entry for {ty}: {default} (field {field})"), } diff --git a/crates/rust-analyzer/src/global_state.rs b/crates/rust-analyzer/src/global_state.rs index 717d8a632c3e..d3e8233eaa4d 100644 --- a/crates/rust-analyzer/src/global_state.rs +++ b/crates/rust-analyzer/src/global_state.rs @@ -20,9 +20,9 @@ use parking_lot::{ use proc_macro_api::ProcMacroServer; use project_model::{ManifestPath, ProjectWorkspace, ProjectWorkspaceKind, WorkspaceBuildScripts}; use rustc_hash::{FxHashMap, FxHashSet}; -use tracing::{span, Level}; +use tracing::{span, trace, Level}; use triomphe::Arc; -use vfs::{AnchoredPathBuf, ChangeKind, Vfs}; +use vfs::{AbsPathBuf, AnchoredPathBuf, ChangeKind, Vfs}; use crate::{ config::{Config, ConfigChange, ConfigErrors}, @@ -41,6 +41,11 @@ use crate::{ task_pool::{TaskPool, TaskQueue}, }; +pub(crate) struct FetchWorkspaceRequest { + pub(crate) path: Option, + pub(crate) force_crate_graph_reload: bool, +} + // Enforces drop order pub(crate) struct Handle { pub(crate) handle: H, @@ -95,6 +100,11 @@ pub(crate) struct GlobalState { pub(crate) test_run_receiver: Receiver, pub(crate) test_run_remaining_jobs: usize, + // Project loading + pub(crate) discover_handle: Option, + pub(crate) discover_sender: Sender, + pub(crate) discover_receiver: Receiver, + // VFS pub(crate) loader: Handle, Receiver>, pub(crate) vfs: Arc)>>, @@ -134,11 +144,12 @@ pub(crate) struct GlobalState { // op queues pub(crate) fetch_workspaces_queue: - OpQueue>, bool)>>, + OpQueue>, bool)>>, pub(crate) fetch_build_data_queue: OpQueue<(), (Arc>, Vec>)>, pub(crate) fetch_proc_macros_queue: OpQueue, bool>, pub(crate) prime_caches_queue: OpQueue, + pub(crate) discover_workspace_queue: OpQueue, /// A deferred task queue. /// @@ -146,7 +157,7 @@ pub(crate) struct GlobalState { /// handlers, as accessing the database may block latency-sensitive /// interactions and should be moved away from the main thread. /// - /// For certain features, such as [`lsp_ext::UnindexedProjectParams`], + /// For certain features, such as [`GlobalState::handle_discover_msg`], /// this queue should run only *after* [`GlobalState::process_changes`] has /// been called. pub(crate) deferred_task_queue: TaskQueue, @@ -202,6 +213,9 @@ impl GlobalState { } let (flycheck_sender, flycheck_receiver) = unbounded(); let (test_run_sender, test_run_receiver) = unbounded(); + + let (discover_sender, discover_receiver) = unbounded(); + let mut this = GlobalState { sender, req_queue: ReqQueue::default(), @@ -233,6 +247,10 @@ impl GlobalState { test_run_receiver, test_run_remaining_jobs: 0, + discover_handle: None, + discover_sender, + discover_receiver, + vfs: Arc::new(RwLock::new((vfs::Vfs::default(), IntMap::default()))), vfs_config_version: 0, vfs_progress_config_version: 0, @@ -247,6 +265,7 @@ impl GlobalState { fetch_proc_macros_queue: OpQueue::default(), prime_caches_queue: OpQueue::default(), + discover_workspace_queue: OpQueue::default(), deferred_task_queue: task_queue, }; @@ -296,11 +315,24 @@ impl GlobalState { modified_rust_files.push(file.file_id); } + let additional_files = self + .config + .discover_workspace_config() + .map(|cfg| { + cfg.files_to_watch.iter().map(String::as_str).collect::>() + }) + .unwrap_or_default(); + let path = path.to_path_buf(); if file.is_created_or_deleted() { workspace_structure_change.get_or_insert((path, false)).1 |= self.crate_graph_file_dependencies.contains(vfs_path); - } else if reload::should_refresh_for_change(&path, file.kind()) { + } else if reload::should_refresh_for_change( + &path, + file.kind(), + &additional_files, + ) { + trace!(?path, kind = ?file.kind(), "refreshing for a change"); workspace_structure_change.get_or_insert((path.clone(), false)); } } @@ -419,7 +451,7 @@ impl GlobalState { self.fetch_workspaces_queue.request_op( format!("workspace vfs file change: {path}"), - force_crate_graph_reload, + FetchWorkspaceRequest { path: Some(path.to_owned()), force_crate_graph_reload }, ); } } diff --git a/crates/rust-analyzer/src/handlers/notification.rs b/crates/rust-analyzer/src/handlers/notification.rs index 2dbc297ea6c2..4e94f5f31d81 100644 --- a/crates/rust-analyzer/src/handlers/notification.rs +++ b/crates/rust-analyzer/src/handlers/notification.rs @@ -14,7 +14,7 @@ use vfs::{AbsPathBuf, ChangeKind, VfsPath}; use crate::{ config::{Config, ConfigChange}, - global_state::GlobalState, + global_state::{FetchWorkspaceRequest, GlobalState}, lsp::{from_proto, utils::apply_document_changes}, lsp_ext::{self, RunFlycheckParams}, mem_docs::DocumentData, @@ -73,7 +73,9 @@ pub(crate) fn handle_did_open_text_document( tracing::info!("New file content set {:?}", params.text_document.text); state.vfs.write().0.set_file_contents(path, Some(params.text_document.text.into_bytes())); - if state.config.notifications().unindexed_project { + if state.config.discover_workspace_config().is_some() + || state.config.notifications().unindexed_project + { tracing::debug!("queuing task"); let _ = state .deferred_task_queue @@ -150,15 +152,29 @@ pub(crate) fn handle_did_save_text_document( if let Ok(vfs_path) = from_proto::vfs_path(¶ms.text_document.uri) { // Re-fetch workspaces if a workspace related file has changed - if let Some(abs_path) = vfs_path.as_path() { - if reload::should_refresh_for_change(abs_path, ChangeKind::Modify) { - state - .fetch_workspaces_queue - .request_op(format!("workspace vfs file change saved {abs_path}"), false); - } else if state.detached_files.contains(abs_path) { - state - .fetch_workspaces_queue - .request_op(format!("detached file saved {abs_path}"), false); + if let Some(path) = vfs_path.as_path() { + let additional_files = &state + .config + .discover_workspace_config() + .map(|cfg| cfg.files_to_watch.iter().map(String::as_str).collect::>()) + .unwrap_or_default(); + + if reload::should_refresh_for_change(path, ChangeKind::Modify, additional_files) { + state.fetch_workspaces_queue.request_op( + format!("workspace vfs file change saved {path}"), + FetchWorkspaceRequest { + path: Some(path.to_owned()), + force_crate_graph_reload: false, + }, + ); + } else if state.detached_files.contains(path) { + state.fetch_workspaces_queue.request_op( + format!("detached file saved {path}"), + FetchWorkspaceRequest { + path: Some(path.to_owned()), + force_crate_graph_reload: false, + }, + ); } } @@ -240,7 +256,9 @@ pub(crate) fn handle_did_change_workspace_folders( if !config.has_linked_projects() && config.detached_files().is_empty() { config.rediscover_workspaces(); - state.fetch_workspaces_queue.request_op("client workspaces changed".to_owned(), false) + + let req = FetchWorkspaceRequest { path: None, force_crate_graph_reload: false }; + state.fetch_workspaces_queue.request_op("client workspaces changed".to_owned(), req); } Ok(()) diff --git a/crates/rust-analyzer/src/handlers/request.rs b/crates/rust-analyzer/src/handlers/request.rs index 8e39b15da3dd..81b8be93fe85 100644 --- a/crates/rust-analyzer/src/handlers/request.rs +++ b/crates/rust-analyzer/src/handlers/request.rs @@ -37,7 +37,7 @@ use vfs::{AbsPath, AbsPathBuf, FileId, VfsPath}; use crate::{ config::{Config, RustfmtConfig, WorkspaceSymbolConfig}, diff::diff, - global_state::{GlobalState, GlobalStateSnapshot}, + global_state::{FetchWorkspaceRequest, GlobalState, GlobalStateSnapshot}, hack_recover_crate_name, line_index::LineEndings, lsp::{ @@ -57,7 +57,8 @@ pub(crate) fn handle_workspace_reload(state: &mut GlobalState, _: ()) -> anyhow: state.proc_macro_clients = Arc::from_iter([]); state.build_deps_changed = false; - state.fetch_workspaces_queue.request_op("reload workspace request".to_owned(), false); + let req = FetchWorkspaceRequest { path: None, force_crate_graph_reload: false }; + state.fetch_workspaces_queue.request_op("reload workspace request".to_owned(), req); Ok(()) } diff --git a/crates/rust-analyzer/src/lsp/ext.rs b/crates/rust-analyzer/src/lsp/ext.rs index b82ba4419041..8d0f654d8b0c 100644 --- a/crates/rust-analyzer/src/lsp/ext.rs +++ b/crates/rust-analyzer/src/lsp/ext.rs @@ -535,7 +535,7 @@ pub struct ServerStatusParams { pub message: Option, } -#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug)] #[serde(rename_all = "camelCase")] pub enum Health { Ok, diff --git a/crates/rust-analyzer/src/lsp/utils.rs b/crates/rust-analyzer/src/lsp/utils.rs index 800c0eee53a0..46797ec65845 100644 --- a/crates/rust-analyzer/src/lsp/utils.rs +++ b/crates/rust-analyzer/src/lsp/utils.rs @@ -74,7 +74,6 @@ impl GlobalState { } } - /// Sends a notification to the client containing the error `message`. /// If `additional_info` is [`Some`], appends a note to the notification telling to check the logs. /// This will always log `message` + `additional_info` to the server's error log. pub(crate) fn show_and_log_error(&mut self, message: String, additional_info: Option) { diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs index 07414a6e49cc..83394812eee8 100644 --- a/crates/rust-analyzer/src/main_loop.rs +++ b/crates/rust-analyzer/src/main_loop.rs @@ -9,18 +9,19 @@ use std::{ use always_assert::always; use crossbeam_channel::{select, Receiver}; +use flycheck::JsonArguments; use ide_db::base_db::{SourceDatabase, SourceDatabaseExt, VfsPath}; use lsp_server::{Connection, Notification, Request}; use lsp_types::{notification::Notification as _, TextDocumentIdentifier}; use stdx::thread::ThreadIntent; use tracing::{span, Level}; -use vfs::FileId; +use vfs::{AbsPathBuf, FileId}; use crate::{ config::Config, diagnostics::{fetch_native_diagnostics, DiagnosticsGeneration}, dispatch::{NotificationDispatcher, RequestDispatcher}, - global_state::{file_id_to_url, url_to_file_id, GlobalState}, + global_state::{file_id_to_url, url_to_file_id, FetchWorkspaceRequest, GlobalState}, hack_recover_crate_name, lsp::{ from_proto, to_proto, @@ -62,6 +63,7 @@ enum Event { Vfs(vfs::loader::Message), Flycheck(flycheck::Message), TestResult(flycheck::CargoTestMessage), + DiscoverProject(flycheck::DiscoverProjectMessage), } impl fmt::Display for Event { @@ -73,6 +75,7 @@ impl fmt::Display for Event { Event::Flycheck(_) => write!(f, "Event::Flycheck"), Event::QueuedTask(_) => write!(f, "Event::QueuedTask"), Event::TestResult(_) => write!(f, "Event::TestResult"), + Event::DiscoverProject(_) => write!(f, "Event::DiscoverProject"), } } } @@ -86,6 +89,7 @@ pub(crate) enum QueuedTask { #[derive(Debug)] pub(crate) enum Task { Response(lsp_server::Response), + DiscoverLinkedProjects(DiscoverProjectParam), ClientNotification(lsp_ext::UnindexedProjectParams), Retry(lsp_server::Request), Diagnostics(DiagnosticsGeneration, Vec<(FileId, Vec)>), @@ -97,6 +101,12 @@ pub(crate) enum Task { BuildDepsHaveChanged, } +#[derive(Debug)] +pub(crate) enum DiscoverProjectParam { + Label(String), + Path(AbsPathBuf), +} + #[derive(Debug)] pub(crate) enum PrimeCachesProgress { Begin, @@ -134,6 +144,7 @@ impl fmt::Debug for Event { Event::Vfs(it) => fmt::Debug::fmt(it, f), Event::Flycheck(it) => fmt::Debug::fmt(it, f), Event::TestResult(it) => fmt::Debug::fmt(it, f), + Event::DiscoverProject(it) => fmt::Debug::fmt(it, f), } } } @@ -143,14 +154,24 @@ impl GlobalState { self.update_status_or_notify(); if self.config.did_save_text_document_dynamic_registration() { - self.register_did_save_capability(); + let additional_patterns = self + .config + .discover_workspace_config() + .map(|cfg| cfg.files_to_watch.clone().into_iter()) + .into_iter() + .flatten() + .map(|f| format!("**/{f}")); + self.register_did_save_capability(additional_patterns); } - self.fetch_workspaces_queue.request_op("startup".to_owned(), false); - if let Some((cause, force_crate_graph_reload)) = - self.fetch_workspaces_queue.should_start_op() - { - self.fetch_workspaces(cause, force_crate_graph_reload); + if self.config.discover_workspace_config().is_none() { + let req = FetchWorkspaceRequest { path: None, force_crate_graph_reload: false }; + self.fetch_workspaces_queue.request_op("startup".to_owned(), req); + if let Some((cause, FetchWorkspaceRequest { path, force_crate_graph_reload })) = + self.fetch_workspaces_queue.should_start_op() + { + self.fetch_workspaces(cause, path, force_crate_graph_reload); + } } while let Some(event) = self.next_event(&inbox) { @@ -167,32 +188,36 @@ impl GlobalState { anyhow::bail!("client exited without proper shutdown sequence") } - fn register_did_save_capability(&mut self) { + fn register_did_save_capability(&mut self, additional_patterns: impl Iterator) { + let additional_filters = additional_patterns.map(|pattern| lsp_types::DocumentFilter { + language: None, + scheme: None, + pattern: (Some(pattern)), + }); + + let mut selectors = vec![ + lsp_types::DocumentFilter { + language: None, + scheme: None, + pattern: Some("**/*.rs".into()), + }, + lsp_types::DocumentFilter { + language: None, + scheme: None, + pattern: Some("**/Cargo.toml".into()), + }, + lsp_types::DocumentFilter { + language: None, + scheme: None, + pattern: Some("**/Cargo.lock".into()), + }, + ]; + selectors.extend(additional_filters); + let save_registration_options = lsp_types::TextDocumentSaveRegistrationOptions { include_text: Some(false), text_document_registration_options: lsp_types::TextDocumentRegistrationOptions { - document_selector: Some(vec![ - lsp_types::DocumentFilter { - language: None, - scheme: None, - pattern: Some("**/*.rs".into()), - }, - lsp_types::DocumentFilter { - language: None, - scheme: None, - pattern: Some("**/Cargo.toml".into()), - }, - lsp_types::DocumentFilter { - language: None, - scheme: None, - pattern: Some("**/Cargo.lock".into()), - }, - lsp_types::DocumentFilter { - language: None, - scheme: None, - pattern: Some("**/rust-analyzer.toml".into()), - }, - ]), + document_selector: Some(selectors), }, }; @@ -230,6 +255,8 @@ impl GlobalState { recv(self.test_run_receiver) -> task => Some(Event::TestResult(task.unwrap())), + recv(self.discover_receiver) -> task => + Some(Event::DiscoverProject(task.unwrap())), } } @@ -340,6 +367,13 @@ impl GlobalState { self.handle_cargo_test_msg(message); } } + Event::DiscoverProject(message) => { + self.handle_discover_msg(message); + // Coalesce many project discovery events into a single loop turn. + while let Ok(message) = self.discover_receiver.try_recv() { + self.handle_discover_msg(message); + } + } } let event_handling_duration = loop_start.elapsed(); @@ -427,11 +461,13 @@ impl GlobalState { } } - if self.config.cargo_autoreload_config() { - if let Some((cause, force_crate_graph_reload)) = + if self.config.cargo_autoreload_config() + || self.config.discover_workspace_config().is_some() + { + if let Some((cause, FetchWorkspaceRequest { path, force_crate_graph_reload })) = self.fetch_workspaces_queue.should_start_op() { - self.fetch_workspaces(cause, force_crate_graph_reload); + self.fetch_workspaces(cause, path, force_crate_graph_reload); } } @@ -647,6 +683,31 @@ impl GlobalState { self.report_progress("Fetching", state, msg, None, None); } + Task::DiscoverLinkedProjects(arg) => { + if let Some(cfg) = self.config.discover_workspace_config() { + if !self.discover_workspace_queue.op_in_progress() { + // the clone is unfortunately necessary to avoid a borrowck error when + // `self.report_progress` is called later + let title = &cfg.progress_label.clone(); + let command = cfg.command.clone(); + let discover = + flycheck::JsonWorkspace::new(self.discover_sender.clone(), command); + + self.report_progress(title, Progress::Begin, None, None, None); + self.discover_workspace_queue + .request_op("Discovering workspace".to_owned(), ()); + let _ = self.discover_workspace_queue.should_start_op(); + + let arg = match arg { + DiscoverProjectParam::Label(label) => JsonArguments::Label(label), + DiscoverProjectParam::Path(path) => JsonArguments::Path(path), + }; + + let handle = discover.spawn(arg).unwrap(); + self.discover_handle = Some(handle); + } + } + } Task::FetchBuildData(progress) => { let (state, msg) = match progress { BuildDataProgress::Begin => (Some(Progress::Begin), None), @@ -755,10 +816,17 @@ impl GlobalState { let id = from_proto::file_id(&snap, &uri).expect("unable to get FileId"); if let Ok(crates) = &snap.analysis.crates_for(id) { if crates.is_empty() { - let params = lsp_ext::UnindexedProjectParams { - text_documents: vec![lsp_types::TextDocumentIdentifier { uri }], + if snap.config.discover_workspace_config().is_some() { + let path = + from_proto::abs_path(&uri).expect("Unable to get AbsPath"); + let arg = DiscoverProjectParam::Path(path); + sender.send(Task::DiscoverLinkedProjects(arg)).unwrap(); + } else if snap.config.notifications().unindexed_project { + let params = lsp_ext::UnindexedProjectParams { + text_documents: vec![lsp_types::TextDocumentIdentifier { uri }], + }; + sender.send(Task::ClientNotification(params)).unwrap(); }; - sender.send(Task::ClientNotification(params)).unwrap(); } else { tracing::debug!(?uri, "is indexed"); } @@ -787,6 +855,33 @@ impl GlobalState { } } + fn handle_discover_msg(&mut self, message: flycheck::DiscoverProjectMessage) { + let title = self + .config + .discover_workspace_config() + .map(|cfg| cfg.progress_label.clone()) + .expect("No title could be found; this is a bug"); + match message { + flycheck::DiscoverProjectMessage::Finished(output) => { + self.report_progress(&title, Progress::End, None, None, None); + self.discover_workspace_queue.op_completed(()); + + let mut config = Config::clone(&*self.config); + config.add_linked_projects(output.project, output.buildfile); + self.update_configuration(config); + } + flycheck::DiscoverProjectMessage::Progress { message } => { + self.report_progress(&title, Progress::Report, Some(message), None, None) + } + flycheck::DiscoverProjectMessage::Error { message, context } => { + let message = format!("Project discovery failed: {message}"); + self.discover_workspace_queue.op_completed(()); + self.show_and_log_error(message.clone(), context); + self.report_progress(&title, Progress::End, Some(message), None, None) + } + } + } + fn handle_cargo_test_msg(&mut self, message: flycheck::CargoTestMessage) { match message { flycheck::CargoTestMessage::Test { name, state } => { diff --git a/crates/rust-analyzer/src/op_queue.rs b/crates/rust-analyzer/src/op_queue.rs index 932730fc234b..99f9e9829c93 100644 --- a/crates/rust-analyzer/src/op_queue.rs +++ b/crates/rust-analyzer/src/op_queue.rs @@ -3,6 +3,7 @@ pub(crate) type Cause = String; +#[derive(Debug)] pub(crate) struct OpQueue { op_requested: Option<(Cause, Args)>, op_in_progress: bool, diff --git a/crates/rust-analyzer/src/reload.rs b/crates/rust-analyzer/src/reload.rs index bd0f733ef390..f297d7c4d12d 100644 --- a/crates/rust-analyzer/src/reload.rs +++ b/crates/rust-analyzer/src/reload.rs @@ -33,11 +33,12 @@ use vfs::{AbsPath, AbsPathBuf, ChangeKind}; use crate::{ config::{Config, FilesWatcher, LinkedProject}, - global_state::GlobalState, + global_state::{FetchWorkspaceRequest, GlobalState}, lsp_ext, - main_loop::Task, + main_loop::{DiscoverProjectParam, Task}, op_queue::Cause, }; +use tracing::{debug, info}; #[derive(Debug)] pub(crate) enum ProjectWorkspaceProgress { @@ -66,6 +67,7 @@ impl GlobalState { || self.fetch_workspaces_queue.op_in_progress() || self.fetch_build_data_queue.op_in_progress() || self.fetch_proc_macros_queue.op_in_progress() + || self.discover_workspace_queue.op_in_progress() || self.vfs_progress_config_version < self.vfs_config_version || self.vfs_progress_n_done < self.vfs_progress_n_total) } @@ -81,9 +83,11 @@ impl GlobalState { &self.config.lru_query_capacities_config().cloned().unwrap_or_default(), ); } + if self.config.linked_or_discovered_projects() != old_config.linked_or_discovered_projects() { - self.fetch_workspaces_queue.request_op("discovered projects changed".to_owned(), false) + let req = FetchWorkspaceRequest { path: None, force_crate_graph_reload: false }; + self.fetch_workspaces_queue.request_op("discovered projects changed".to_owned(), req) } else if self.config.flycheck() != old_config.flycheck() { self.reload_flycheck(); } @@ -109,6 +113,7 @@ impl GlobalState { if !self.config.cargo_autoreload() && self.is_quiescent() && self.fetch_workspaces_queue.op_requested() + && self.config.discover_workspace_config().is_none() { status.health |= lsp_ext::Health::Warning; message.push_str("Auto-reloading is disabled and the workspace has changed, a manual workspace reload is required.\n\n"); @@ -124,7 +129,6 @@ impl GlobalState { status.health |= lsp_ext::Health::Warning; message.push_str("Failed to run build scripts of some packages.\n\n"); } - if let Some(err) = &self.config_errors { status.health |= lsp_ext::Health::Warning; format_to!(message, "{err}\n"); @@ -203,8 +207,13 @@ impl GlobalState { status } - pub(crate) fn fetch_workspaces(&mut self, cause: Cause, force_crate_graph_reload: bool) { - tracing::info!(%cause, "will fetch workspaces"); + pub(crate) fn fetch_workspaces( + &mut self, + cause: Cause, + path: Option, + force_crate_graph_reload: bool, + ) { + info!(%cause, "will fetch workspaces"); self.task_pool.handle.spawn_with_sender(ThreadIntent::Worker, { let linked_projects = self.config.linked_or_discovered_projects(); @@ -217,6 +226,10 @@ impl GlobalState { .filter_map(Result::ok) .collect(); let cargo_config = self.config.cargo(); + let discover_command = self.config.discover_workspace_config().cloned(); + let is_quiescent = !(self.discover_workspace_queue.op_in_progress() + || self.vfs_progress_config_version < self.vfs_config_version + || self.vfs_progress_n_done < self.vfs_progress_n_total); move |sender| { let progress = { @@ -230,10 +243,26 @@ impl GlobalState { sender.send(Task::FetchWorkspace(ProjectWorkspaceProgress::Begin)).unwrap(); + if let (Some(_command), Some(path)) = (&discover_command, &path) { + let build = linked_projects.iter().find_map(|project| match project { + LinkedProject::InlineJsonProject(it) => it.crate_by_buildfile(path), + _ => None, + }); + + if let Some(build) = build { + if is_quiescent { + let arg = DiscoverProjectParam::Label(build.label); + sender.send(Task::DiscoverLinkedProjects(arg)).unwrap(); + } + } + } + let mut workspaces = linked_projects .iter() .map(|project| match project { LinkedProject::ProjectManifest(manifest) => { + debug!(path = %manifest, "loading project from manifest"); + project_model::ProjectWorkspace::load( manifest.clone(), &cargo_config, @@ -241,12 +270,13 @@ impl GlobalState { ) } LinkedProject::InlineJsonProject(it) => { - Ok(project_model::ProjectWorkspace::load_inline( + let workspace = project_model::ProjectWorkspace::load_inline( it.clone(), cargo_config.target.as_deref(), &cargo_config.extra_env, &cargo_config.cfg_overrides, - )) + ); + Ok(workspace) } }) .collect::>(); @@ -272,7 +302,7 @@ impl GlobalState { )); } - tracing::info!("did fetch workspaces {:?}", workspaces); + info!(?workspaces, "did fetch workspaces"); sender .send(Task::FetchWorkspace(ProjectWorkspaceProgress::End( workspaces, @@ -284,7 +314,7 @@ impl GlobalState { } pub(crate) fn fetch_build_data(&mut self, cause: Cause) { - tracing::info!(%cause, "will fetch build data"); + info!(%cause, "will fetch build data"); let workspaces = Arc::clone(&self.workspaces); let config = self.config.cargo(); let root_path = self.config.root_path().clone(); @@ -310,7 +340,7 @@ impl GlobalState { } pub(crate) fn fetch_proc_macros(&mut self, cause: Cause, paths: Vec) { - tracing::info!(%cause, "will load proc macros"); + info!(%cause, "will load proc macros"); let ignored_proc_macros = self.config.ignored_proc_macros().clone(); let proc_macro_clients = self.proc_macro_clients.clone(); @@ -381,6 +411,7 @@ impl GlobalState { return; }; + info!(%cause, ?force_reload_crate_graph); if self.fetch_workspace_error().is_err() && !self.workspaces.is_empty() { if *force_reload_crate_graph { self.recreate_crate_graph(cause); @@ -402,7 +433,7 @@ impl GlobalState { if same_workspaces { let (workspaces, build_scripts) = self.fetch_build_data_queue.last_op_result(); if Arc::ptr_eq(workspaces, &self.workspaces) { - tracing::debug!("set build scripts to workspaces"); + info!("set build scripts to workspaces"); let workspaces = workspaces .iter() @@ -414,9 +445,10 @@ impl GlobalState { }) .collect::>(); // Workspaces are the same, but we've updated build data. + info!("same workspace, but new build data"); self.workspaces = Arc::new(workspaces); } else { - tracing::info!("build scripts do not match the version of the active workspace"); + info!("build scripts do not match the version of the active workspace"); if *force_reload_crate_graph { self.recreate_crate_graph(cause); } @@ -426,7 +458,7 @@ impl GlobalState { return; } } else { - tracing::debug!("abandon build scripts for workspaces"); + info!("abandon build scripts for workspaces"); // Here, we completely changed the workspace (Cargo.toml edit), so // we don't care about build-script results, they are stale. @@ -521,7 +553,7 @@ impl GlobalState { if (self.proc_macro_clients.is_empty() || !same_workspaces) && self.config.expand_proc_macros() { - tracing::info!("Spawning proc-macro servers"); + info!("Spawning proc-macro servers"); self.proc_macro_clients = Arc::from_iter(self.workspaces.iter().map(|ws| { let path = match self.config.proc_macro_srv() { @@ -548,7 +580,7 @@ impl GlobalState { _ => Default::default(), }; - tracing::info!("Using proc-macro server at {path}"); + info!("Using proc-macro server at {path}"); ProcMacroServer::spawn(&path, &env).map_err(|err| { tracing::error!( @@ -574,12 +606,14 @@ impl GlobalState { self.source_root_config = project_folders.source_root_config; self.local_roots_parent_map = Arc::new(self.source_root_config.source_root_parent_map()); + info!(?cause, "recreating the crate graph"); self.recreate_crate_graph(cause); - tracing::info!("did switch workspaces"); + info!("did switch workspaces"); } fn recreate_crate_graph(&mut self, cause: String) { + info!(?cause, "Building Crate Graph"); self.report_progress( "Building CrateGraph", crate::lsp::utils::Progress::Begin, @@ -644,12 +678,19 @@ impl GlobalState { let Some((last_op_result, _)) = self.fetch_workspaces_queue.last_op_result() else { return Ok(()); }; - if last_op_result.is_empty() { - stdx::format_to!(buf, "rust-analyzer failed to discover workspace"); - } else { - for ws in last_op_result { - if let Err(err) = ws { - stdx::format_to!(buf, "rust-analyzer failed to load workspace: {:#}\n", err); + + if !self.discover_workspace_queue.op_in_progress() { + if last_op_result.is_empty() { + stdx::format_to!(buf, "rust-analyzer failed to discover workspace"); + } else { + for ws in last_op_result { + if let Err(err) = ws { + stdx::format_to!( + buf, + "rust-analyzer failed to load workspace: {:#}\n", + err + ); + } } } } @@ -804,7 +845,11 @@ pub fn ws_to_crate_graph( (crate_graph, proc_macro_paths, layouts, toolchains) } -pub(crate) fn should_refresh_for_change(path: &AbsPath, change_kind: ChangeKind) -> bool { +pub(crate) fn should_refresh_for_change( + path: &AbsPath, + change_kind: ChangeKind, + additional_paths: &[&str], +) -> bool { const IMPLICIT_TARGET_FILES: &[&str] = &["build.rs", "src/main.rs", "src/lib.rs"]; const IMPLICIT_TARGET_DIRS: &[&str] = &["src/bin", "examples", "tests", "benches"]; @@ -816,6 +861,11 @@ pub(crate) fn should_refresh_for_change(path: &AbsPath, change_kind: ChangeKind) if let "Cargo.toml" | "Cargo.lock" = file_name { return true; } + + if additional_paths.contains(&file_name) { + return true; + } + if change_kind == ChangeKind::Modify { return false; } diff --git a/crates/rust-analyzer/src/tracing/config.rs b/crates/rust-analyzer/src/tracing/config.rs index fcdbf6c69497..f330754f19a1 100644 --- a/crates/rust-analyzer/src/tracing/config.rs +++ b/crates/rust-analyzer/src/tracing/config.rs @@ -48,7 +48,10 @@ where let writer = self.writer; - let ra_fmt_layer = tracing_subscriber::fmt::layer().with_writer(writer).with_filter(filter); + let ra_fmt_layer = tracing_subscriber::fmt::layer() + .with_target(false) + .with_writer(writer) + .with_filter(filter); let mut chalk_layer = None; if let Some(chalk_filter) = self.chalk_filter { diff --git a/crates/rust-analyzer/tests/slow-tests/main.rs b/crates/rust-analyzer/tests/slow-tests/main.rs index f886df60e681..baf6677ef267 100644 --- a/crates/rust-analyzer/tests/slow-tests/main.rs +++ b/crates/rust-analyzer/tests/slow-tests/main.rs @@ -31,7 +31,6 @@ use lsp_types::{ InlayHint, InlayHintLabel, InlayHintParams, PartialResultParams, Position, Range, RenameFilesParams, TextDocumentItem, TextDocumentPositionParams, WorkDoneProgressParams, }; - use rust_analyzer::lsp::ext::{OnEnter, Runnables, RunnablesParams, UnindexedProject}; use serde_json::json; use stdx::format_to_acc; diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md index 695fec7e8e01..e68641b7fba7 100644 --- a/docs/dev/lsp-extensions.md +++ b/docs/dev/lsp-extensions.md @@ -1,5 +1,5 @@