393 lines · 15.0 KB · Rust
RawDownload
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<Self, Self::Err> {        let parser = ConfigParser::new_buffered(contents.as_bytes())            .context("creating editorconfig parser")?;        let is_root = parser.is_root;        let sections = parser            .collect::<Result<SmallVec<_>, _>>()            .context("parsing editorconfig sections")?;        Ok(Self { is_root, sections })    }} #[derive(Clone, Debug)]pub enum EditorconfigEvent {    ExternalConfigChanged {        path: LocalSettingsPath,        content: Option<String>,        affected_worktree_ids: Vec<WorktreeId>,    },} impl EventEmitter<EditorconfigEvent> for EditorconfigStore {} #[derive(Default)]pub struct EditorconfigStore {    external_configs: BTreeMap<Arc<Path>, (String, Option<Editorconfig>)>,    worktree_state: BTreeMap<WorktreeId, EditorconfigWorktreeState>,    local_external_config_watchers: BTreeMap<Arc<Path>, Task<()>>,    local_external_config_discovery_tasks: BTreeMap<WorktreeId, Task<()>>,} #[derive(Default)]struct EditorconfigWorktreeState {    internal_configs: BTreeMap<Arc<RelPath>, (String, Option<Editorconfig>)>,    external_config_paths: BTreeSet<Arc<Path>>,} 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::<Editorconfig>() {                        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::<Editorconfig>() {                        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<Item = (&RelPath, &str, Option<&Editorconfig>)> {        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<Item = (&Path, &str, Option<&Editorconfig>)> {        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<Item = (LocalSettingsPath, &str, Option<&Editorconfig>)> {        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<Path>,        fs: Arc<dyn Fs>,        cx: &mut Context<Self>,    ) {        // 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<Path> = 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<dyn Fs>,        dir_path: Arc<Path>,        cx: &mut Context<Self>,    ) -> 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<WorktreeId> = 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<EditorconfigProperties> {        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<WorktreeId>, Vec<Arc<Path>>, Vec<Arc<Path>>) {        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<Arc<Path>> {        self.worktree_state            .get(&worktree_id)            .map(|state| state.external_config_paths.iter().cloned().collect())            .unwrap_or_default()    }}