diff --git a/Cargo.lock b/Cargo.lock index d5cebd5102de..aaedd6281a54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1606,6 +1606,7 @@ dependencies = [ "rayon", "rustc-dependencies", "rustc-hash", + "salsa", "scip", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index c382a5a37d23..e24ab0867236 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -100,6 +100,7 @@ nohash-hasher = "0.2.0" text-size = "1.1.0" serde = { version = "1.0.156", features = ["derive"] } serde_json = "1.0.96" +salsa = "0.17.0-pre.2" triomphe = { version = "0.1.8", default-features = false, features = ["std"] } # can't upgrade due to dashmap depending on 0.12.3 currently hashbrown = { version = "0.12.3", features = [ diff --git a/crates/base-db/Cargo.toml b/crates/base-db/Cargo.toml index 8435dba86d22..d6fd0047a22c 100644 --- a/crates/base-db/Cargo.toml +++ b/crates/base-db/Cargo.toml @@ -12,7 +12,7 @@ rust-version.workspace = true doctest = false [dependencies] -salsa = "0.17.0-pre.2" +salsa.workspace = true rustc-hash = "1.1.0" triomphe.workspace = true diff --git a/crates/paths/src/lib.rs b/crates/paths/src/lib.rs index 88b8d0aee3a4..55bd5376dfa8 100644 --- a/crates/paths/src/lib.rs +++ b/crates/paths/src/lib.rs @@ -136,7 +136,8 @@ impl AbsPath { /// # Panics /// /// Panics if `path` is not absolute. - pub fn assert(path: &Path) -> &AbsPath { + pub fn assert + ?Sized>(path: &P) -> &AbsPath { + let path = path.as_ref(); assert!(path.is_absolute()); unsafe { &*(path as *const Path as *const AbsPath) } } @@ -194,6 +195,10 @@ impl AbsPath { self.0.ends_with(&suffix.0) } + pub fn ancestors(&self) -> impl Iterator { + self.0.ancestors().map(AbsPath::assert) + } + pub fn name_and_extension(&self) -> Option<(&str, Option<&str>)> { Some(( self.file_stem()?.to_str()?, diff --git a/crates/rust-analyzer/Cargo.toml b/crates/rust-analyzer/Cargo.toml index 7d6326448d8c..009c03b645f7 100644 --- a/crates/rust-analyzer/Cargo.toml +++ b/crates/rust-analyzer/Cargo.toml @@ -75,6 +75,7 @@ toolchain.workspace = true vfs-notify.workspace = true vfs.workspace = true la-arena.workspace = true +salsa.workspace = true [target.'cfg(windows)'.dependencies] winapi = "0.3.9" diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index 3c65356bb721..5fce21ddce77 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -42,6 +42,7 @@ use crate::{ }; mod patch_old_style; +mod tree; // Conventions for configuration keys to preserve maximal extendability without breakage: // - Toggles (be it binary true/false or with more options in-between) should almost always suffix as `_enable` @@ -2194,7 +2195,7 @@ enum AdjustmentHintsDef { Reborrow, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(untagged)] enum DiscriminantHintsDef { #[serde(with = "true_or_always")] diff --git a/crates/rust-analyzer/src/config/tree.rs b/crates/rust-analyzer/src/config/tree.rs new file mode 100644 index 000000000000..ef2803f3d1d7 --- /dev/null +++ b/crates/rust-analyzer/src/config/tree.rs @@ -0,0 +1,685 @@ +use itertools::Itertools; +use rustc_hash::{FxHashMap, FxHashSet}; +use std::sync::Arc; +use vfs::{AbsPathBuf, FileId, Vfs}; + +use super::{ConfigInput, LocalConfigData, RootLocalConfigData}; + +#[derive(Debug, PartialEq)] +pub enum ConfigTreeError { + Removed, + NonExistent, + Utf8(FileId, std::str::Utf8Error), + TomlParse(FileId, toml::de::Error), + TomlDeserialize { file_id: FileId, field: String, error: toml::de::Error }, +} + +/// Some rust-analyzer.toml files have changed, and/or the LSP client sent a new configuration. +pub struct ConfigChanges { + /// - `None` => no change + /// - `Some(None)` => the client config was removed / reset or something + /// - `Some(Some(...))` => the client config was updated + client_change: Option>>, + set_project_root: Option, + set_source_roots: Option>, + ra_toml_changes: Vec, +} + +/// Internal version +struct ConfigChangesInner { + /// - `None` => no change + /// - `Some(None)` => the client config was removed / reset or something + /// - `Some(Some(...))` => the client config was updated + client_change: Option>>, + parent_changes: FxHashMap, + ra_toml_changes: Vec, +} + +#[derive(Debug)] +pub enum ConfigParent { + /// The node is now a root in its own right, but still inherits from the config in XDG_CONFIG_HOME + /// etc + UserDefault, + /// The node is now a child of another rust-analyzer.toml. Even if that one is a non-existent + /// file, it's fine. + /// + /// + /// ```ignore,text + /// /project_root/ + /// rust-analyzer.toml + /// crate_a/ + /// crate_b/ + /// rust-analyzer.toml + /// + /// ``` + /// + /// ```ignore + /// // imagine set_file_contents = vfs.set_file_contents() and then get the vfs.file_id() + /// + /// let root = vfs.set_file_contents("/project_root/rust-analyzer.toml", Some("...")); + /// let crate_a = vfs.set_file_contents("/project_root/crate_a/rust-analyzer.toml", None); + /// let crate_b = vfs.set_file_contents("/project_root/crate_a/crate_b/rust-analyzer.toml", Some("...")); + /// let parent_changes = FxHashMap::from_iter([ + /// (root, ConfigParent::UserDefault), + /// (crate_a, ConfigParent::Parent(root)), + /// (crate_b, ConfigParent::Parent(crate_a)), + /// ]); + /// ``` + Parent(FileId), +} + +/// Easier and probably more performant than making ConfigInput implement Eq +#[derive(Debug)] +struct PointerCmp(Arc); +impl PointerCmp { + fn new(t: T) -> Self { + Self(Arc::new(t)) + } +} +impl Clone for PointerCmp { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} +impl PartialEq for PointerCmp { + fn eq(&self, other: &Self) -> bool { + (Arc::as_ptr(&self.0) as *const ()).eq(&Arc::as_ptr(&other.0).cast()) + } +} +impl Eq for PointerCmp {} +impl std::ops::Deref for PointerCmp { + type Target = T; + fn deref(&self) -> &T { + self.0.deref() + } +} + +#[salsa::query_group(ConfigTreeStorage)] +trait ConfigTreeQueries { + #[salsa::input] + fn client_config(&self) -> Option>; + + #[salsa::input] + fn parent(&self, file_id: FileId) -> Option; + + #[salsa::input] + fn config_input(&self, file_id: FileId) -> Option>; + + fn recursive_local(&self, file_id: FileId) -> PointerCmp; + + /// The output + fn computed_local_config(&self, file_id: FileId) -> PointerCmp; +} + +fn recursive_local(db: &dyn ConfigTreeQueries, file_id: FileId) -> PointerCmp { + let self_input = db.config_input(file_id); + tracing::trace!(?self_input, ?file_id); + match db.parent(file_id) { + Some(parent) if parent != file_id => { + let parent_computed = db.recursive_local(parent); + if let Some(input) = self_input.as_deref() { + PointerCmp::new(parent_computed.clone_with_overrides(input.local.clone())) + } else { + parent_computed + } + } + _ => { + // this is a root node, or we just broke a cycle + if let Some(input) = self_input.as_deref() { + let root_local = RootLocalConfigData::from_root_input(input.local.clone()); + PointerCmp::new(root_local.0) + } else { + PointerCmp::new(LocalConfigData::default()) + } + } + } +} + +fn computed_local_config( + db: &dyn ConfigTreeQueries, + file_id: FileId, +) -> PointerCmp { + let computed = db.recursive_local(file_id); + if let Some(client) = db.client_config() { + PointerCmp::new(computed.clone_with_overrides(client.local.clone())) + } else { + computed + } +} + +#[salsa::database(ConfigTreeStorage)] +pub struct ConfigDb { + storage: salsa::Storage, + known_file_ids: FxHashSet, + xdg_config_file_id: FileId, + source_roots: FxHashSet, + project_root: AbsPathBuf, +} + +impl salsa::Database for ConfigDb {} + +impl ConfigDb { + pub fn new(xdg_config_file_id: FileId, project_root: AbsPathBuf) -> Self { + let mut this = Self { + storage: Default::default(), + known_file_ids: FxHashSet::default(), + xdg_config_file_id, + source_roots: FxHashSet::default(), + project_root, + }; + this.set_client_config(None); + this.ensure_node(xdg_config_file_id); + this + } + + /// Gets the value of LocalConfigData for a given `rust-analyzer.toml` FileId. + /// + /// The rust-analyzer.toml does not need to exist on disk. All values are the expression of + /// overriding the parent `rust-analyzer.toml`, set by adding an entry in + /// `ConfigChanges.parent_changes`. + /// + /// If the db is not aware of the given `rust-analyzer.toml` FileId, then the config is read + /// from the user's system-wide default config. + /// + /// Note that the client config overrides all configs. + pub fn local_config(&self, ra_toml_file_id: FileId) -> Arc { + if self.known_file_ids.contains(&ra_toml_file_id) { + self.computed_local_config(ra_toml_file_id).0 + } else { + tracing::warn!(?ra_toml_file_id, "called local_config with unknown file id"); + self.computed_local_config(self.xdg_config_file_id).0 + } + } + + /// Applies a bunch of [`ConfigChanges`]. The FileIds referred to in `ConfigChanges` do not + /// need to exist. + pub fn apply_changes(&mut self, changes: ConfigChanges, vfs: &mut Vfs) -> Vec { + if let Some(new_project_root) = &changes.set_project_root { + self.project_root = new_project_root.clone(); + } + let source_root_change = changes.set_source_roots.as_ref().or_else(|| { + if changes.set_project_root.is_some() { + Some(&self.source_roots) + } else { + None + } + }); + let parent_changes = if let Some(source_roots) = source_root_change { + let parent_changes = source_roots + .iter() + .flat_map(|path: &AbsPathBuf| { + path.ancestors() + // Note that Path::new("/root2/abc").starts_with(Path::new("/root")) is false + .take_while(|x| x.starts_with(&self.project_root)) + .map(|dir| dir.join("rust-analyzer.toml")) + .map(|path| vfs.alloc_file_id(path.into())) + .collect_vec() + // immediately get tuple_windows before returning from flat_map + .into_iter() + .tuple_windows() + .map(|(a, b)| (a, ConfigParent::Parent(b))) + }) + .collect::>(); + + // Remove source roots (& their parent config files) that are no longer part of the project root + self.known_file_ids + .iter() + .cloned() + .filter(|&x| x != self.xdg_config_file_id && !parent_changes.contains_key(&x)) + .collect_vec() + .into_iter() + .for_each(|deleted| { + self.known_file_ids.remove(&deleted); + self.reset_node(deleted); + }); + + parent_changes + } else { + Default::default() + }; + + if tracing::enabled!(tracing::Level::TRACE) { + for (&a, parent) in &parent_changes { + tracing::trace!( + "{a:?} ({:?}): parent = {parent:?} ({:?})", + vfs.file_path(a), + match parent { + ConfigParent::Parent(p) => vfs.file_path(*p).to_string(), + ConfigParent::UserDefault => "xdg".to_string(), + } + ); + } + } + + let inner = ConfigChangesInner { + ra_toml_changes: changes.ra_toml_changes, + client_change: changes.client_change, + parent_changes, + }; + self.apply_changes_inner(inner, vfs) + } + + fn apply_changes_inner( + &mut self, + changes: ConfigChangesInner, + vfs: &Vfs, + ) -> Vec { + let mut scratch_errors = Vec::new(); + let mut errors = Vec::new(); + let ConfigChangesInner { client_change, ra_toml_changes, parent_changes } = changes; + + if let Some(change) = client_change { + let current = self.client_config(); + let change = change.map(PointerCmp); + match (current.as_ref(), change.as_ref()) { + (None, None) => {} + (Some(a), Some(b)) if a == b => {} + _ => { + self.set_client_config(change); + } + } + } + + for (file_id, parent) in parent_changes { + self.ensure_node(file_id); + let parent_node_id = match parent { + ConfigParent::Parent(parent_file_id) => { + self.ensure_node(parent_file_id); + parent_file_id + } + ConfigParent::UserDefault if file_id == self.xdg_config_file_id => continue, + ConfigParent::UserDefault => self.xdg_config_file_id, + }; + self.set_parent(file_id, Some(parent_node_id)) + } + + for change in ra_toml_changes { + if !self.known_file_ids.contains(&change.file_id) { + // Irrelevant Vfs change. Ideally you would not pass these in at all, but it's not + // a problem to filter them out here. + continue; + } + // turn and face the strain + match change.change_kind { + vfs::ChangeKind::Create | vfs::ChangeKind::Modify => { + let input = parse_toml(change.file_id, vfs, &mut scratch_errors, &mut errors) + .map(PointerCmp); + tracing::trace!("updating toml for {:?} to {:?}", change.file_id, input); + + self.set_config_input(change.file_id, input); + } + vfs::ChangeKind::Delete => { + self.set_config_input(change.file_id, None); + } + } + } + + errors + } + + /// Inserts default values into the salsa inputs for the given file_id + /// if it's never been seen before + fn ensure_node(&mut self, file_id: FileId) { + if self.known_file_ids.insert(file_id) { + self.reset_node(file_id); + } + } + + fn reset_node(&mut self, file_id: FileId) { + self.set_config_input(file_id, None); + self.set_parent( + file_id, + if file_id == self.xdg_config_file_id { None } else { Some(self.xdg_config_file_id) }, + ); + } +} + +fn parse_toml( + file_id: FileId, + vfs: &Vfs, + scratch: &mut Vec<(String, toml::de::Error)>, + errors: &mut Vec, +) -> Option> { + let content = vfs.file_contents(file_id); + let content_str = match std::str::from_utf8(content) { + Err(e) => { + tracing::error!("non-UTF8 TOML content for {file_id:?}: {e}"); + errors.push(ConfigTreeError::Utf8(file_id, e)); + return None; + } + Ok(str) => str, + }; + if content_str.is_empty() { + return None; + } + let table = match toml::from_str(content_str) { + Ok(table) => table, + Err(e) => { + errors.push(ConfigTreeError::TomlParse(file_id, e)); + return None; + } + }; + let input = Arc::new(ConfigInput::from_toml(table, scratch)); + scratch.drain(..).for_each(|(field, error)| { + errors.push(ConfigTreeError::TomlDeserialize { file_id, field, error }); + }); + Some(input) +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use vfs::{AbsPath, AbsPathBuf, VfsPath}; + + fn alloc_file_id(vfs: &mut Vfs, s: &str) -> FileId { + let abs_path = AbsPath::assert(s).to_path_buf(); + let vfs_path = VfsPath::from(abs_path); + let file_id = vfs.alloc_file_id(vfs_path); + vfs.set_file_id_contents(file_id, None); + file_id + } + + fn alloc_config(vfs: &mut Vfs, s: &str, config: &str) -> FileId { + let abs_path = AbsPath::assert(s).to_path_buf(); + let vfs_path = VfsPath::from(abs_path); + let file_id = vfs.alloc_file_id(vfs_path); + vfs.set_file_id_contents(file_id, Some(config.to_string().into_bytes())); + file_id + } + + const XDG_CONFIG_HOME_RATOML: &'static str = + "/home/username/.config/rust-analyzer/rust-analyzer.toml"; + + use super::*; + #[test] + fn basic() { + tracing_subscriber::fmt().try_init().ok(); + let mut vfs = Vfs::default(); + let project_root = AbsPath::assert("/root"); + let xdg_config_file_id = alloc_file_id(&mut vfs, XDG_CONFIG_HOME_RATOML); + let mut config_tree = ConfigDb::new(xdg_config_file_id, project_root.to_path_buf()); + + let source_roots = ["/root/crate_a"].map(AbsPath::assert); + + let _root = alloc_config( + &mut vfs, + "/root/rust-analyzer.toml", + r#" + [completion.autoself] + enable = false + "#, + ); + + let crate_a = alloc_config( + &mut vfs, + "/root/crate_a/rust-analyzer.toml", + r#" + [completion.autoimport] + enable = false + # will be overridden by client + [semanticHighlighting.strings] + enable = true + "#, + ); + + let new_source_roots = source_roots.into_iter().map(|abs| abs.to_path_buf()).collect(); + let changes = ConfigChanges { + // Normally you will filter these! + ra_toml_changes: vfs.take_changes(), + set_project_root: None, + set_source_roots: Some(new_source_roots), + client_change: Some(Some(Arc::new(ConfigInput { + local: crate::config::LocalConfigInput { + semanticHighlighting_strings_enable: Some(false), + ..Default::default() + }, + ..Default::default() + }))), + }; + + dbg!(config_tree.apply_changes(changes, &mut vfs)); + + let local = config_tree.local_config(crate_a); + // from root + assert_eq!(local.completion_autoself_enable, false); + // from crate_a + assert_eq!(local.completion_autoimport_enable, false); + // from client + assert_eq!(local.semanticHighlighting_strings_enable, false); + + // -------------------------------------------------------- + + // Now let's modify the xdg_config_file_id, which should invalidate everything else + vfs.set_file_id_contents( + xdg_config_file_id, + Some( + r#" + # default is "never" + [inlayHints.discriminantHints] + enable = "always" + [completion.autoself] + enable = true + [completion.autoimport] + enable = true + [semanticHighlighting.strings] + enable = true + "# + .to_string() + .into_bytes(), + ), + ); + + let changes = ConfigChanges { + client_change: None, + set_project_root: None, + set_source_roots: None, + ra_toml_changes: dbg!(vfs.take_changes()), + }; + dbg!(config_tree.apply_changes(changes, &mut vfs)); + + let prev = local; + let local = config_tree.local_config(crate_a); + // Should have been recomputed + assert!(!Arc::ptr_eq(&prev, &local)); + // But without changes in between, should give the same Arc back + assert!(Arc::ptr_eq(&local, &config_tree.local_config(crate_a))); + + // The newly added xdg_config_file_id should affect the output if nothing else touches + // this key + assert_eq!( + local.inlayHints_discriminantHints_enable, + crate::config::DiscriminantHintsDef::Always + ); + // But it should not win + assert_eq!(local.completion_autoself_enable, false); + assert_eq!(local.completion_autoimport_enable, false); + assert_eq!(local.semanticHighlighting_strings_enable, false); + } + + #[test] + fn set_source_roots() { + tracing_subscriber::fmt().try_init().ok(); + let mut vfs = Vfs::default(); + + let project_root = AbsPath::assert("/root"); + let xdg = alloc_file_id(&mut vfs, XDG_CONFIG_HOME_RATOML); + let mut config_tree = ConfigDb::new(xdg, project_root.to_path_buf()); + + let source_roots = ["/root/crate_a", "/root/crate_a/crate_b"].map(AbsPath::assert); + let [crate_a, crate_b] = source_roots + .map(|dir| dir.join("rust-analyzer.toml")) + .map(|path| vfs.alloc_file_id(path.into())); + + vfs.set_file_id_contents( + xdg, + Some(b"[inlayHints.discriminantHints]\nenable = \"always\"".to_vec()), + ); + vfs.set_file_id_contents(crate_a, Some(b"[completion.autoself]\nenable = false".to_vec())); + // note that crate_b's rust-analyzer.toml doesn't exist + + let new_source_roots = source_roots.into_iter().map(|abs| abs.to_path_buf()).collect(); + let changes = ConfigChanges { + client_change: None, + set_project_root: None, // already set in ConfigDb::new(...) + set_source_roots: Some(new_source_roots), + ra_toml_changes: dbg!(vfs.take_changes()), + }; + + dbg!(config_tree.apply_changes(changes, &mut vfs)); + let local = config_tree.local_config(crate_b); + + assert_eq!( + local.inlayHints_discriminantHints_enable, + crate::config::DiscriminantHintsDef::Always + ); + assert_eq!(local.completion_autoself_enable, false); + + // ---- + + // Now move crate b to the root. This gives a new FileId for crate_b/ra.toml. + let source_roots = ["/root/crate_a", "/root/crate_b"].map(AbsPath::assert); + let [_crate_a, crate_b] = source_roots + .map(|dir| dir.join("rust-analyzer.toml")) + .map(|path| vfs.alloc_file_id(path.into())); + let new_source_roots = source_roots.into_iter().map(|abs| abs.to_path_buf()).collect(); + let changes = ConfigChanges { + client_change: None, + set_project_root: None, // already set in ConfigDb::new(...) + set_source_roots: Some(new_source_roots), + ra_toml_changes: dbg!(vfs.take_changes()), + }; + + dbg!(config_tree.apply_changes(changes, &mut vfs)); + let local = config_tree.local_config(crate_b); + + // Still inherits from xdg + assert_eq!( + local.inlayHints_discriminantHints_enable, + crate::config::DiscriminantHintsDef::Always + ); + // new crate_b does not inherit from crate_a + assert_eq!(local.completion_autoself_enable, true); + } + + #[test] + fn change_project_root() { + tracing_subscriber::fmt().try_init().ok(); + let mut vfs = Vfs::default(); + + let project_root = AbsPath::assert("/root"); + let xdg = alloc_file_id(&mut vfs, XDG_CONFIG_HOME_RATOML); + let mut config_tree = ConfigDb::new(xdg, project_root.to_path_buf()); + + let source_roots = ["/root/crate_a"].map(AbsPath::assert); + let crate_a = vfs.alloc_file_id(source_roots[0].join("rust-analyzer.toml").into()); + + let _root = alloc_config( + &mut vfs, + "/root/rust-analyzer.toml", + r#" + [completion.autoself] + enable = false + "#, + ); + + let new_source_roots = source_roots.into_iter().map(|abs| abs.to_path_buf()).collect(); + let changes = ConfigChanges { + client_change: None, + set_project_root: None, // already set in ConfigDb::new(...) + set_source_roots: Some(new_source_roots), + ra_toml_changes: dbg!(vfs.take_changes()), + }; + config_tree.apply_changes(changes, &mut vfs); + let local = config_tree.local_config(crate_a); + // initially crate_a is part of the project root, so it does inherit + // from /root/rust-analyzer.toml + assert_eq!(local.completion_autoself_enable, false); + + // change project root + let changes = ConfigChanges { + client_change: None, + set_project_root: Some(AbsPath::assert("/ro").to_path_buf()), + set_source_roots: None, + ra_toml_changes: dbg!(vfs.take_changes()), + }; + config_tree.apply_changes(changes, &mut vfs); + // crate_a is now outside the project root and hence inherit (1) xdg (2) + // crate_a/ra.toml, but not /root/rust-analyzer.toml any more + let local = config_tree.local_config(crate_a); + assert_eq!(local.completion_autoself_enable, true); + } + + #[test] + fn no_change_to_source_roots() { + tracing_subscriber::fmt().try_init().ok(); + let mut vfs = Vfs::default(); + + let project_root = AbsPath::assert("/root"); + let xdg = alloc_file_id(&mut vfs, XDG_CONFIG_HOME_RATOML); + let mut config_tree = ConfigDb::new(xdg, project_root.to_path_buf()); + + let source_roots = ["/root/crate_a"].map(AbsPath::assert); + let crate_a = vfs.alloc_file_id(source_roots[0].join("rust-analyzer.toml").into()); + + let _root = alloc_config( + &mut vfs, + "/root/rust-analyzer.toml", + r#" + [completion.autoself] + enable = false + "#, + ); + + let new_source_roots = source_roots.into_iter().map(|abs| abs.to_path_buf()).collect(); + let changes = ConfigChanges { + client_change: None, + set_project_root: None, // already set in ConfigDb::new(...) + set_source_roots: Some(new_source_roots), + ra_toml_changes: dbg!(vfs.take_changes()), + }; + config_tree.apply_changes(changes, &mut vfs); + let local = config_tree.local_config(crate_a); + // initially crate_a is part of the project root, so it does inherit + // from /root/rust-analyzer.toml + assert_eq!(local.completion_autoself_enable, false); + + // Send in an empty change, should have no effect + let changes = ConfigChanges { + client_change: None, + set_project_root: None, + set_source_roots: None, + ra_toml_changes: dbg!(vfs.take_changes()), + }; + config_tree.apply_changes(changes, &mut vfs); + let local = config_tree.local_config(crate_a); + assert_eq!(local.completion_autoself_enable, false); + } + + #[test] + fn ignore_irrelevant_vfs_changes() { + tracing_subscriber::fmt().try_init().ok(); + let mut vfs = Vfs::default(); + + let project_root = AbsPath::assert("/root"); + let xdg = alloc_file_id(&mut vfs, XDG_CONFIG_HOME_RATOML); + let mut config_tree = ConfigDb::new(xdg, project_root.to_path_buf()); + + // The main way an irrelevant vfs file change is going to show up is in TOML parse errors. + let invalid_utf8 = b"\xc3\x28"; + vfs.set_file_contents( + AbsPath::assert("/irrelevant/file.bin").to_path_buf().into(), + Some(invalid_utf8.to_vec()), + ); + let errors = config_tree.apply_changes( + ConfigChanges { + client_change: None, + set_project_root: None, + set_source_roots: None, + ra_toml_changes: vfs.take_changes(), + }, + &mut vfs, + ); + assert_eq!(errors, vec![]); + } +} diff --git a/crates/vfs/src/lib.rs b/crates/vfs/src/lib.rs index 06004adad34a..0c8474f60753 100644 --- a/crates/vfs/src/lib.rs +++ b/crates/vfs/src/lib.rs @@ -157,8 +157,18 @@ impl Vfs { /// /// If the path does not currently exists in the `Vfs`, allocates a new /// [`FileId`] for it. - pub fn set_file_contents(&mut self, path: VfsPath, mut contents: Option>) -> bool { + pub fn set_file_contents(&mut self, path: VfsPath, contents: Option>) -> bool { let file_id = self.alloc_file_id(path); + self.set_file_id_contents(file_id, contents) + } + + /// Update the given `file_id` with the given `contents`. `None` means the file was deleted. + /// + /// Returns `true` if the file was modified, and saves the [change](ChangedFile). + /// + /// If the path does not currently exists in the `Vfs`, allocates a new + /// [`FileId`] for it. + pub fn set_file_id_contents(&mut self, file_id: FileId, mut contents: Option>) -> bool { let change_kind = match (self.get(file_id), &contents) { (None, None) => return false, (Some(old), Some(new)) if old == new => return false, @@ -196,7 +206,7 @@ impl Vfs { /// - Else, returns `path`'s id. /// /// Does not record a change. - fn alloc_file_id(&mut self, path: VfsPath) -> FileId { + pub fn alloc_file_id(&mut self, path: VfsPath) -> FileId { let file_id = self.interner.intern(path); let idx = file_id.0 as usize; let len = self.data.len().max(idx + 1);