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 empty_path: Arc<RelPath> = RelPath::empty().into();
327 let internal_root_config_is_root = state
328 .and_then(|state| state.internal_configs.get(&empty_path))
329 .and_then(|data| data.1.as_ref())
330 .is_some_and(|ec| ec.is_root);
331
332 if !internal_root_config_is_root {
333 for (_, _, parsed_editorconfig) in self.external_configs(for_worktree) {
334 if let Some(parsed_editorconfig) = parsed_editorconfig {
335 if parsed_editorconfig.is_root {
336 properties = EditorconfigProperties::new();
337 }
338 for section in &parsed_editorconfig.sections {
339 section
340 .apply_to(&mut properties, for_path.as_std_path())
341 .log_err()?;
342 }
343 }
344 }
345 }
346
347 for (directory_with_config, _, parsed_editorconfig) in self.internal_configs(for_worktree) {
348 if !for_path.starts_with(directory_with_config) {
349 properties.use_fallbacks();
350 return Some(properties);
351 }
352 let parsed_editorconfig = parsed_editorconfig?;
353 if parsed_editorconfig.is_root {
354 properties = EditorconfigProperties::new();
355 }
356 for section in &parsed_editorconfig.sections {
357 section
358 .apply_to(&mut properties, for_path.as_std_path())
359 .log_err()?;
360 }
361 }
362
363 properties.use_fallbacks();
364 Some(properties)
365 }
366}
367
368#[cfg(any(test, feature = "test-support"))]
369impl EditorconfigStore {
370 pub fn test_state(&self) -> (Vec<WorktreeId>, Vec<Arc<Path>>, Vec<Arc<Path>>) {
371 let worktree_ids: Vec<_> = self.worktree_state.keys().copied().collect();
372 let external_paths: Vec<_> = self.external_configs.keys().cloned().collect();
373 let watcher_paths: Vec<_> = self
374 .local_external_config_watchers
375 .keys()
376 .cloned()
377 .collect();
378 (worktree_ids, external_paths, watcher_paths)
379 }
380
381 pub fn external_config_paths_for_worktree(&self, worktree_id: WorktreeId) -> Vec<Arc<Path>> {
382 self.worktree_state
383 .get(&worktree_id)
384 .map(|state| state.external_config_paths.iter().cloned().collect())
385 .unwrap_or_default()
386 }
387}