settings: Improve performance for internal editorconfig resolution (#48243)

Smit Barmase created

Replaces O(N) iteration over all internal configs with O(D × log N)
direct ancestor lookups, where D is path depth and N is total config
count.

Release Notes:

- N/A

Change summary

crates/project/tests/integration/project_tests.rs | 48 +++++++++++++++++
crates/settings/src/editorconfig_store.rs         | 42 ++++++++------
2 files changed, 71 insertions(+), 19 deletions(-)

Detailed changes

crates/project/tests/integration/project_tests.rs 🔗

@@ -349,6 +349,54 @@ async fn test_external_editorconfig_support(cx: &mut gpui::TestAppContext) {
     });
 }
 
+#[gpui::test]
+async fn test_internal_editorconfig_root_stops_traversal(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_tree(
+        path!("/worktree"),
+        json!({
+            ".editorconfig": "[*]\nindent_size = 99\n",
+            "src": {
+                ".editorconfig": "root = true\n[*]\nindent_size = 2\n",
+                "file.rs": "fn main() {}",
+            }
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs, [path!("/worktree").as_ref()], cx).await;
+
+    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+    language_registry.add(rust_lang());
+
+    let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
+
+    cx.executor().run_until_parked();
+
+    cx.update(|cx| {
+        let tree = worktree.read(cx);
+        let file_entry = tree
+            .entry_for_path(rel_path("src/file.rs"))
+            .unwrap()
+            .clone();
+        let file = File::for_entry(file_entry, worktree.clone());
+        let file_language = project
+            .read(cx)
+            .languages()
+            .load_language_for_file_path(file.path.as_std_path());
+        let file_language = cx
+            .foreground_executor()
+            .block_on(file_language)
+            .expect("Failed to get file language");
+        let file = file as _;
+        let settings = language_settings(Some(file_language.name()), Some(&file), cx).into_owned();
+
+        assert_eq!(Some(settings.tab_size), NonZeroU32::new(2));
+    });
+}
+
 #[gpui::test]
 async fn test_external_editorconfig_root_stops_traversal(cx: &mut gpui::TestAppContext) {
     init_test(cx);

crates/settings/src/editorconfig_store.rs 🔗

@@ -323,12 +323,13 @@ impl EditorconfigStore {
     ) -> Option<EditorconfigProperties> {
         let mut properties = EditorconfigProperties::new();
         let state = self.worktree_state.get(&for_worktree);
-        let empty_path: Arc<RelPath> = RelPath::empty().into();
         let internal_root_config_is_root = state
-            .and_then(|state| state.internal_configs.get(&empty_path))
+            .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 {
@@ -336,29 +337,32 @@ impl EditorconfigStore {
                         properties = EditorconfigProperties::new();
                     }
                     for section in &parsed_editorconfig.sections {
-                        section
-                            .apply_to(&mut properties, for_path.as_std_path())
-                            .log_err()?;
+                        section.apply_to(&mut properties, std_path).log_err()?;
                     }
                 }
             }
         }
 
-        for (directory_with_config, _, parsed_editorconfig) in self.internal_configs(for_worktree) {
-            if directory_with_config > for_path {
-                break;
-            }
-            if !for_path.starts_with(directory_with_config) {
-                continue;
-            }
-            let parsed_editorconfig = parsed_editorconfig?;
-            if parsed_editorconfig.is_root {
-                properties = EditorconfigProperties::new();
+        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 section in &parsed_editorconfig.sections {
-                section
-                    .apply_to(&mut properties, for_path.as_std_path())
-                    .log_err()?;
+
+            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()?;
+                }
             }
         }