editorconfig_store.rs

  1use anyhow::{Context as _, Result};
  2use collections::{BTreeMap, BTreeSet, HashSet};
  3use ec4rs::{ConfigParser, PropertiesSource, Section};
  4use fs::Fs;
  5use futures::StreamExt;
  6use gpui::{Context, EventEmitter, Task};
  7use paths::EDITORCONFIG_NAME;
  8use smallvec::SmallVec;
  9use std::{path::Path, str::FromStr, sync::Arc};
 10use util::{ResultExt as _, rel_path::RelPath};
 11
 12use crate::{InvalidSettingsError, LocalSettingsPath, WorktreeId, watch_config_file};
 13
 14pub type EditorconfigProperties = ec4rs::Properties;
 15
 16#[derive(Clone)]
 17pub struct Editorconfig {
 18    pub is_root: bool,
 19    pub sections: SmallVec<[Section; 5]>,
 20}
 21
 22impl FromStr for Editorconfig {
 23    type Err = anyhow::Error;
 24
 25    fn from_str(contents: &str) -> Result<Self, Self::Err> {
 26        let parser = ConfigParser::new_buffered(contents.as_bytes())
 27            .context("creating editorconfig parser")?;
 28        let is_root = parser.is_root;
 29        let sections = parser
 30            .collect::<Result<SmallVec<_>, _>>()
 31            .context("parsing editorconfig sections")?;
 32        Ok(Self { is_root, sections })
 33    }
 34}
 35
 36#[derive(Clone, Debug)]
 37pub enum EditorconfigEvent {
 38    ExternalConfigChanged {
 39        path: LocalSettingsPath,
 40        content: Option<String>,
 41        affected_worktree_ids: Vec<WorktreeId>,
 42    },
 43}
 44
 45impl EventEmitter<EditorconfigEvent> for EditorconfigStore {}
 46
 47#[derive(Default)]
 48pub struct EditorconfigStore {
 49    external_configs: BTreeMap<Arc<Path>, (String, Option<Editorconfig>)>,
 50    worktree_state: BTreeMap<WorktreeId, EditorconfigWorktreeState>,
 51    local_external_config_watchers: BTreeMap<Arc<Path>, Task<()>>,
 52    local_external_config_discovery_tasks: BTreeMap<WorktreeId, Task<()>>,
 53}
 54
 55#[derive(Default)]
 56struct EditorconfigWorktreeState {
 57    internal_configs: BTreeMap<Arc<RelPath>, (String, Option<Editorconfig>)>,
 58    external_config_paths: BTreeSet<Arc<Path>>,
 59}
 60
 61impl EditorconfigStore {
 62    pub(crate) fn set_configs(
 63        &mut self,
 64        worktree_id: WorktreeId,
 65        path: LocalSettingsPath,
 66        content: Option<&str>,
 67    ) -> std::result::Result<(), InvalidSettingsError> {
 68        match (&path, content) {
 69            (LocalSettingsPath::InWorktree(rel_path), None) => {
 70                if let Some(state) = self.worktree_state.get_mut(&worktree_id) {
 71                    state.internal_configs.remove(rel_path);
 72                }
 73            }
 74            (LocalSettingsPath::OutsideWorktree(abs_path), None) => {
 75                if let Some(state) = self.worktree_state.get_mut(&worktree_id) {
 76                    state.external_config_paths.remove(abs_path);
 77                }
 78                let still_in_use = self
 79                    .worktree_state
 80                    .values()
 81                    .any(|state| state.external_config_paths.contains(abs_path));
 82                if !still_in_use {
 83                    self.external_configs.remove(abs_path);
 84                    self.local_external_config_watchers.remove(abs_path);
 85                }
 86            }
 87            (LocalSettingsPath::InWorktree(rel_path), Some(content)) => {
 88                let state = self.worktree_state.entry(worktree_id).or_default();
 89                let should_update = state
 90                    .internal_configs
 91                    .get(rel_path)
 92                    .map_or(true, |entry| entry.0 != content);
 93                if should_update {
 94                    let parsed = match content.parse::<Editorconfig>() {
 95                        Ok(parsed) => Some(parsed),
 96                        Err(e) => {
 97                            state
 98                                .internal_configs
 99                                .insert(rel_path.clone(), (content.to_owned(), None));
100                            return Err(InvalidSettingsError::Editorconfig {
101                                message: e.to_string(),
102                                path: LocalSettingsPath::InWorktree(
103                                    rel_path.join(RelPath::unix(EDITORCONFIG_NAME).unwrap()),
104                                ),
105                            });
106                        }
107                    };
108                    state
109                        .internal_configs
110                        .insert(rel_path.clone(), (content.to_owned(), parsed));
111                }
112            }
113            (LocalSettingsPath::OutsideWorktree(abs_path), Some(content)) => {
114                let state = self.worktree_state.entry(worktree_id).or_default();
115                state.external_config_paths.insert(abs_path.clone());
116                let should_update = self
117                    .external_configs
118                    .get(abs_path)
119                    .map_or(true, |entry| entry.0 != content);
120                if should_update {
121                    let parsed = match content.parse::<Editorconfig>() {
122                        Ok(parsed) => Some(parsed),
123                        Err(e) => {
124                            self.external_configs
125                                .insert(abs_path.clone(), (content.to_owned(), None));
126                            return Err(InvalidSettingsError::Editorconfig {
127                                message: e.to_string(),
128                                path: LocalSettingsPath::OutsideWorktree(
129                                    abs_path.join(EDITORCONFIG_NAME).into(),
130                                ),
131                            });
132                        }
133                    };
134                    self.external_configs
135                        .insert(abs_path.clone(), (content.to_owned(), parsed));
136                }
137            }
138        }
139        Ok(())
140    }
141
142    pub(crate) fn remove_for_worktree(&mut self, root_id: WorktreeId) {
143        self.local_external_config_discovery_tasks.remove(&root_id);
144        let Some(removed) = self.worktree_state.remove(&root_id) else {
145            return;
146        };
147        let paths_in_use: HashSet<_> = self
148            .worktree_state
149            .values()
150            .flat_map(|w| w.external_config_paths.iter())
151            .collect();
152        for path in removed.external_config_paths.iter() {
153            if !paths_in_use.contains(path) {
154                self.external_configs.remove(path);
155                self.local_external_config_watchers.remove(path);
156            }
157        }
158    }
159
160    fn internal_configs(
161        &self,
162        root_id: WorktreeId,
163    ) -> impl '_ + Iterator<Item = (&RelPath, &str, Option<&Editorconfig>)> {
164        self.worktree_state
165            .get(&root_id)
166            .into_iter()
167            .flat_map(|state| {
168                state
169                    .internal_configs
170                    .iter()
171                    .map(|(path, data)| (path.as_ref(), data.0.as_str(), data.1.as_ref()))
172            })
173    }
174
175    fn external_configs(
176        &self,
177        worktree_id: WorktreeId,
178    ) -> impl '_ + Iterator<Item = (&Path, &str, Option<&Editorconfig>)> {
179        self.worktree_state
180            .get(&worktree_id)
181            .into_iter()
182            .flat_map(|state| {
183                state.external_config_paths.iter().filter_map(|path| {
184                    self.external_configs
185                        .get(path)
186                        .map(|entry| (path.as_ref(), entry.0.as_str(), entry.1.as_ref()))
187                })
188            })
189    }
190
191    pub fn local_editorconfig_settings(
192        &self,
193        worktree_id: WorktreeId,
194    ) -> impl '_ + Iterator<Item = (LocalSettingsPath, &str, Option<&Editorconfig>)> {
195        let external = self
196            .external_configs(worktree_id)
197            .map(|(path, content, parsed)| {
198                (
199                    LocalSettingsPath::OutsideWorktree(path.into()),
200                    content,
201                    parsed,
202                )
203            });
204        let internal = self
205            .internal_configs(worktree_id)
206            .map(|(path, content, parsed)| {
207                (LocalSettingsPath::InWorktree(path.into()), content, parsed)
208            });
209        external.chain(internal)
210    }
211
212    pub fn discover_local_external_configs_chain(
213        &mut self,
214        worktree_id: WorktreeId,
215        worktree_path: Arc<Path>,
216        fs: Arc<dyn Fs>,
217        cx: &mut Context<Self>,
218    ) {
219        // We should only have one discovery task per worktree.
220        if self
221            .local_external_config_discovery_tasks
222            .contains_key(&worktree_id)
223        {
224            return;
225        }
226
227        let task = cx.spawn({
228            let fs = fs.clone();
229            async move |this, cx| {
230                let discovered_paths = {
231                    let mut paths = Vec::new();
232                    let mut current = worktree_path.parent().map(|p| p.to_path_buf());
233                    while let Some(dir) = current {
234                        let dir_path: Arc<Path> = Arc::from(dir.as_path());
235                        let path = dir.join(EDITORCONFIG_NAME);
236                        if fs.load(&path).await.is_ok() {
237                            paths.push(dir_path);
238                        }
239                        current = dir.parent().map(|p| p.to_path_buf());
240                    }
241                    paths
242                };
243
244                this.update(cx, |this, cx| {
245                    for dir_path in discovered_paths {
246                        // We insert it here so that watchers can send events to appropriate worktrees.
247                        // external_config_paths gets populated again in set_configs.
248                        this.worktree_state
249                            .entry(worktree_id)
250                            .or_default()
251                            .external_config_paths
252                            .insert(dir_path.clone());
253                        match this.local_external_config_watchers.entry(dir_path.clone()) {
254                            std::collections::btree_map::Entry::Occupied(_) => {
255                                if let Some(existing_config) = this.external_configs.get(&dir_path)
256                                {
257                                    cx.emit(EditorconfigEvent::ExternalConfigChanged {
258                                        path: LocalSettingsPath::OutsideWorktree(dir_path),
259                                        content: Some(existing_config.0.clone()),
260                                        affected_worktree_ids: vec![worktree_id],
261                                    });
262                                } else {
263                                    log::error!("Watcher exists for {dir_path:?} but no config found in external_configs");
264                                }
265                            }
266                            std::collections::btree_map::Entry::Vacant(entry) => {
267                                let watcher =
268                                    Self::watch_local_external_config(fs.clone(), dir_path, cx);
269                                entry.insert(watcher);
270                            }
271                        }
272                    }
273                })
274                .ok();
275            }
276        });
277
278        self.local_external_config_discovery_tasks
279            .insert(worktree_id, task);
280    }
281
282    fn watch_local_external_config(
283        fs: Arc<dyn Fs>,
284        dir_path: Arc<Path>,
285        cx: &mut Context<Self>,
286    ) -> Task<()> {
287        let config_path = dir_path.join(EDITORCONFIG_NAME);
288        let (mut config_rx, watcher_task) =
289            watch_config_file(cx.background_executor(), fs, config_path);
290
291        cx.spawn(async move |this, cx| {
292            let _watcher_task = watcher_task;
293            while let Some(content) = config_rx.next().await {
294                let content = Some(content).filter(|c| !c.is_empty());
295                let dir_path = dir_path.clone();
296                this.update(cx, |this, cx| {
297                    let affected_worktree_ids: Vec<WorktreeId> = this
298                        .worktree_state
299                        .iter()
300                        .filter_map(|(id, state)| {
301                            state
302                                .external_config_paths
303                                .contains(&dir_path)
304                                .then_some(*id)
305                        })
306                        .collect();
307
308                    cx.emit(EditorconfigEvent::ExternalConfigChanged {
309                        path: LocalSettingsPath::OutsideWorktree(dir_path),
310                        content,
311                        affected_worktree_ids,
312                    });
313                })
314                .ok();
315            }
316        })
317    }
318
319    pub fn properties(
320        &self,
321        for_worktree: WorktreeId,
322        for_path: &RelPath,
323    ) -> Option<EditorconfigProperties> {
324        let mut properties = EditorconfigProperties::new();
325        let state = self.worktree_state.get(&for_worktree);
326        let internal_root_config_is_root = state
327            .and_then(|state| state.internal_configs.get(RelPath::empty()))
328            .and_then(|data| data.1.as_ref())
329            .is_some_and(|ec| ec.is_root);
330
331        let std_path = for_path.as_std_path();
332
333        if !internal_root_config_is_root {
334            for (_, _, parsed_editorconfig) in self.external_configs(for_worktree) {
335                if let Some(parsed_editorconfig) = parsed_editorconfig {
336                    if parsed_editorconfig.is_root {
337                        properties = EditorconfigProperties::new();
338                    }
339                    for section in &parsed_editorconfig.sections {
340                        section.apply_to(&mut properties, std_path).log_err()?;
341                    }
342                }
343            }
344        }
345
346        if let Some(state) = state {
347            let mut internal_configs: SmallVec<[&Editorconfig; 8]> = SmallVec::new();
348
349            for ancestor in for_path.ancestors() {
350                if let Some((_, parsed)) = state.internal_configs.get(ancestor) {
351                    let config = parsed.as_ref()?;
352                    internal_configs.push(config);
353                    if config.is_root {
354                        break;
355                    }
356                }
357            }
358
359            for config in internal_configs.into_iter().rev() {
360                if config.is_root {
361                    properties = EditorconfigProperties::new();
362                }
363                for section in &config.sections {
364                    section.apply_to(&mut properties, std_path).log_err()?;
365                }
366            }
367        }
368
369        properties.use_fallbacks();
370        Some(properties)
371    }
372}
373
374#[cfg(any(test, feature = "test-support"))]
375impl EditorconfigStore {
376    pub fn test_state(&self) -> (Vec<WorktreeId>, Vec<Arc<Path>>, Vec<Arc<Path>>) {
377        let worktree_ids: Vec<_> = self.worktree_state.keys().copied().collect();
378        let external_paths: Vec<_> = self.external_configs.keys().cloned().collect();
379        let watcher_paths: Vec<_> = self
380            .local_external_config_watchers
381            .keys()
382            .cloned()
383            .collect();
384        (worktree_ids, external_paths, watcher_paths)
385    }
386
387    pub fn external_config_paths_for_worktree(&self, worktree_id: WorktreeId) -> Vec<Arc<Path>> {
388        self.worktree_state
389            .get(&worktree_id)
390            .map(|state| state.external_config_paths.iter().cloned().collect())
391            .unwrap_or_default()
392    }
393}