Skip to repository content396 lines · 15.4 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:08:17.052Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
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
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