use anyhow::{Context as _, Result}; use collections::{BTreeMap, BTreeSet, HashSet}; use ec4rs::{ConfigParser, PropertiesSource, Section}; use fs::Fs; use futures::StreamExt; use gpui::{Context, EventEmitter, Task}; use paths::EDITORCONFIG_NAME; use smallvec::SmallVec; use std::{path::Path, str::FromStr, sync::Arc}; use util::{ResultExt as _, rel_path::RelPath}; use crate::{InvalidSettingsError, LocalSettingsPath, WorktreeId, watch_config_file}; pub type EditorconfigProperties = ec4rs::Properties; #[derive(Clone)] pub struct Editorconfig { pub is_root: bool, pub sections: SmallVec<[Section; 5]>, } impl FromStr for Editorconfig { type Err = anyhow::Error; fn from_str(contents: &str) -> Result { let parser = ConfigParser::new_buffered(contents.as_bytes()) .context("creating editorconfig parser")?; let is_root = parser.is_root; let sections = parser .collect::, _>>() .context("parsing editorconfig sections")?; Ok(Self { is_root, sections }) } } #[derive(Clone, Debug)] pub enum EditorconfigEvent { ExternalConfigChanged { path: LocalSettingsPath, content: Option, affected_worktree_ids: Vec, }, } impl EventEmitter for EditorconfigStore {} #[derive(Default)] pub struct EditorconfigStore { external_configs: BTreeMap, (String, Option)>, worktree_state: BTreeMap, local_external_config_watchers: BTreeMap, Task<()>>, local_external_config_discovery_tasks: BTreeMap>, } #[derive(Default)] struct EditorconfigWorktreeState { internal_configs: BTreeMap, (String, Option)>, external_config_paths: BTreeSet>, } impl EditorconfigStore { pub(crate) fn set_configs( &mut self, worktree_id: WorktreeId, path: LocalSettingsPath, content: Option<&str>, ) -> std::result::Result<(), InvalidSettingsError> { match (&path, content) { (LocalSettingsPath::InWorktree(rel_path), None) => { if let Some(state) = self.worktree_state.get_mut(&worktree_id) { state.internal_configs.remove(rel_path); } } (LocalSettingsPath::OutsideWorktree(abs_path), None) => { if let Some(state) = self.worktree_state.get_mut(&worktree_id) { state.external_config_paths.remove(abs_path); } let still_in_use = self .worktree_state .values() .any(|state| state.external_config_paths.contains(abs_path)); if !still_in_use { self.external_configs.remove(abs_path); self.local_external_config_watchers.remove(abs_path); } } (LocalSettingsPath::InWorktree(rel_path), Some(content)) => { let state = self.worktree_state.entry(worktree_id).or_default(); let should_update = state .internal_configs .get(rel_path) .map_or(true, |entry| entry.0 != content); if should_update { let parsed = match content.parse::() { Ok(parsed) => Some(parsed), Err(e) => { state .internal_configs .insert(rel_path.clone(), (content.to_owned(), None)); return Err(InvalidSettingsError::Editorconfig { message: e.to_string(), path: LocalSettingsPath::InWorktree( rel_path.join(RelPath::unix(EDITORCONFIG_NAME).unwrap()), ), }); } }; state .internal_configs .insert(rel_path.clone(), (content.to_owned(), parsed)); } } (LocalSettingsPath::OutsideWorktree(abs_path), Some(content)) => { let state = self.worktree_state.entry(worktree_id).or_default(); state.external_config_paths.insert(abs_path.clone()); let should_update = self .external_configs .get(abs_path) .map_or(true, |entry| entry.0 != content); if should_update { let parsed = match content.parse::() { Ok(parsed) => Some(parsed), Err(e) => { self.external_configs .insert(abs_path.clone(), (content.to_owned(), None)); return Err(InvalidSettingsError::Editorconfig { message: e.to_string(), path: LocalSettingsPath::OutsideWorktree( abs_path.join(EDITORCONFIG_NAME).into(), ), }); } }; self.external_configs .insert(abs_path.clone(), (content.to_owned(), parsed)); } } } Ok(()) } pub(crate) fn remove_for_worktree(&mut self, root_id: WorktreeId) { self.local_external_config_discovery_tasks.remove(&root_id); let Some(removed) = self.worktree_state.remove(&root_id) else { return; }; let paths_in_use: HashSet<_> = self .worktree_state .values() .flat_map(|w| w.external_config_paths.iter()) .collect(); for path in removed.external_config_paths.iter() { if !paths_in_use.contains(path) { self.external_configs.remove(path); self.local_external_config_watchers.remove(path); } } } fn internal_configs( &self, root_id: WorktreeId, ) -> impl '_ + Iterator)> { self.worktree_state .get(&root_id) .into_iter() .flat_map(|state| { state .internal_configs .iter() .map(|(path, data)| (path.as_ref(), data.0.as_str(), data.1.as_ref())) }) } fn external_configs( &self, worktree_id: WorktreeId, ) -> impl '_ + Iterator)> { self.worktree_state .get(&worktree_id) .into_iter() .flat_map(|state| { state.external_config_paths.iter().filter_map(|path| { self.external_configs .get(path) .map(|entry| (path.as_ref(), entry.0.as_str(), entry.1.as_ref())) }) }) } pub fn local_editorconfig_settings( &self, worktree_id: WorktreeId, ) -> impl '_ + Iterator)> { let external = self .external_configs(worktree_id) .map(|(path, content, parsed)| { ( LocalSettingsPath::OutsideWorktree(path.into()), content, parsed, ) }); let internal = self .internal_configs(worktree_id) .map(|(path, content, parsed)| { (LocalSettingsPath::InWorktree(path.into()), content, parsed) }); external.chain(internal) } pub fn discover_local_external_configs_chain( &mut self, worktree_id: WorktreeId, worktree_path: Arc, fs: Arc, cx: &mut Context, ) { // We should only have one discovery task per worktree. if self .local_external_config_discovery_tasks .contains_key(&worktree_id) { return; } let task = cx.spawn({ let fs = fs.clone(); async move |this, cx| { let discovered_paths = { let mut paths = Vec::new(); let mut current = worktree_path.parent().map(|p| p.to_path_buf()); while let Some(dir) = current { let dir_path: Arc = Arc::from(dir.as_path()); let path = dir.join(EDITORCONFIG_NAME); if fs.load(&path).await.is_ok() { paths.push(dir_path); } current = dir.parent().map(|p| p.to_path_buf()); } paths }; this.update(cx, |this, cx| { for dir_path in discovered_paths { // We insert it here so that watchers can send events to appropriate worktrees. // external_config_paths gets populated again in set_configs. this.worktree_state .entry(worktree_id) .or_default() .external_config_paths .insert(dir_path.clone()); match this.local_external_config_watchers.entry(dir_path.clone()) { std::collections::btree_map::Entry::Occupied(_) => { if let Some(existing_config) = this.external_configs.get(&dir_path) { cx.emit(EditorconfigEvent::ExternalConfigChanged { path: LocalSettingsPath::OutsideWorktree(dir_path), content: Some(existing_config.0.clone()), affected_worktree_ids: vec![worktree_id], }); } else { log::error!("Watcher exists for {dir_path:?} but no config found in external_configs"); } } std::collections::btree_map::Entry::Vacant(entry) => { let watcher = Self::watch_local_external_config(fs.clone(), dir_path, cx); entry.insert(watcher); } } } }) .ok(); } }); self.local_external_config_discovery_tasks .insert(worktree_id, task); } fn watch_local_external_config( fs: Arc, dir_path: Arc, cx: &mut Context, ) -> Task<()> { let config_path = dir_path.join(EDITORCONFIG_NAME); let (mut config_rx, watcher_task) = watch_config_file(cx.background_executor(), fs, config_path); cx.spawn(async move |this, cx| { let _watcher_task = watcher_task; while let Some(content) = config_rx.next().await { let content = Some(content).filter(|c| !c.is_empty()); let dir_path = dir_path.clone(); this.update(cx, |this, cx| { let affected_worktree_ids: Vec = this .worktree_state .iter() .filter_map(|(id, state)| { state .external_config_paths .contains(&dir_path) .then_some(*id) }) .collect(); cx.emit(EditorconfigEvent::ExternalConfigChanged { path: LocalSettingsPath::OutsideWorktree(dir_path), content, affected_worktree_ids, }); }) .ok(); } }) } pub fn properties( &self, for_worktree: WorktreeId, for_path: &RelPath, ) -> Option { let mut properties = EditorconfigProperties::new(); let state = self.worktree_state.get(&for_worktree); let internal_root_config_is_root = state .and_then(|state| state.internal_configs.get(RelPath::empty())) .and_then(|data| data.1.as_ref()) .is_some_and(|ec| ec.is_root); let std_path = for_path.as_std_path(); if !internal_root_config_is_root { for (_, _, parsed_editorconfig) in self.external_configs(for_worktree) { if let Some(parsed_editorconfig) = parsed_editorconfig { if parsed_editorconfig.is_root { properties = EditorconfigProperties::new(); } for section in &parsed_editorconfig.sections { section.apply_to(&mut properties, std_path).log_err()?; } } } } if let Some(state) = state { let mut internal_configs: SmallVec<[&Editorconfig; 8]> = SmallVec::new(); for ancestor in for_path.ancestors() { if let Some((_, parsed)) = state.internal_configs.get(ancestor) { let config = parsed.as_ref()?; internal_configs.push(config); if config.is_root { break; } } } for config in internal_configs.into_iter().rev() { if config.is_root { properties = EditorconfigProperties::new(); } for section in &config.sections { section.apply_to(&mut properties, std_path).log_err()?; } } } properties.use_fallbacks(); Some(properties) } } #[cfg(any(test, feature = "test-support"))] impl EditorconfigStore { pub fn test_state(&self) -> (Vec, Vec>, Vec>) { let worktree_ids: Vec<_> = self.worktree_state.keys().copied().collect(); let external_paths: Vec<_> = self.external_configs.keys().cloned().collect(); let watcher_paths: Vec<_> = self .local_external_config_watchers .keys() .cloned() .collect(); (worktree_ids, external_paths, watcher_paths) } pub fn external_config_paths_for_worktree(&self, worktree_id: WorktreeId) -> Vec> { self.worktree_state .get(&worktree_id) .map(|state| state.external_config_paths.iter().cloned().collect()) .unwrap_or_default() } }