feature: move linked_projects discovery to the rust-analyzer server

This commit is contained in:
David Barsky 2024-07-18 12:01:26 -04:00
parent cf156a7a43
commit db43a5a6e9
22 changed files with 877 additions and 169 deletions

127
Cargo.lock generated
View file

@ -4,9 +4,9 @@ version = 3
[[package]] [[package]]
name = "addr2line" name = "addr2line"
version = "0.21.0" version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
dependencies = [ dependencies = [
"gimli", "gimli",
] ]
@ -28,9 +28,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.83" version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]] [[package]]
name = "arbitrary" name = "arbitrary"
@ -52,16 +52,16 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
[[package]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.71" version = "0.3.72"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11"
dependencies = [ dependencies = [
"addr2line", "addr2line",
"cc", "cc",
"cfg-if", "cfg-if",
"libc", "libc",
"miniz_oxide", "miniz_oxide",
"object 0.32.2", "object 0.35.0",
"rustc-demangle", "rustc-demangle",
] ]
@ -104,9 +104,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "camino" name = "camino"
version = "1.1.6" version = "1.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@ -136,9 +136,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.97" version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f"
[[package]] [[package]]
name = "cfg" name = "cfg"
@ -232,18 +232,18 @@ checksum = "0d48d8f76bd9331f19fe2aaf3821a9f9fb32c3963e1e3d6ce82a8c09cef7444a"
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.4.0" version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]] [[package]]
name = "crossbeam-channel" name = "crossbeam-channel"
version = "0.5.12" version = "0.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2"
dependencies = [ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
@ -269,9 +269,9 @@ dependencies = [
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.19" version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
[[package]] [[package]]
name = "ctrlc" name = "ctrlc"
@ -366,9 +366,9 @@ checksum = "9bda8e21c04aca2ae33ffc2fd8c23134f3cac46db123ba97bd9d3f3b8a4a85e1"
[[package]] [[package]]
name = "either" name = "either"
version = "1.11.0" version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b"
[[package]] [[package]]
name = "ena" name = "ena"
@ -431,6 +431,7 @@ dependencies = [
"crossbeam-channel", "crossbeam-channel",
"paths", "paths",
"process-wrap", "process-wrap",
"project-model",
"rustc-hash", "rustc-hash",
"serde", "serde",
"serde_json", "serde_json",
@ -476,9 +477,9 @@ dependencies = [
[[package]] [[package]]
name = "gimli" name = "gimli"
version = "0.28.1" version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
@ -916,9 +917,9 @@ dependencies = [
[[package]] [[package]]
name = "libmimalloc-sys" name = "libmimalloc-sys"
version = "0.1.37" version = "0.1.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81eb4061c0582dedea1cbc7aff2240300dd6982e0239d1c99e65c1dbf4a30ba7" checksum = "0e7bb23d733dfcc8af652a78b7bf232f0e967710d044732185e561e47c0336b6"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",
@ -1086,18 +1087,18 @@ dependencies = [
[[package]] [[package]]
name = "mimalloc" name = "mimalloc"
version = "0.1.41" version = "0.1.42"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f41a2280ded0da56c8cf898babb86e8f10651a34adcfff190ae9a1159c6908d" checksum = "e9186d86b79b52f4a77af65604b51225e8db1d6ee7e3f41aec1e40829c71a176"
dependencies = [ dependencies = [
"libmimalloc-sys", "libmimalloc-sys",
] ]
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.7.2" version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae"
dependencies = [ dependencies = [
"adler", "adler",
] ]
@ -1162,9 +1163,9 @@ dependencies = [
[[package]] [[package]]
name = "nu-ansi-term" name = "nu-ansi-term"
version = "0.49.0" version = "0.50.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c073d3c1930d0751774acf49e66653acecb416c3a54c6ec095a9b11caddb5a68" checksum = "dd2800e1520bdc966782168a627aa5d1ad92e33b984bf7c7615d31280c83ff14"
dependencies = [ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
@ -1187,18 +1188,18 @@ dependencies = [
[[package]] [[package]]
name = "object" name = "object"
version = "0.32.2" version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" checksum = "d8dd6c0cdf9429bce006e1362bfce61fa1bfd8c898a643ed8d2b471934701d3d"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
[[package]] [[package]]
name = "object" name = "object"
version = "0.33.0" version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8dd6c0cdf9429bce006e1362bfce61fa1bfd8c898a643ed8d2b471934701d3d" checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@ -1223,9 +1224,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.2" version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
dependencies = [ dependencies = [
"lock_api", "lock_api",
"parking_lot_core", "parking_lot_core",
@ -1379,9 +1380,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.82" version = "1.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -1800,18 +1801,18 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.201" version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.201" version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1843,9 +1844,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "0.6.5" version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@ -1867,9 +1868,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]] [[package]]
name = "smol_str" name = "smol_str"
version = "0.2.1" version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6845563ada680337a52d43bb0b29f396f2d911616f6573012645b9e3d048a49" checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@ -1922,9 +1923,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.63" version = "2.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704" checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2009,18 +2010,18 @@ checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233"
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.60" version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.60" version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2104,9 +2105,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.8.12" version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335"
dependencies = [ dependencies = [
"serde", "serde",
"serde_spanned", "serde_spanned",
@ -2116,18 +2117,18 @@ dependencies = [
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "0.6.5" version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf"
dependencies = [ dependencies = [
"serde", "serde",
] ]
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.22.12" version = "0.22.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde", "serde",
@ -2201,9 +2202,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing-tree" name = "tracing-tree"
version = "0.3.0" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65139ecd2c3f6484c3b99bc01c77afe21e95473630747c7aca525e78b0666675" checksum = "b56c62d2c80033cb36fae448730a2f2ef99410fe3ecbffc916681a32f6807dbe"
dependencies = [ dependencies = [
"nu-ansi-term", "nu-ansi-term",
"tracing-core", "tracing-core",
@ -2213,9 +2214,9 @@ dependencies = [
[[package]] [[package]]
name = "triomphe" name = "triomphe"
version = "0.1.11" version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3" checksum = "1b2cb4fbb9995eeb36ac86fadf24031ccd58f99d6b4b2d7b911db70bddb80d90"
dependencies = [ dependencies = [
"serde", "serde",
"stable_deref_trait", "stable_deref_trait",
@ -2555,9 +2556,9 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.6.8" version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d" checksum = "56c52728401e1dc672a56e81e593e912aa54c78f40246869f78359a2bf24d29d"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]

View file

@ -24,6 +24,7 @@ process-wrap.workspace = true
paths.workspace = true paths.workspace = true
stdx.workspace = true stdx.workspace = true
toolchain.workspace = true toolchain.workspace = true
project-model.workspace = true
[lints] [lints]
workspace = true workspace = true

View file

@ -20,6 +20,7 @@ pub use cargo_metadata::diagnostic::{
use toolchain::Tool; use toolchain::Tool;
mod command; mod command;
pub mod project_json;
mod test_runner; mod test_runner;
use command::{CommandHandle, ParseFromLine}; use command::{CommandHandle, ParseFromLine};
@ -240,7 +241,7 @@ enum FlycheckStatus {
Finished, Finished,
} }
const SAVED_FILE_PLACEHOLDER: &str = "$saved_file"; pub const SAVED_FILE_PLACEHOLDER: &str = "$saved_file";
impl FlycheckActor { impl FlycheckActor {
fn new( fn new(

View file

@ -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<String>,
sender: Sender<DiscoverProjectMessage>,
}
#[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<S>(path: &AbsPathBuf, se: S) -> Result<S::Ok, S::Error>
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<DiscoverProjectMessage>, command: Vec<String>) -> Self {
Self { sender, command }
}
/// Spawn the command inside [Discover] and report progress, if any.
pub fn spawn(&self, discover_arg: DiscoverArgument) -> io::Result<DiscoverHandle> {
let command = &self.command[0];
let args = &self.command[1..];
let args: Vec<String> = 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<DiscoverProjectMessage>,
}
/// 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<String> },
Progress { message: String },
}
#[derive(Debug, PartialEq, Clone)]
pub enum DiscoverProjectMessage {
Finished { project: ProjectJsonData, buildfile: AbsPathBuf },
Error { error: String, source: Option<String> },
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<Self> {
// can the line even be deserialized as JSON?
let Ok(data) = serde_json::from_str::<Value>(line) else {
let err = DiscoverProjectData::Error { error: line.to_owned(), source: None };
return Some(DiscoverProjectMessage::new(err));
};
let Ok(data) = serde_json::from_value::<DiscoverProjectData>(data) else {
return None;
};
let msg = DiscoverProjectMessage::new(data);
Some(msg)
}
fn from_eof() -> Option<Self> {
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 { .. }));
}

View file

@ -17,7 +17,6 @@ use itertools::Itertools;
use proc_macro_api::{MacroDylib, ProcMacroServer}; use proc_macro_api::{MacroDylib, ProcMacroServer};
use project_model::{CargoConfig, ManifestPath, PackageRoot, ProjectManifest, ProjectWorkspace}; use project_model::{CargoConfig, ManifestPath, PackageRoot, ProjectManifest, ProjectWorkspace};
use span::Span; use span::Span;
use tracing::instrument;
use vfs::{file_set::FileSetConfig, loader::Handle, AbsPath, AbsPathBuf, VfsPath}; use vfs::{file_set::FileSetConfig, loader::Handle, AbsPath, AbsPathBuf, VfsPath};
pub struct LoadCargoConfig { pub struct LoadCargoConfig {
@ -51,7 +50,6 @@ pub fn load_workspace_at(
load_workspace(workspace, &cargo_config.extra_env, load_config) load_workspace(workspace, &cargo_config.extra_env, load_config)
} }
#[instrument(skip_all)]
pub fn load_workspace( pub fn load_workspace(
ws: ProjectWorkspace, ws: ProjectWorkspace,
extra_env: &FxHashMap<String, String>, extra_env: &FxHashMap<String, String>,

View file

@ -276,6 +276,16 @@ impl ProjectJson {
self.manifest.as_ref() self.manifest.as_ref()
} }
pub fn crate_by_buildfile(&self, path: &AbsPath) -> Option<Build> {
// 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. /// Returns the path to the project's manifest or root folder, if no manifest exists.
pub fn manifest_or_root(&self) -> &AbsPath { pub fn manifest_or_root(&self) -> &AbsPath {
self.manifest.as_ref().map_or(&self.project_root, |manifest| manifest.as_ref()) 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 { pub struct ProjectJsonData {
sysroot: Option<Utf8PathBuf>, sysroot: Option<Utf8PathBuf>,
sysroot_src: Option<Utf8PathBuf>, sysroot_src: Option<Utf8PathBuf>,
@ -295,7 +305,7 @@ pub struct ProjectJsonData {
runnables: Vec<RunnableData>, runnables: Vec<RunnableData>,
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
struct CrateData { struct CrateData {
display_name: Option<String>, display_name: Option<String>,
root_module: Utf8PathBuf, root_module: Utf8PathBuf,
@ -319,7 +329,7 @@ struct CrateData {
build: Option<BuildData>, build: Option<BuildData>,
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
#[serde(rename = "edition")] #[serde(rename = "edition")]
enum EditionData { enum EditionData {
#[serde(rename = "2015")] #[serde(rename = "2015")]
@ -332,7 +342,7 @@ enum EditionData {
Edition2024, Edition2024,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub struct BuildData { pub struct BuildData {
label: String, label: String,
build_file: Utf8PathBuf, build_file: Utf8PathBuf,
@ -419,7 +429,7 @@ pub(crate) struct Dep {
pub(crate) name: CrateName, pub(crate) name: CrateName,
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
struct CrateSource { struct CrateSource {
include_dirs: Vec<Utf8PathBuf>, include_dirs: Vec<Utf8PathBuf>,
exclude_dirs: Vec<Utf8PathBuf>, exclude_dirs: Vec<Utf8PathBuf>,

View file

@ -31,6 +31,7 @@ use crate::{
utf8_stdout, CargoConfig, CargoWorkspace, InvocationStrategy, ManifestPath, Package, utf8_stdout, CargoConfig, CargoWorkspace, InvocationStrategy, ManifestPath, Package,
ProjectJson, ProjectManifest, Sysroot, TargetData, TargetKind, WorkspaceBuildScripts, ProjectJson, ProjectManifest, Sysroot, TargetData, TargetKind, WorkspaceBuildScripts,
}; };
use tracing::{debug, error, info};
pub type FileLoader<'a> = &'a mut dyn for<'b> FnMut(&'b AbsPath) -> Option<FileId>; pub type FileLoader<'a> = &'a mut dyn for<'b> FnMut(&'b AbsPath) -> Option<FileId>;
@ -250,7 +251,7 @@ impl ProjectWorkspace {
}; };
let rustc = rustc_dir.and_then(|rustc_dir| { 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( match CargoWorkspace::fetch_metadata(
&rustc_dir, &rustc_dir,
cargo_toml.parent(), cargo_toml.parent(),
@ -767,9 +768,9 @@ impl ProjectWorkspace {
}; };
if matches!(sysroot.mode(), SysrootMode::Stitched(_)) && crate_graph.patch_cfg_if() { 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 { } 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) (crate_graph, proc_macros)
} }
@ -917,6 +918,11 @@ fn project_json_to_crate_graph(
CrateOrigin::Local { repo: None, name: None } 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 *is_proc_macro {
if let Some(path) = proc_macro_dylib_path.clone() { if let Some(path) = proc_macro_dylib_path.clone() {
let node = Ok(( let node = Ok((
@ -931,6 +937,7 @@ fn project_json_to_crate_graph(
) )
.collect(); .collect();
debug!(map = ?idx_to_crate_id);
for (from_idx, krate) in project.crates() { for (from_idx, krate) in project.crates() {
if let Some(&from) = idx_to_crate_id.get(&from_idx) { if let Some(&from) = idx_to_crate_id.get(&from_idx) {
public_deps.add_to_crate_graph(crate_graph, from); 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) { let file_id = match load(detached_file) {
Some(file_id) => file_id, Some(file_id) => file_id,
None => { None => {
tracing::error!("Failed to load detached file {:?}", detached_file); error!("Failed to load detached file {:?}", detached_file);
return (crate_graph, FxHashMap::default()); return (crate_graph, FxHashMap::default());
} }
}; };
@ -1351,7 +1358,7 @@ fn add_target_crate_root(
crate_id crate_id
} }
#[derive(Default)] #[derive(Default, Debug)]
struct SysrootPublicDeps { struct SysrootPublicDeps {
deps: Vec<(CrateName, CrateId, bool)>, deps: Vec<(CrateName, CrateId, bool)>,
} }

View file

@ -175,6 +175,7 @@ fn run_server() -> anyhow::Result<()> {
return Err(e.into()); return Err(e.into());
} }
}; };
tracing::info!("InitializeParams: {}", initialize_params); tracing::info!("InitializeParams: {}", initialize_params);
let lsp_types::InitializeParams { let lsp_types::InitializeParams {
root_uri, root_uri,
@ -264,7 +265,10 @@ fn run_server() -> anyhow::Result<()> {
return Err(e.into()); 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(); config.rediscover_workspaces();
} }

View file

@ -328,6 +328,102 @@ config_data! {
/// `textDocument/rangeFormatting` request. The rustfmt option is unstable and only /// `textDocument/rangeFormatting` request. The rustfmt option is unstable and only
/// available on a nightly build. /// available on a nightly build.
rustfmt_rangeFormatting_enable: bool = false, 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<String> },
/// 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<DiscoverWorkspaceConfig> = None,
} }
} }
@ -924,6 +1020,21 @@ impl Config {
); );
(config, e, should_update) (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)] #[derive(Default, Debug)]
@ -988,6 +1099,14 @@ impl From<ProjectJson> for LinkedProject {
} }
} }
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DiscoverWorkspaceConfig {
pub command: Vec<String>,
pub progress_label: String,
pub files_to_watch: Vec<String>,
}
pub struct CallInfoConfig { pub struct CallInfoConfig {
pub params_only: bool, pub params_only: bool,
pub docs: bool, pub docs: bool,
@ -1528,15 +1647,27 @@ impl Config {
pub fn has_linked_projects(&self) -> bool { pub fn has_linked_projects(&self) -> bool {
!self.linkedProjects().is_empty() !self.linkedProjects().is_empty()
} }
pub fn linked_manifests(&self) -> impl Iterator<Item = &Utf8Path> + '_ {
pub fn linked_manifests(&self) -> impl Iterator<Item = &AbsPath> + '_ {
self.linkedProjects().iter().filter_map(|it| match it { self.linkedProjects().iter().filter_map(|it| match it {
ManifestOrProjectJson::Manifest(p) => Some(&**p), 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 { 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<LinkedProject> { pub fn linked_or_discovered_projects(&self) -> Vec<LinkedProject> {
match self.linkedProjects().as_slice() { match self.linkedProjects().as_slice() {
[] => { [] => {
@ -1561,6 +1692,12 @@ impl Config {
.ok() .ok()
.map(Into::into) .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) => { ManifestOrProjectJson::ProjectJson(it) => {
Some(ProjectJson::new(None, &self.root_path, it.clone()).into()) 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)] #[serde(untagged)]
enum ManifestOrProjectJson { enum ManifestOrProjectJson {
Manifest(Utf8PathBuf), Manifest(
#[serde(serialize_with = "serialize_abs_pathbuf")]
#[serde(deserialize_with = "deserialize_abs_pathbuf")]
AbsPathBuf,
),
ProjectJson(ProjectJsonData), 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<AbsPathBuf, D::Error>
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<S>(path: &AbsPathBuf, se: S) -> Result<S::Ok, S::Error>
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)] #[derive(Serialize, Deserialize, Debug, Clone)]
@ -3084,6 +3259,29 @@ fn field_props(field: &str, ty: &str, doc: &[&str], default: &str) -> serde_json
}, },
], ],
}, },
"Option<DiscoverWorkspaceConfig>" => 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})"), _ => panic!("missing entry for {ty}: {default} (field {field})"),
} }

View file

@ -6,7 +6,7 @@
use std::{ops::Not as _, time::Instant}; use std::{ops::Not as _, time::Instant};
use crossbeam_channel::{unbounded, Receiver, Sender}; use crossbeam_channel::{unbounded, Receiver, Sender};
use flycheck::FlycheckHandle; use flycheck::{project_json, FlycheckHandle};
use hir::ChangeWithProcMacros; use hir::ChangeWithProcMacros;
use ide::{Analysis, AnalysisHost, Cancellable, FileId, SourceRootId}; use ide::{Analysis, AnalysisHost, Cancellable, FileId, SourceRootId};
use ide_db::base_db::{CrateId, ProcMacroPaths, SourceDatabaseExt}; use ide_db::base_db::{CrateId, ProcMacroPaths, SourceDatabaseExt};
@ -20,9 +20,9 @@ use parking_lot::{
use proc_macro_api::ProcMacroServer; use proc_macro_api::ProcMacroServer;
use project_model::{ManifestPath, ProjectWorkspace, ProjectWorkspaceKind, WorkspaceBuildScripts}; use project_model::{ManifestPath, ProjectWorkspace, ProjectWorkspaceKind, WorkspaceBuildScripts};
use rustc_hash::{FxHashMap, FxHashSet}; use rustc_hash::{FxHashMap, FxHashSet};
use tracing::{span, Level}; use tracing::{span, trace, Level};
use triomphe::Arc; use triomphe::Arc;
use vfs::{AnchoredPathBuf, ChangeKind, Vfs}; use vfs::{AbsPathBuf, AnchoredPathBuf, ChangeKind, Vfs};
use crate::{ use crate::{
config::{Config, ConfigChange, ConfigErrors}, config::{Config, ConfigChange, ConfigErrors},
@ -41,6 +41,11 @@ use crate::{
task_pool::{TaskPool, TaskQueue}, task_pool::{TaskPool, TaskQueue},
}; };
pub(crate) struct FetchWorkspaceRequest {
pub(crate) path: Option<AbsPathBuf>,
pub(crate) force_crate_graph_reload: bool,
}
// Enforces drop order // Enforces drop order
pub(crate) struct Handle<H, C> { pub(crate) struct Handle<H, C> {
pub(crate) handle: H, pub(crate) handle: H,
@ -95,6 +100,11 @@ pub(crate) struct GlobalState {
pub(crate) test_run_receiver: Receiver<flycheck::CargoTestMessage>, pub(crate) test_run_receiver: Receiver<flycheck::CargoTestMessage>,
pub(crate) test_run_remaining_jobs: usize, pub(crate) test_run_remaining_jobs: usize,
// Project loading
pub(crate) discover_handle: Option<project_json::DiscoverHandle>,
pub(crate) discover_sender: Sender<project_json::DiscoverProjectMessage>,
pub(crate) discover_receiver: Receiver<project_json::DiscoverProjectMessage>,
// VFS // VFS
pub(crate) loader: Handle<Box<dyn vfs::loader::Handle>, Receiver<vfs::loader::Message>>, pub(crate) loader: Handle<Box<dyn vfs::loader::Handle>, Receiver<vfs::loader::Message>>,
pub(crate) vfs: Arc<RwLock<(vfs::Vfs, IntMap<FileId, LineEndings>)>>, pub(crate) vfs: Arc<RwLock<(vfs::Vfs, IntMap<FileId, LineEndings>)>>,
@ -134,11 +144,12 @@ pub(crate) struct GlobalState {
// op queues // op queues
pub(crate) fetch_workspaces_queue: pub(crate) fetch_workspaces_queue:
OpQueue<bool, Option<(Vec<anyhow::Result<ProjectWorkspace>>, bool)>>, OpQueue<FetchWorkspaceRequest, Option<(Vec<anyhow::Result<ProjectWorkspace>>, bool)>>,
pub(crate) fetch_build_data_queue: pub(crate) fetch_build_data_queue:
OpQueue<(), (Arc<Vec<ProjectWorkspace>>, Vec<anyhow::Result<WorkspaceBuildScripts>>)>, OpQueue<(), (Arc<Vec<ProjectWorkspace>>, Vec<anyhow::Result<WorkspaceBuildScripts>>)>,
pub(crate) fetch_proc_macros_queue: OpQueue<Vec<ProcMacroPaths>, bool>, pub(crate) fetch_proc_macros_queue: OpQueue<Vec<ProcMacroPaths>, bool>,
pub(crate) prime_caches_queue: OpQueue, pub(crate) prime_caches_queue: OpQueue,
pub(crate) discover_workspace_queue: OpQueue,
/// A deferred task queue. /// A deferred task queue.
/// ///
@ -146,7 +157,7 @@ pub(crate) struct GlobalState {
/// handlers, as accessing the database may block latency-sensitive /// handlers, as accessing the database may block latency-sensitive
/// interactions and should be moved away from the main thread. /// 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 /// this queue should run only *after* [`GlobalState::process_changes`] has
/// been called. /// been called.
pub(crate) deferred_task_queue: TaskQueue, pub(crate) deferred_task_queue: TaskQueue,
@ -202,6 +213,9 @@ impl GlobalState {
} }
let (flycheck_sender, flycheck_receiver) = unbounded(); let (flycheck_sender, flycheck_receiver) = unbounded();
let (test_run_sender, test_run_receiver) = unbounded(); let (test_run_sender, test_run_receiver) = unbounded();
let (discover_sender, discover_receiver) = unbounded();
let mut this = GlobalState { let mut this = GlobalState {
sender, sender,
req_queue: ReqQueue::default(), req_queue: ReqQueue::default(),
@ -233,6 +247,10 @@ impl GlobalState {
test_run_receiver, test_run_receiver,
test_run_remaining_jobs: 0, test_run_remaining_jobs: 0,
discover_handle: None,
discover_sender,
discover_receiver,
vfs: Arc::new(RwLock::new((vfs::Vfs::default(), IntMap::default()))), vfs: Arc::new(RwLock::new((vfs::Vfs::default(), IntMap::default()))),
vfs_config_version: 0, vfs_config_version: 0,
vfs_progress_config_version: 0, vfs_progress_config_version: 0,
@ -247,6 +265,7 @@ impl GlobalState {
fetch_proc_macros_queue: OpQueue::default(), fetch_proc_macros_queue: OpQueue::default(),
prime_caches_queue: OpQueue::default(), prime_caches_queue: OpQueue::default(),
discover_workspace_queue: OpQueue::default(),
deferred_task_queue: task_queue, deferred_task_queue: task_queue,
}; };
@ -296,11 +315,24 @@ impl GlobalState {
modified_rust_files.push(file.file_id); 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::<Vec<&str>>()
})
.unwrap_or_default();
let path = path.to_path_buf(); let path = path.to_path_buf();
if file.is_created_or_deleted() { if file.is_created_or_deleted() {
workspace_structure_change.get_or_insert((path, false)).1 |= workspace_structure_change.get_or_insert((path, false)).1 |=
self.crate_graph_file_dependencies.contains(vfs_path); 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)); workspace_structure_change.get_or_insert((path.clone(), false));
} }
} }
@ -419,7 +451,7 @@ impl GlobalState {
self.fetch_workspaces_queue.request_op( self.fetch_workspaces_queue.request_op(
format!("workspace vfs file change: {path}"), format!("workspace vfs file change: {path}"),
force_crate_graph_reload, FetchWorkspaceRequest { path: Some(path.to_owned()), force_crate_graph_reload },
); );
} }
} }

View file

@ -14,7 +14,7 @@ use vfs::{AbsPathBuf, ChangeKind, VfsPath};
use crate::{ use crate::{
config::{Config, ConfigChange}, config::{Config, ConfigChange},
global_state::GlobalState, global_state::{FetchWorkspaceRequest, GlobalState},
lsp::{from_proto, utils::apply_document_changes}, lsp::{from_proto, utils::apply_document_changes},
lsp_ext::{self, RunFlycheckParams}, lsp_ext::{self, RunFlycheckParams},
mem_docs::DocumentData, 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); 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())); 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"); tracing::debug!("queuing task");
let _ = state let _ = state
.deferred_task_queue .deferred_task_queue
@ -150,15 +152,29 @@ pub(crate) fn handle_did_save_text_document(
if let Ok(vfs_path) = from_proto::vfs_path(&params.text_document.uri) { if let Ok(vfs_path) = from_proto::vfs_path(&params.text_document.uri) {
// Re-fetch workspaces if a workspace related file has changed // Re-fetch workspaces if a workspace related file has changed
if let Some(abs_path) = vfs_path.as_path() { if let Some(path) = vfs_path.as_path() {
if reload::should_refresh_for_change(abs_path, ChangeKind::Modify) { let additional_files = &state
state .config
.fetch_workspaces_queue .discover_workspace_config()
.request_op(format!("workspace vfs file change saved {abs_path}"), false); .map(|cfg| cfg.files_to_watch.iter().map(String::as_str).collect::<Vec<&str>>())
} else if state.detached_files.contains(abs_path) { .unwrap_or_default();
state
.fetch_workspaces_queue if reload::should_refresh_for_change(path, ChangeKind::Modify, additional_files) {
.request_op(format!("detached file saved {abs_path}"), false); 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() { if !config.has_linked_projects() && config.detached_files().is_empty() {
config.rediscover_workspaces(); 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(()) Ok(())

View file

@ -37,7 +37,7 @@ use vfs::{AbsPath, AbsPathBuf, FileId, VfsPath};
use crate::{ use crate::{
config::{Config, RustfmtConfig, WorkspaceSymbolConfig}, config::{Config, RustfmtConfig, WorkspaceSymbolConfig},
diff::diff, diff::diff,
global_state::{GlobalState, GlobalStateSnapshot}, global_state::{FetchWorkspaceRequest, GlobalState, GlobalStateSnapshot},
hack_recover_crate_name, hack_recover_crate_name,
line_index::LineEndings, line_index::LineEndings,
lsp::{ lsp::{
@ -57,7 +57,8 @@ pub(crate) fn handle_workspace_reload(state: &mut GlobalState, _: ()) -> anyhow:
state.proc_macro_clients = Arc::from_iter([]); state.proc_macro_clients = Arc::from_iter([]);
state.build_deps_changed = false; 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(()) Ok(())
} }

View file

@ -531,7 +531,7 @@ pub struct ServerStatusParams {
pub message: Option<String>, pub message: Option<String>,
} }
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] #[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub enum Health { pub enum Health {
Ok, Ok,

View file

@ -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. /// 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. /// 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<String>) { pub(crate) fn show_and_log_error(&mut self, message: String, additional_info: Option<String>) {

View file

@ -9,18 +9,19 @@ use std::{
use always_assert::always; use always_assert::always;
use crossbeam_channel::{select, Receiver}; use crossbeam_channel::{select, Receiver};
use flycheck::project_json;
use ide_db::base_db::{SourceDatabase, SourceDatabaseExt, VfsPath}; use ide_db::base_db::{SourceDatabase, SourceDatabaseExt, VfsPath};
use lsp_server::{Connection, Notification, Request}; use lsp_server::{Connection, Notification, Request};
use lsp_types::{notification::Notification as _, TextDocumentIdentifier}; use lsp_types::{notification::Notification as _, TextDocumentIdentifier};
use stdx::thread::ThreadIntent; use stdx::thread::ThreadIntent;
use tracing::{span, Level}; use tracing::{span, Level};
use vfs::FileId; use vfs::{AbsPathBuf, FileId};
use crate::{ use crate::{
config::Config, config::Config,
diagnostics::{fetch_native_diagnostics, DiagnosticsGeneration}, diagnostics::{fetch_native_diagnostics, DiagnosticsGeneration},
dispatch::{NotificationDispatcher, RequestDispatcher}, 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, hack_recover_crate_name,
lsp::{ lsp::{
from_proto, to_proto, from_proto, to_proto,
@ -62,6 +63,7 @@ enum Event {
Vfs(vfs::loader::Message), Vfs(vfs::loader::Message),
Flycheck(flycheck::Message), Flycheck(flycheck::Message),
TestResult(flycheck::CargoTestMessage), TestResult(flycheck::CargoTestMessage),
DiscoverProject(project_json::DiscoverProjectMessage),
} }
impl fmt::Display for Event { impl fmt::Display for Event {
@ -73,6 +75,7 @@ impl fmt::Display for Event {
Event::Flycheck(_) => write!(f, "Event::Flycheck"), Event::Flycheck(_) => write!(f, "Event::Flycheck"),
Event::QueuedTask(_) => write!(f, "Event::QueuedTask"), Event::QueuedTask(_) => write!(f, "Event::QueuedTask"),
Event::TestResult(_) => write!(f, "Event::TestResult"), Event::TestResult(_) => write!(f, "Event::TestResult"),
Event::DiscoverProject(_) => write!(f, "Event::DiscoverProject"),
} }
} }
} }
@ -86,6 +89,7 @@ pub(crate) enum QueuedTask {
#[derive(Debug)] #[derive(Debug)]
pub(crate) enum Task { pub(crate) enum Task {
Response(lsp_server::Response), Response(lsp_server::Response),
DiscoverLinkedProjects(DiscoverProjectParam),
ClientNotification(lsp_ext::UnindexedProjectParams), ClientNotification(lsp_ext::UnindexedProjectParams),
Retry(lsp_server::Request), Retry(lsp_server::Request),
Diagnostics(DiagnosticsGeneration, Vec<(FileId, Vec<lsp_types::Diagnostic>)>), Diagnostics(DiagnosticsGeneration, Vec<(FileId, Vec<lsp_types::Diagnostic>)>),
@ -97,6 +101,12 @@ pub(crate) enum Task {
BuildDepsHaveChanged, BuildDepsHaveChanged,
} }
#[derive(Debug)]
pub(crate) enum DiscoverProjectParam {
Buildfile(AbsPathBuf),
Path(AbsPathBuf),
}
#[derive(Debug)] #[derive(Debug)]
pub(crate) enum PrimeCachesProgress { pub(crate) enum PrimeCachesProgress {
Begin, Begin,
@ -134,6 +144,7 @@ impl fmt::Debug for Event {
Event::Vfs(it) => fmt::Debug::fmt(it, f), Event::Vfs(it) => fmt::Debug::fmt(it, f),
Event::Flycheck(it) => fmt::Debug::fmt(it, f), Event::Flycheck(it) => fmt::Debug::fmt(it, f),
Event::TestResult(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(); self.update_status_or_notify();
if self.config.did_save_text_document_dynamic_registration() { 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 self.config.discover_workspace_config().is_none() {
if let Some((cause, force_crate_graph_reload)) = let req = FetchWorkspaceRequest { path: None, force_crate_graph_reload: false };
self.fetch_workspaces_queue.should_start_op() self.fetch_workspaces_queue.request_op("startup".to_owned(), req);
{ if let Some((cause, FetchWorkspaceRequest { path, force_crate_graph_reload })) =
self.fetch_workspaces(cause, 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) { while let Some(event) = self.next_event(&inbox) {
@ -167,32 +188,36 @@ impl GlobalState {
anyhow::bail!("client exited without proper shutdown sequence") 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<Item = String>) {
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 { let save_registration_options = lsp_types::TextDocumentSaveRegistrationOptions {
include_text: Some(false), include_text: Some(false),
text_document_registration_options: lsp_types::TextDocumentRegistrationOptions { text_document_registration_options: lsp_types::TextDocumentRegistrationOptions {
document_selector: Some(vec![ document_selector: Some(selectors),
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()),
},
]),
}, },
}; };
@ -230,6 +255,8 @@ impl GlobalState {
recv(self.test_run_receiver) -> task => recv(self.test_run_receiver) -> task =>
Some(Event::TestResult(task.unwrap())), 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); 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(); let event_handling_duration = loop_start.elapsed();
@ -427,11 +461,13 @@ impl GlobalState {
} }
} }
if self.config.cargo_autoreload_config() { if self.config.cargo_autoreload_config()
if let Some((cause, force_crate_graph_reload)) = || 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_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); 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) => { Task::FetchBuildData(progress) => {
let (state, msg) = match progress { let (state, msg) = match progress {
BuildDataProgress::Begin => (Some(Progress::Begin), None), 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"); let id = from_proto::file_id(&snap, &uri).expect("unable to get FileId");
if let Ok(crates) = &snap.analysis.crates_for(id) { if let Ok(crates) = &snap.analysis.crates_for(id) {
if crates.is_empty() { if crates.is_empty() {
let params = lsp_ext::UnindexedProjectParams { if snap.config.discover_workspace_config().is_some() {
text_documents: vec![lsp_types::TextDocumentIdentifier { uri }], 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 { } else {
tracing::debug!(?uri, "is indexed"); 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) { fn handle_cargo_test_msg(&mut self, message: flycheck::CargoTestMessage) {
match message { match message {
flycheck::CargoTestMessage::Test { name, state } => { flycheck::CargoTestMessage::Test { name, state } => {

View file

@ -3,6 +3,7 @@
pub(crate) type Cause = String; pub(crate) type Cause = String;
#[derive(Debug)]
pub(crate) struct OpQueue<Args = (), Output = ()> { pub(crate) struct OpQueue<Args = (), Output = ()> {
op_requested: Option<(Cause, Args)>, op_requested: Option<(Cause, Args)>,
op_in_progress: bool, op_in_progress: bool,

View file

@ -33,11 +33,12 @@ use vfs::{AbsPath, AbsPathBuf, ChangeKind};
use crate::{ use crate::{
config::{Config, FilesWatcher, LinkedProject}, config::{Config, FilesWatcher, LinkedProject},
global_state::GlobalState, global_state::{FetchWorkspaceRequest, GlobalState},
lsp_ext, lsp_ext,
main_loop::Task, main_loop::{DiscoverProjectParam, Task},
op_queue::Cause, op_queue::Cause,
}; };
use tracing::{debug, info};
#[derive(Debug)] #[derive(Debug)]
pub(crate) enum ProjectWorkspaceProgress { pub(crate) enum ProjectWorkspaceProgress {
@ -66,6 +67,7 @@ impl GlobalState {
|| self.fetch_workspaces_queue.op_in_progress() || self.fetch_workspaces_queue.op_in_progress()
|| self.fetch_build_data_queue.op_in_progress() || self.fetch_build_data_queue.op_in_progress()
|| self.fetch_proc_macros_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_config_version < self.vfs_config_version
|| self.vfs_progress_n_done < self.vfs_progress_n_total) || 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(), &self.config.lru_query_capacities_config().cloned().unwrap_or_default(),
); );
} }
if self.config.linked_or_discovered_projects() != old_config.linked_or_discovered_projects() 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() { } else if self.config.flycheck() != old_config.flycheck() {
self.reload_flycheck(); self.reload_flycheck();
} }
@ -109,6 +113,7 @@ impl GlobalState {
if !self.config.cargo_autoreload() if !self.config.cargo_autoreload()
&& self.is_quiescent() && self.is_quiescent()
&& self.fetch_workspaces_queue.op_requested() && self.fetch_workspaces_queue.op_requested()
&& self.config.discover_workspace_config().is_none()
{ {
status.health |= lsp_ext::Health::Warning; 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"); 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; status.health |= lsp_ext::Health::Warning;
message.push_str("Failed to run build scripts of some packages.\n\n"); message.push_str("Failed to run build scripts of some packages.\n\n");
} }
if let Some(err) = &self.config_errors { if let Some(err) = &self.config_errors {
status.health |= lsp_ext::Health::Warning; status.health |= lsp_ext::Health::Warning;
format_to!(message, "{err}\n"); format_to!(message, "{err}\n");
@ -217,8 +221,13 @@ impl GlobalState {
status status
} }
pub(crate) fn fetch_workspaces(&mut self, cause: Cause, force_crate_graph_reload: bool) { pub(crate) fn fetch_workspaces(
tracing::info!(%cause, "will fetch workspaces"); &mut self,
cause: Cause,
path: Option<AbsPathBuf>,
force_crate_graph_reload: bool,
) {
info!(%cause, "will fetch workspaces");
self.task_pool.handle.spawn_with_sender(ThreadIntent::Worker, { self.task_pool.handle.spawn_with_sender(ThreadIntent::Worker, {
let linked_projects = self.config.linked_or_discovered_projects(); let linked_projects = self.config.linked_or_discovered_projects();
@ -231,6 +240,10 @@ impl GlobalState {
.filter_map(Result::ok) .filter_map(Result::ok)
.collect(); .collect();
let cargo_config = self.config.cargo(); 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| { move |sender| {
let progress = { let progress = {
@ -244,10 +257,28 @@ impl GlobalState {
sender.send(Task::FetchWorkspace(ProjectWorkspaceProgress::Begin)).unwrap(); 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 let mut workspaces = linked_projects
.iter() .iter()
.map(|project| match project { .map(|project| match project {
LinkedProject::ProjectManifest(manifest) => { LinkedProject::ProjectManifest(manifest) => {
debug!(path = %manifest, "loading project from manifest");
project_model::ProjectWorkspace::load( project_model::ProjectWorkspace::load(
manifest.clone(), manifest.clone(),
&cargo_config, &cargo_config,
@ -255,12 +286,13 @@ impl GlobalState {
) )
} }
LinkedProject::InlineJsonProject(it) => { LinkedProject::InlineJsonProject(it) => {
Ok(project_model::ProjectWorkspace::load_inline( let workspace = project_model::ProjectWorkspace::load_inline(
it.clone(), it.clone(),
cargo_config.target.as_deref(), cargo_config.target.as_deref(),
&cargo_config.extra_env, &cargo_config.extra_env,
&cargo_config.cfg_overrides, &cargo_config.cfg_overrides,
)) );
Ok(workspace)
} }
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -286,7 +318,7 @@ impl GlobalState {
)); ));
} }
tracing::info!("did fetch workspaces {:?}", workspaces); info!(?workspaces, "did fetch workspaces");
sender sender
.send(Task::FetchWorkspace(ProjectWorkspaceProgress::End( .send(Task::FetchWorkspace(ProjectWorkspaceProgress::End(
workspaces, workspaces,
@ -298,7 +330,7 @@ impl GlobalState {
} }
pub(crate) fn fetch_build_data(&mut self, cause: Cause) { 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 workspaces = Arc::clone(&self.workspaces);
let config = self.config.cargo(); let config = self.config.cargo();
let root_path = self.config.root_path().clone(); 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<ProcMacroPaths>) { pub(crate) fn fetch_proc_macros(&mut self, cause: Cause, paths: Vec<ProcMacroPaths>) {
tracing::info!(%cause, "will load proc macros"); info!(%cause, "will load proc macros");
let ignored_proc_macros = self.config.ignored_proc_macros().clone(); let ignored_proc_macros = self.config.ignored_proc_macros().clone();
let proc_macro_clients = self.proc_macro_clients.clone(); let proc_macro_clients = self.proc_macro_clients.clone();
@ -395,6 +427,7 @@ impl GlobalState {
return; return;
}; };
info!(%cause, ?force_reload_crate_graph);
if self.fetch_workspace_error().is_err() && !self.workspaces.is_empty() { if self.fetch_workspace_error().is_err() && !self.workspaces.is_empty() {
if *force_reload_crate_graph { if *force_reload_crate_graph {
self.recreate_crate_graph(cause); self.recreate_crate_graph(cause);
@ -416,7 +449,7 @@ impl GlobalState {
if same_workspaces { if same_workspaces {
let (workspaces, build_scripts) = self.fetch_build_data_queue.last_op_result(); let (workspaces, build_scripts) = self.fetch_build_data_queue.last_op_result();
if Arc::ptr_eq(workspaces, &self.workspaces) { if Arc::ptr_eq(workspaces, &self.workspaces) {
tracing::debug!("set build scripts to workspaces"); info!("set build scripts to workspaces");
let workspaces = workspaces let workspaces = workspaces
.iter() .iter()
@ -428,9 +461,10 @@ impl GlobalState {
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
// Workspaces are the same, but we've updated build data. // Workspaces are the same, but we've updated build data.
info!("same workspace, but new build data");
self.workspaces = Arc::new(workspaces); self.workspaces = Arc::new(workspaces);
} else { } 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 { if *force_reload_crate_graph {
self.recreate_crate_graph(cause); self.recreate_crate_graph(cause);
} }
@ -440,7 +474,7 @@ impl GlobalState {
return; return;
} }
} else { } else {
tracing::debug!("abandon build scripts for workspaces"); info!("abandon build scripts for workspaces");
// Here, we completely changed the workspace (Cargo.toml edit), so // Here, we completely changed the workspace (Cargo.toml edit), so
// we don't care about build-script results, they are stale. // 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) if (self.proc_macro_clients.is_empty() || !same_workspaces)
&& self.config.expand_proc_macros() && 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| { self.proc_macro_clients = Arc::from_iter(self.workspaces.iter().map(|ws| {
let path = match self.config.proc_macro_srv() { let path = match self.config.proc_macro_srv() {
@ -562,7 +596,7 @@ impl GlobalState {
_ => Default::default(), _ => Default::default(),
}; };
tracing::info!("Using proc-macro server at {path}"); info!("Using proc-macro server at {path}");
ProcMacroServer::spawn(&path, &env).map_err(|err| { ProcMacroServer::spawn(&path, &env).map_err(|err| {
tracing::error!( tracing::error!(
@ -588,12 +622,14 @@ impl GlobalState {
self.source_root_config = project_folders.source_root_config; self.source_root_config = project_folders.source_root_config;
self.local_roots_parent_map = Arc::new(self.source_root_config.source_root_parent_map()); 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); self.recreate_crate_graph(cause);
tracing::info!("did switch workspaces"); info!("did switch workspaces");
} }
fn recreate_crate_graph(&mut self, cause: String) { fn recreate_crate_graph(&mut self, cause: String) {
info!(?cause, "Building Crate Graph");
self.report_progress( self.report_progress(
"Building CrateGraph", "Building CrateGraph",
crate::lsp::utils::Progress::Begin, crate::lsp::utils::Progress::Begin,
@ -658,12 +694,19 @@ impl GlobalState {
let Some((last_op_result, _)) = self.fetch_workspaces_queue.last_op_result() else { let Some((last_op_result, _)) = self.fetch_workspaces_queue.last_op_result() else {
return Ok(()); return Ok(());
}; };
if last_op_result.is_empty() {
stdx::format_to!(buf, "rust-analyzer failed to discover workspace"); if !self.discover_workspace_queue.op_in_progress() {
} else { if last_op_result.is_empty() {
for ws in last_op_result { stdx::format_to!(buf, "rust-analyzer failed to discover workspace");
if let Err(err) = ws { } else {
stdx::format_to!(buf, "rust-analyzer failed to load workspace: {:#}\n", err); 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) (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_FILES: &[&str] = &["build.rs", "src/main.rs", "src/lib.rs"];
const IMPLICIT_TARGET_DIRS: &[&str] = &["src/bin", "examples", "tests", "benches"]; 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 { if let "Cargo.toml" | "Cargo.lock" = file_name {
return true; return true;
} }
if additional_paths.contains(&file_name) {
return true;
}
if change_kind == ChangeKind::Modify { if change_kind == ChangeKind::Modify {
return false; return false;
} }

View file

@ -48,7 +48,10 @@ where
let writer = self.writer; 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; let mut chalk_layer = None;
if let Some(chalk_filter) = self.chalk_filter { if let Some(chalk_filter) = self.chalk_filter {

View file

@ -27,7 +27,6 @@ use lsp_types::{
InlayHint, InlayHintLabel, InlayHintParams, PartialResultParams, Position, Range, InlayHint, InlayHintLabel, InlayHintParams, PartialResultParams, Position, Range,
RenameFilesParams, TextDocumentItem, TextDocumentPositionParams, WorkDoneProgressParams, RenameFilesParams, TextDocumentItem, TextDocumentPositionParams, WorkDoneProgressParams,
}; };
use rust_analyzer::lsp::ext::{OnEnter, Runnables, RunnablesParams, UnindexedProject}; use rust_analyzer::lsp::ext::{OnEnter, Runnables, RunnablesParams, UnindexedProject};
use serde_json::json; use serde_json::json;
use stdx::format_to_acc; use stdx::format_to_acc;

View file

@ -1,5 +1,5 @@
<!--- <!---
lsp/ext.rs hash: 39b47906286ad9c lsp/ext.rs hash: 278250dba58cd879
If you need to change the above hash to make the test pass, please check if you If you need to change the above hash to make the test pass, please check if you
need to adjust this doc as well and ping this issue: need to adjust this doc as well and ping this issue:

View file

@ -1015,6 +1015,104 @@ Show documentation.
-- --
Whether to insert closing angle brackets when typing an opening angle bracket of a generic argument list. Whether to insert closing angle brackets when typing an opening angle bracket of a generic argument list.
-- --
[[rust-analyzer.workspace.discoverConfig]]rust-analyzer.workspace.discoverConfig (default: `null`)::
+
--
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<String> },
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.
--
[[rust-analyzer.workspace.symbol.search.kind]]rust-analyzer.workspace.symbol.search.kind (default: `"only_types"`):: [[rust-analyzer.workspace.symbol.search.kind]]rust-analyzer.workspace.symbol.search.kind (default: `"only_types"`)::
+ +
-- --

View file

@ -2595,6 +2595,40 @@
} }
} }
}, },
{
"title": "workspace",
"properties": {
"rust-analyzer.workspace.discoverConfig": {
"markdownDescription": "Enables automatic discovery of projects using [`DiscoverWorkspaceConfig::command`].\n\n[`DiscoverWorkspaceConfig`] also requires setting `progress_label` and `files_to_watch`.\n`progress_label` is used for the title in progress indicators, whereas `files_to_watch`\nis used to determine which build system-specific files should be watched in order to\nreload rust-analyzer.\n\nBelow is an example of a valid configuration:\n```json\n\"rust-analyzer.workspace.discoverConfig\": {\n \"command\": [\n \"rust-project\",\n \"develop-json\",\n {arg}\n ],\n \"progressLabel\": \"rust-analyzer\",\n \"filesToWatch\": [\n \"BUCK\",\n ],\n}\n```\n\n## On `DiscoverWorkspaceConfig::command`\n\n**Warning**: This format is provisional and subject to change.\n\n[`DiscoverWorkspaceConfig::command`] *must* return a JSON object\ncorresponding to `DiscoverProjectData::Finished`:\n\n```norun\n#[derive(Debug, Clone, Deserialize, Serialize)]\n#[serde(tag = \"kind\")]\n#[serde(rename_all = \"snake_case\")]\nenum DiscoverProjectData {\n Finished { buildfile: Utf8PathBuf, project: ProjectJsonData },\n Error { error: String, source: Option<String> },\n Progress { message: String },\n}\n```\n\nAs JSON, `DiscoverProjectData::Finished` is:\n\n```json\n{\n // the internally-tagged representation of the enum.\n \"kind\": \"finished\",\n // the file used by a non-Cargo build system to define\n // a package or target.\n \"buildfile\": \"rust-analyzer/BUILD\",\n // the contents of a rust-project.json, elided for brevity\n \"project\": {\n \"sysroot\": \"foo\",\n \"crates\": []\n }\n}\n```\n\nIt is encouraged, but not required, to use the other variants on\n`DiscoverProjectData` to provide a more polished end-user experience.\n\n`DiscoverWorkspaceConfig::command` may *optionally* include an `{arg}`,\nwhich will be substituted with the JSON-serialized form of the following\nenum:\n\n```norun\n#[derive(PartialEq, Clone, Debug, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub enum DiscoverArgument {\n Path(AbsPathBuf),\n Buildfile(AbsPathBuf),\n}\n```\n\nThe JSON representation of `DiscoverArgument::Path` is:\n\n```json\n{\n \"path\": \"src/main.rs\"\n}\n```\n\nSimilarly, the JSON representation of `DiscoverArgument::Buildfile` is:\n\n```\n{\n \"buildfile\": \"BUILD\"\n}\n```\n\n`DiscoverArgument::Path` is used to find and generate a `rust-project.json`,\nand therefore, a workspace, whereas `DiscoverArgument::buildfile` is used to\nto update an existing workspace. As a reference for implementors,\nbuck2's `rust-project` will likely be useful:\nhttps://github.com/facebook/buck2/tree/main/integrations/rust-project.",
"default": null,
"anyOf": [
{
"type": "null"
},
{
"type": "object",
"properties": {
"command": {
"type": "array",
"items": {
"type": "string"
}
},
"progressLabel": {
"type": "string"
},
"filesToWatch": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
]
}
}
},
{ {
"title": "workspace", "title": "workspace",
"properties": { "properties": {