From db43a5a6e97ceb90920169afef4aaa9aa06e82ac Mon Sep 17 00:00:00 2001 From: David Barsky Date: Thu, 18 Jul 2024 12:01:26 -0400 Subject: [PATCH] feature: move `linked_projects` discovery to the rust-analyzer server --- Cargo.lock | 127 +++++------ crates/flycheck/Cargo.toml | 1 + crates/flycheck/src/lib.rs | 3 +- crates/flycheck/src/project_json.rs | 152 +++++++++++++ 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 | 208 +++++++++++++++++- crates/rust-analyzer/src/global_state.rs | 46 +++- .../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 | 173 +++++++++++---- crates/rust-analyzer/src/op_queue.rs | 1 + crates/rust-analyzer/src/reload.rs | 100 +++++++-- 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 | 98 +++++++++ editors/code/package.json | 34 +++ 22 files changed, 877 insertions(+), 169 deletions(-) create mode 100644 crates/flycheck/src/project_json.rs diff --git a/Cargo.lock b/Cargo.lock index 2a1e7c4d59aa..b98a1195d8bb 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", ] @@ -104,9 +104,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", ] @@ -136,9 +136,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" @@ -232,18 +232,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", ] @@ -269,9 +269,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" @@ -366,9 +366,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" @@ -431,6 +431,7 @@ dependencies = [ "crossbeam-channel", "paths", "process-wrap", + "project-model", "rustc-hash", "serde", "serde_json", @@ -476,9 +477,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" @@ -916,9 +917,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", @@ -1086,18 +1087,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", ] @@ -1162,9 +1163,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", ] @@ -1187,18 +1188,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", ] @@ -1223,9 +1224,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", ] @@ -1800,18 +1801,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", @@ -1843,9 +1844,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", ] @@ -1867,9 +1868,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", ] @@ -1922,9 +1923,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", @@ -2009,18 +2010,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", @@ -2104,9 +2105,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", @@ -2116,18 +2117,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", @@ -2201,9 +2202,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", @@ -2213,9 +2214,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", @@ -2555,9 +2556,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/lib.rs b/crates/flycheck/src/lib.rs index 778def5d2bed..ce148037fcfb 100644 --- a/crates/flycheck/src/lib.rs +++ b/crates/flycheck/src/lib.rs @@ -20,6 +20,7 @@ pub use cargo_metadata::diagnostic::{ use toolchain::Tool; mod command; +pub mod project_json; mod test_runner; use command::{CommandHandle, ParseFromLine}; @@ -240,7 +241,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/flycheck/src/project_json.rs b/crates/flycheck/src/project_json.rs new file mode 100644 index 000000000000..b6e4495bc6d6 --- /dev/null +++ b/crates/flycheck/src/project_json.rs @@ -0,0 +1,152 @@ +//! A `cargo-metadata`-equivalent for non-Cargo build systems. +use std::{io, process::Command}; + +use crossbeam_channel::Sender; +use paths::{AbsPathBuf, Utf8Path, Utf8PathBuf}; +use project_model::ProjectJsonData; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::command::{CommandHandle, ParseFromLine}; + +pub const ARG_PLACEHOLDER: &str = "{arg}"; + +/// A command wrapper for getting a `rust-project.json`. +/// +/// This is analogous to `cargo-metadata`, but for non-Cargo build systems. +pub struct Discover { + command: Vec, + sender: Sender, +} + +#[derive(PartialEq, Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum DiscoverArgument { + Path(#[serde(serialize_with = "serialize_abs_pathbuf")] AbsPathBuf), + Buildfile(#[serde(serialize_with = "serialize_abs_pathbuf")] AbsPathBuf), +} + +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 Discover { + /// Create a new [Discover]. + pub fn new(sender: Sender, command: Vec) -> Self { + Self { sender, command } + } + + /// Spawn the command inside [Discover] and report progress, if any. + pub fn spawn(&self, discover_arg: DiscoverArgument) -> io::Result { + let command = &self.command[0]; + let args = &self.command[1..]; + + let args: Vec = args + .iter() + .map(|arg| { + if arg == ARG_PLACEHOLDER { + serde_json::to_string(&discover_arg).expect("Unable to serialize args") + } else { + arg.to_owned() + } + }) + .collect(); + + let mut cmd = Command::new(command); + cmd.args(args); + + Ok(DiscoverHandle { _handle: CommandHandle::spawn(cmd, self.sender.clone())? }) + } +} + +/// A handle to a spawned [Discover]. +#[derive(Debug)] +pub struct DiscoverHandle { + _handle: CommandHandle, +} + +/// An enum containing either progress messages, an error, +/// or the materialized `rust-project`. +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(tag = "kind")] +#[serde(rename_all = "snake_case")] +enum DiscoverProjectData { + Finished { buildfile: Utf8PathBuf, project: ProjectJsonData }, + Error { error: String, source: Option }, + Progress { message: String }, +} + +#[derive(Debug, PartialEq, Clone)] +pub enum DiscoverProjectMessage { + Finished { project: ProjectJsonData, buildfile: AbsPathBuf }, + Error { error: String, source: Option }, + Progress { message: String }, +} + +impl DiscoverProjectMessage { + fn new(data: DiscoverProjectData) -> Self { + match data { + DiscoverProjectData::Finished { project, buildfile, .. } => { + let buildfile = buildfile.try_into().expect("Unable to make path absolute"); + DiscoverProjectMessage::Finished { project, buildfile } + } + DiscoverProjectData::Error { error, source } => { + DiscoverProjectMessage::Error { error, source } + } + DiscoverProjectData::Progress { message } => { + DiscoverProjectMessage::Progress { message } + } + } + } +} + +impl ParseFromLine for DiscoverProjectMessage { + fn from_line(line: &str, _error: &mut String) -> Option { + // can the line even be deserialized as JSON? + let Ok(data) = serde_json::from_str::(line) else { + let err = DiscoverProjectData::Error { error: line.to_owned(), source: None }; + return Some(DiscoverProjectMessage::new(err)); + }; + + let Ok(data) = serde_json::from_value::(data) else { + return None; + }; + + let msg = DiscoverProjectMessage::new(data); + Some(msg) + } + + fn from_eof() -> Option { + None + } +} + +#[test] +fn test_deserialization() { + let message = r#" + {"kind": "progress", "message":"querying build system","input":{"files":["src/main.rs"]}} + "#; + let message: DiscoverProjectData = + serde_json::from_str(message).expect("Unable to deserialize message"); + assert!(matches!(message, DiscoverProjectData::Progress { .. })); + + let message = r#" + {"kind": "error", "error":"failed to deserialize command output","source":"command"} + "#; + + let message: DiscoverProjectData = + serde_json::from_str(message).expect("Unable to deserialize message"); + assert!(matches!(message, DiscoverProjectData::Error { .. })); + + let message = r#" + {"kind": "finished", "project": {"sysroot": "foo", "crates": [], "runnables": []}, "buildfile":"rust-analyzer/BUILD"} + "#; + + let message: DiscoverProjectData = + serde_json::from_str(message).expect("Unable to deserialize message"); + assert!(matches!(message, DiscoverProjectData::Finished { .. })); +} diff --git a/crates/load-cargo/src/lib.rs b/crates/load-cargo/src/lib.rs index b7ddbc9665a5..1c81240e4f9c 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 4b55f9d2b9e4..421ef25a45ac 100644 --- a/crates/project-model/src/project_json.rs +++ b/crates/project-model/src/project_json.rs @@ -276,6 +276,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()) @@ -286,7 +296,7 @@ impl ProjectJson { } } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] pub struct ProjectJsonData { sysroot: Option, sysroot_src: Option, @@ -295,7 +305,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, @@ -319,7 +329,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")] @@ -332,7 +342,7 @@ enum EditionData { Edition2024, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] pub struct BuildData { label: String, build_file: Utf8PathBuf, @@ -419,7 +429,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 dd7a11ca85f1..ec6cbf369a36 100644 --- a/crates/project-model/src/workspace.rs +++ b/crates/project-model/src/workspace.rs @@ -31,6 +31,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; @@ -250,7 +251,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(), @@ -767,9 +768,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) } @@ -917,6 +918,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().as_str()), + "added root to crate graph" + ); if *is_proc_macro { if let Some(path) = proc_macro_dylib_path.clone() { let node = Ok(( @@ -931,6 +937,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); @@ -1156,7 +1163,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()); } }; @@ -1351,7 +1358,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 1985093bc5c3..6a980a153c99 100644 --- a/crates/rust-analyzer/src/bin/main.rs +++ b/crates/rust-analyzer/src/bin/main.rs @@ -175,6 +175,7 @@ fn run_server() -> anyhow::Result<()> { return Err(e.into()); } }; + tracing::info!("InitializeParams: {}", initialize_params); let lsp_types::InitializeParams { root_uri, @@ -264,7 +265,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 a0ec3920e6e2..c13a98d22327 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -328,6 +328,102 @@ 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 indicators, whereas `files_to_watch` + /// is used to determine which build system-specific files should be watched in order to + /// reload rust-analyzer. + /// + /// Below is an example of a valid configuration: + /// ```json + /// "rust-analyzer.workspace.discoverConfig": { + /// "command": [ + /// "rust-project", + /// "develop-json", + /// {arg} + /// ], + /// "progressLabel": "rust-analyzer", + /// "filesToWatch": [ + /// "BUCK", + /// ], + /// } + /// ``` + /// + /// ## On `DiscoverWorkspaceConfig::command` + /// + /// **Warning**: This format is provisional and subject to change. + /// + /// [`DiscoverWorkspaceConfig::command`] *must* return a JSON object + /// corresponding to `DiscoverProjectData::Finished`: + /// + /// ```norun + /// #[derive(Debug, Clone, Deserialize, Serialize)] + /// #[serde(tag = "kind")] + /// #[serde(rename_all = "snake_case")] + /// enum DiscoverProjectData { + /// Finished { buildfile: Utf8PathBuf, project: ProjectJsonData }, + /// Error { error: String, source: Option }, + /// Progress { message: String }, + /// } + /// ``` + /// + /// As JSON, `DiscoverProjectData::Finished` is: + /// + /// ```json + /// { + /// // the internally-tagged representation of the enum. + /// "kind": "finished", + /// // the file used by a non-Cargo build system to define + /// // a package or target. + /// "buildfile": "rust-analyzer/BUILD", + /// // the contents of a rust-project.json, elided for brevity + /// "project": { + /// "sysroot": "foo", + /// "crates": [] + /// } + /// } + /// ``` + /// + /// It is encouraged, but not required, to use the other variants on + /// `DiscoverProjectData` to provide a more polished end-user experience. + /// + /// `DiscoverWorkspaceConfig::command` may *optionally* include an `{arg}`, + /// which will be substituted with the JSON-serialized form of the following + /// enum: + /// + /// ```norun + /// #[derive(PartialEq, Clone, Debug, Serialize)] + /// #[serde(rename_all = "camelCase")] + /// pub enum DiscoverArgument { + /// Path(AbsPathBuf), + /// Buildfile(AbsPathBuf), + /// } + /// ``` + /// + /// The JSON representation of `DiscoverArgument::Path` is: + /// + /// ```json + /// { + /// "path": "src/main.rs" + /// } + /// ``` + /// + /// Similarly, the JSON representation of `DiscoverArgument::Buildfile` is: + /// + /// ``` + /// { + /// "buildfile": "BUILD" + /// } + /// ``` + /// + /// `DiscoverArgument::Path` is used to find and generate a `rust-project.json`, + /// and therefore, a workspace, whereas `DiscoverArgument::buildfile` is used to + /// to update an existing workspace. As a reference for implementors, + /// buck2's `rust-project` will likely be useful: + /// https://github.com/facebook/buck2/tree/main/integrations/rust-project. + workspace_discoverConfig: Option = None, } } @@ -924,6 +1020,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)] @@ -988,6 +1099,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, @@ -1528,15 +1647,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() { [] => { @@ -1561,6 +1692,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()) } @@ -2101,11 +2238,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)] @@ -3084,6 +3259,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 1976e4de3068..9fd9bee5377a 100644 --- a/crates/rust-analyzer/src/global_state.rs +++ b/crates/rust-analyzer/src/global_state.rs @@ -6,7 +6,7 @@ use std::{ops::Not as _, time::Instant}; use crossbeam_channel::{unbounded, Receiver, Sender}; -use flycheck::FlycheckHandle; +use flycheck::{project_json, FlycheckHandle}; use hir::ChangeWithProcMacros; use ide::{Analysis, AnalysisHost, Cancellable, FileId, SourceRootId}; use ide_db::base_db::{CrateId, ProcMacroPaths, SourceDatabaseExt}; @@ -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 095d7c941c10..4d9cbae7d1f5 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 89961431143c..8ec159278d4d 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 9a852067f2ea..efa5d47fbaab 100644 --- a/crates/rust-analyzer/src/lsp/ext.rs +++ b/crates/rust-analyzer/src/lsp/ext.rs @@ -531,7 +531,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..db90d2d964c1 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::project_json; 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(project_json::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 { + Buildfile(AbsPathBuf), + 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,35 @@ 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 = + project_json::Discover::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::Buildfile(it) => { + project_json::DiscoverArgument::Buildfile(it) + } + DiscoverProjectParam::Path(it) => { + project_json::DiscoverArgument::Path(it) + } + }; + + 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 +820,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 +859,33 @@ impl GlobalState { } } + fn handle_discover_msg(&mut self, message: project_json::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 { + project_json::DiscoverProjectMessage::Finished { project, buildfile } => { + 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(project, buildfile); + self.update_configuration(config); + } + project_json::DiscoverProjectMessage::Progress { message } => { + self.report_progress(&title, Progress::Report, Some(message), None, None) + } + project_json::DiscoverProjectMessage::Error { error, source } => { + let message = format!("Project discovery failed: {error}"); + self.discover_workspace_queue.op_completed(()); + self.show_and_log_error(message.clone(), source); + 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 1039daf850ca..fb16f28a14bd 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"); @@ -217,8 +221,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(); @@ -231,6 +240,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 = { @@ -244,10 +257,28 @@ 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 path = AbsPathBuf::try_from(build.build_file) + .expect("Unable to convert to an AbsPath"); + let arg = DiscoverProjectParam::Buildfile(path); + 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, @@ -255,12 +286,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::>(); @@ -286,7 +318,7 @@ impl GlobalState { )); } - tracing::info!("did fetch workspaces {:?}", workspaces); + info!(?workspaces, "did fetch workspaces"); sender .send(Task::FetchWorkspace(ProjectWorkspaceProgress::End( workspaces, @@ -298,7 +330,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(); @@ -324,7 +356,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(); @@ -395,6 +427,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); @@ -416,7 +449,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() @@ -428,9 +461,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); } @@ -440,7 +474,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. @@ -535,7 +569,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() { @@ -562,7 +596,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!( @@ -588,12 +622,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, @@ -658,12 +694,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 + ); + } } } } @@ -818,7 +861,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"]; @@ -830,6 +877,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 aa17b587e022..6bbf82a77547 100644 --- a/crates/rust-analyzer/tests/slow-tests/main.rs +++ b/crates/rust-analyzer/tests/slow-tests/main.rs @@ -27,7 +27,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 74acb6f9940b..fbb4fc6113f9 100644 --- a/docs/dev/lsp-extensions.md +++ b/docs/dev/lsp-extensions.md @@ -1,5 +1,5 @@