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]]
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",
]

View file

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

View file

@ -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(

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 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<String, String>,

View file

@ -276,6 +276,16 @@ impl ProjectJson {
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.
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<Utf8PathBuf>,
sysroot_src: Option<Utf8PathBuf>,
@ -295,7 +305,7 @@ pub struct ProjectJsonData {
runnables: Vec<RunnableData>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
struct CrateData {
display_name: Option<String>,
root_module: Utf8PathBuf,
@ -319,7 +329,7 @@ struct CrateData {
build: Option<BuildData>,
}
#[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<Utf8PathBuf>,
exclude_dirs: Vec<Utf8PathBuf>,

View file

@ -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<FileId>;
@ -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)>,
}

View file

@ -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();
}

View file

@ -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<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)
}
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<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 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<Item = &Utf8Path> + '_ {
pub fn linked_manifests(&self) -> impl Iterator<Item = &AbsPath> + '_ {
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<LinkedProject> {
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<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)]
@ -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})"),
}

View file

@ -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<AbsPathBuf>,
pub(crate) force_crate_graph_reload: bool,
}
// Enforces drop order
pub(crate) struct Handle<H, C> {
pub(crate) handle: H,
@ -95,6 +100,11 @@ pub(crate) struct GlobalState {
pub(crate) test_run_receiver: Receiver<flycheck::CargoTestMessage>,
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
pub(crate) loader: Handle<Box<dyn vfs::loader::Handle>, Receiver<vfs::loader::Message>>,
pub(crate) vfs: Arc<RwLock<(vfs::Vfs, IntMap<FileId, LineEndings>)>>,
@ -134,11 +144,12 @@ pub(crate) struct GlobalState {
// op queues
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:
OpQueue<(), (Arc<Vec<ProjectWorkspace>>, Vec<anyhow::Result<WorkspaceBuildScripts>>)>,
pub(crate) fetch_proc_macros_queue: OpQueue<Vec<ProcMacroPaths>, 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::<Vec<&str>>()
})
.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 },
);
}
}

View file

@ -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(&params.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::<Vec<&str>>())
.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(())

View file

@ -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(())
}

View file

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

View file

@ -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<lsp_types::Diagnostic>)>),
@ -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<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 {
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 } => {

View file

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

View file

@ -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<AbsPathBuf>,
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::<Vec<_>>();
@ -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<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 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::<Vec<_>>();
// 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;
}

View file

@ -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 {

View file

@ -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;

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
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.
--
[[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"`)::
+
--

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",
"properties": {