Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:59:24.788Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

editorconfig_store.rs

396 lines · 15.4 KB · rust
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
104                                        .join(RelPath::from_unix_str(EDITORCONFIG_NAME).unwrap())
105                                        .into(),
106                                ),
107                            });
108                        }
109                    };
110                    state
111                        .internal_configs
112                        .insert(rel_path.clone(), (content.to_owned(), parsed));
113                }
114            }
115            (LocalSettingsPath::OutsideWorktree(abs_path), Some(content)) => {
116                let state = self.worktree_state.entry(worktree_id).or_default();
117                state.external_config_paths.insert(abs_path.clone());
118                let should_update = self
119                    .external_configs
120                    .get(abs_path)
121                    .map_or(true, |entry| entry.0 != content);
122                if should_update {
123                    let parsed = match content.parse::<Editorconfig>() {
124                        Ok(parsed) => Some(parsed),
125                        Err(e) => {
126                            self.external_configs
127                                .insert(abs_path.clone(), (content.to_owned(), None));
128                            return Err(InvalidSettingsError::Editorconfig {
129                                message: e.to_string(),
130                                path: LocalSettingsPath::OutsideWorktree(
131                                    abs_path.join(EDITORCONFIG_NAME).into(),
132                                ),
133                            });
134                        }
135                    };
136                    self.external_configs
137                        .insert(abs_path.clone(), (content.to_owned(), parsed));
138                }
139            }
140        }
141        Ok(())
142    }
143
144    pub(crate) fn remove_for_worktree(&mut self, root_id: WorktreeId) {
145        self.local_external_config_discovery_tasks.remove(&root_id);
146        let Some(removed) = self.worktree_state.remove(&root_id) else {
147            return;
148        };
149        let paths_in_use: HashSet<_> = self
150            .worktree_state
151            .values()
152            .flat_map(|w| w.external_config_paths.iter())
153            .collect();
154        for path in removed.external_config_paths.iter() {
155            if !paths_in_use.contains(path) {
156                self.external_configs.remove(path);
157                self.local_external_config_watchers.remove(path);
158            }
159        }
160    }
161
162    fn internal_configs(
163        &self,
164        root_id: WorktreeId,
165    ) -> impl '_ + Iterator<Item = (&RelPath, &str, Option<&Editorconfig>)> {
166        self.worktree_state
167            .get(&root_id)
168            .into_iter()
169            .flat_map(|state| {
170                state
171                    .internal_configs
172                    .iter()
173                    .map(|(path, data)| (path.as_ref(), data.0.as_str(), data.1.as_ref()))
174            })
175    }
176
177    fn external_configs(
178        &self,
179        worktree_id: WorktreeId,
180    ) -> impl '_ + Iterator<Item = (&Path, &str, Option<&Editorconfig>)> {
181        self.worktree_state
182            .get(&worktree_id)
183            .into_iter()
184            .flat_map(|state| {
185                state.external_config_paths.iter().filter_map(|path| {
186                    self.external_configs
187                        .get(path)
188                        .map(|entry| (path.as_ref(), entry.0.as_str(), entry.1.as_ref()))
189                })
190            })
191    }
192
193    pub fn local_editorconfig_settings(
194        &self,
195        worktree_id: WorktreeId,
196    ) -> impl '_ + Iterator<Item = (LocalSettingsPath, &str, Option<&Editorconfig>)> {
197        let external = self
198            .external_configs(worktree_id)
199            .map(|(path, content, parsed)| {
200                (
201                    LocalSettingsPath::OutsideWorktree(path.into()),
202                    content,
203                    parsed,
204                )
205            });
206        let internal = self
207            .internal_configs(worktree_id)
208            .map(|(path, content, parsed)| {
209                (LocalSettingsPath::InWorktree(path.into()), content, parsed)
210            });
211        external.chain(internal)
212    }
213
214    pub fn discover_local_external_configs_chain(
215        &mut self,
216        worktree_id: WorktreeId,
217        worktree_path: Arc<Path>,
218        fs: Arc<dyn Fs>,
219        cx: &mut Context<Self>,
220    ) {
221        // We should only have one discovery task per worktree.
222        if self
223            .local_external_config_discovery_tasks
224            .contains_key(&worktree_id)
225        {
226            return;
227        }
228
229        let task = cx.spawn({
230            let fs = fs.clone();
231            async move |this, cx| {
232                let discovered_paths = {
233                    let mut paths = Vec::new();
234                    let mut current = worktree_path.parent().map(|p| p.to_path_buf());
235                    while let Some(dir) = current {
236                        let dir_path: Arc<Path> = Arc::from(dir.as_path());
237                        let path = dir.join(EDITORCONFIG_NAME);
238                        if fs.load(&path).await.is_ok() {
239                            paths.push(dir_path);
240                        }
241                        current = dir.parent().map(|p| p.to_path_buf());
242                    }
243                    paths
244                };
245
246                this.update(cx, |this, cx| {
247                    for dir_path in discovered_paths {
248                        // We insert it here so that watchers can send events to appropriate worktrees.
249                        // external_config_paths gets populated again in set_configs.
250                        this.worktree_state
251                            .entry(worktree_id)
252                            .or_default()
253                            .external_config_paths
254                            .insert(dir_path.clone());
255                        match this.local_external_config_watchers.entry(dir_path.clone()) {
256                            std::collections::btree_map::Entry::Occupied(_) => {
257                                if let Some(existing_config) = this.external_configs.get(&dir_path)
258                                {
259                                    cx.emit(EditorconfigEvent::ExternalConfigChanged {
260                                        path: LocalSettingsPath::OutsideWorktree(dir_path),
261                                        content: Some(existing_config.0.clone()),
262                                        affected_worktree_ids: vec![worktree_id],
263                                    });
264                                } else {
265                                    log::error!("Watcher exists for {dir_path:?} but no config found in external_configs");
266                                }
267                            }
268                            std::collections::btree_map::Entry::Vacant(entry) => {
269                                let watcher =
270                                    Self::watch_local_external_config(fs.clone(), dir_path, cx);
271                                entry.insert(watcher);
272                            }
273                        }
274                    }
275                })
276                .ok();
277            }
278        });
279
280        self.local_external_config_discovery_tasks
281            .insert(worktree_id, task);
282    }
283
284    fn watch_local_external_config(
285        fs: Arc<dyn Fs>,
286        dir_path: Arc<Path>,
287        cx: &mut Context<Self>,
288    ) -> Task<()> {
289        let config_path = dir_path.join(EDITORCONFIG_NAME);
290        let (mut config_rx, watcher_task) =
291            watch_config_file(cx.background_executor(), fs, config_path);
292
293        cx.spawn(async move |this, cx| {
294            let _watcher_task = watcher_task;
295            while let Some(content) = config_rx.next().await {
296                let content = Some(content).filter(|c| !c.is_empty());
297                let dir_path = dir_path.clone();
298                this.update(cx, |this, cx| {
299                    let affected_worktree_ids: Vec<WorktreeId> = this
300                        .worktree_state
301                        .iter()
302                        .filter_map(|(id, state)| {
303                            state
304                                .external_config_paths
305                                .contains(&dir_path)
306                                .then_some(*id)
307                        })
308                        .collect();
309
310                    cx.emit(EditorconfigEvent::ExternalConfigChanged {
311                        path: LocalSettingsPath::OutsideWorktree(dir_path),
312                        content,
313                        affected_worktree_ids,
314                    });
315                })
316                .ok();
317            }
318        })
319    }
320
321    pub fn properties(
322        &self,
323        for_worktree: WorktreeId,
324        for_path: &RelPath,
325    ) -> Option<EditorconfigProperties> {
326        let mut properties = EditorconfigProperties::new();
327        let state = self.worktree_state.get(&for_worktree);
328        let internal_root_config_is_root = state
329            .and_then(|state| state.internal_configs.get(RelPath::empty()))
330            .and_then(|data| data.1.as_ref())
331            .is_some_and(|ec| ec.is_root);
332
333        let std_path = for_path.as_std_path();
334
335        if !internal_root_config_is_root {
336            for (_, _, parsed_editorconfig) in self.external_configs(for_worktree) {
337                if let Some(parsed_editorconfig) = parsed_editorconfig {
338                    if parsed_editorconfig.is_root {
339                        properties = EditorconfigProperties::new();
340                    }
341                    for section in &parsed_editorconfig.sections {
342                        section.apply_to(&mut properties, std_path).log_err()?;
343                    }
344                }
345            }
346        }
347
348        if let Some(state) = state {
349            let mut internal_configs: SmallVec<[&Editorconfig; 8]> = SmallVec::new();
350
351            for ancestor in for_path.ancestors() {
352                if let Some((_, parsed)) = state.internal_configs.get(ancestor) {
353                    let config = parsed.as_ref()?;
354                    internal_configs.push(config);
355                    if config.is_root {
356                        break;
357                    }
358                }
359            }
360
361            for config in internal_configs.into_iter().rev() {
362                if config.is_root {
363                    properties = EditorconfigProperties::new();
364                }
365                for section in &config.sections {
366                    section.apply_to(&mut properties, std_path).log_err()?;
367                }
368            }
369        }
370
371        properties.use_fallbacks();
372        Some(properties)
373    }
374}
375
376#[cfg(any(test, feature = "test-support"))]
377impl EditorconfigStore {
378    pub fn test_state(&self) -> (Vec<WorktreeId>, Vec<Arc<Path>>, Vec<Arc<Path>>) {
379        let worktree_ids: Vec<_> = self.worktree_state.keys().copied().collect();
380        let external_paths: Vec<_> = self.external_configs.keys().cloned().collect();
381        let watcher_paths: Vec<_> = self
382            .local_external_config_watchers
383            .keys()
384            .cloned()
385            .collect();
386        (worktree_ids, external_paths, watcher_paths)
387    }
388
389    pub fn external_config_paths_for_worktree(&self, worktree_id: WorktreeId) -> Vec<Arc<Path>> {
390        self.worktree_state
391            .get(&worktree_id)
392            .map(|state| state.external_config_paths.iter().cloned().collect())
393            .unwrap_or_default()
394    }
395}
396
Served at tenant.openagents/omega Member data and write actions are omitted.