Add a setting for custom associations between languages and files (#9290)

Max Brunsfeld and Marshall created

Closes #5178

Release Notes:

- Added a `file_types` setting that can be used to associate languages
with file names and file extensions. For example, to interpret all `.c`
files as C++, and files called `MyLockFile` as TOML, add the following
to `settings.json`:

    ```json
    {
      "file_types": {
        "C++": ["c"],
        "TOML": ["MyLockFile"]
      }
    }
    ```

As with most zed settings, this can be configured on a per-directory
basis by including a local `.zed/settings.json` file in that directory.

---------

Co-authored-by: Marshall <marshall@zed.dev>

Change summary

assets/settings/default.json                       |  13 
crates/assistant/src/assistant_panel.rs            |   8 
crates/collab/src/tests/test_server.rs             |   5 
crates/collab_ui/src/chat_panel.rs                 |  12 
crates/collab_ui/src/chat_panel/message_editor.rs  |   2 
crates/copilot_ui/src/copilot_button.rs            |  14 
crates/editor/src/editor.rs                        |  17 
crates/editor/src/editor_tests.rs                  |  31 -
crates/editor/src/hover_popover.rs                 |   7 
crates/editor/src/inlay_hint_cache.rs              |  12 
crates/editor/src/items.rs                         |  56 --
crates/editor/src/test/editor_test_context.rs      |  14 
crates/extension/src/extension_store_test.rs       |   2 
crates/language/src/buffer.rs                      |  49 ++
crates/language/src/buffer_tests.rs                | 127 +++++
crates/language/src/language.rs                    |  43 -
crates/language/src/language_registry.rs           | 337 +++++++++------
crates/language/src/language_settings.rs           |  29 +
crates/language/src/syntax_map/syntax_map_tests.rs |  77 ++-
crates/markdown_preview/src/markdown_parser.rs     |   4 
crates/project/src/project.rs                      |  95 ++-
crates/semantic_index/src/semantic_index.rs        |   4 
crates/semantic_index/src/semantic_index_tests.rs  |   6 
crates/settings/src/settings.rs                    |   2 
crates/settings/src/settings_store.rs              |  39 +
crates/workspace/src/workspace.rs                  |   2 
crates/worktree/src/worktree.rs                    |  12 
crates/zed/src/main.rs                             |   4 
crates/zed/src/zed.rs                              |   9 
docs/src/configuring_zed.md                        |  19 
30 files changed, 638 insertions(+), 413 deletions(-)

Detailed changes

assets/settings/default.json πŸ”—

@@ -532,6 +532,19 @@
     "enable": false
   },
   "code_actions_on_format": {},
+  // An object whose keys are language names, and whose values
+  // are arrays of filenames or extensions of files that should
+  // use those languages.
+  //
+  // For example, to treat files like `foo.notjs` as JavaScript,
+  // and 'Embargo.lock' as TOML:
+  //
+  // {
+  //   "JavaScript": ["notjs"],
+  //   "TOML": ["Embargo.lock"]
+  // }
+  //
+  "file_types": {},
   // Different settings for specific languages.
   "languages": {
     "Plain Text": {

crates/assistant/src/assistant_panel.rs πŸ”—

@@ -3268,7 +3268,7 @@ mod tests {
         let settings_store = SettingsStore::test(cx);
         cx.set_global(settings_store);
         init(cx);
-        let registry = Arc::new(LanguageRegistry::test());
+        let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
 
         let completion_provider = Arc::new(FakeCompletionProvider::new());
         let conversation = cx.new_model(|cx| Conversation::new(registry, cx, completion_provider));
@@ -3399,7 +3399,7 @@ mod tests {
         let settings_store = SettingsStore::test(cx);
         cx.set_global(settings_store);
         init(cx);
-        let registry = Arc::new(LanguageRegistry::test());
+        let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
         let completion_provider = Arc::new(FakeCompletionProvider::new());
 
         let conversation = cx.new_model(|cx| Conversation::new(registry, cx, completion_provider));
@@ -3498,7 +3498,7 @@ mod tests {
         let settings_store = SettingsStore::test(cx);
         cx.set_global(settings_store);
         init(cx);
-        let registry = Arc::new(LanguageRegistry::test());
+        let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
         let completion_provider = Arc::new(FakeCompletionProvider::new());
         let conversation = cx.new_model(|cx| Conversation::new(registry, cx, completion_provider));
         let buffer = conversation.read(cx).buffer.clone();
@@ -3582,7 +3582,7 @@ mod tests {
         let settings_store = cx.update(SettingsStore::test);
         cx.set_global(settings_store);
         cx.update(init);
-        let registry = Arc::new(LanguageRegistry::test());
+        let registry = Arc::new(LanguageRegistry::test(cx.executor()));
         let completion_provider = Arc::new(FakeCompletionProvider::new());
         let conversation =
             cx.new_model(|cx| Conversation::new(registry.clone(), cx, completion_provider));

crates/collab/src/tests/test_server.rs πŸ”—

@@ -257,13 +257,12 @@ impl TestServer {
         let fs = FakeFs::new(cx.executor());
         let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
         let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
-        let mut language_registry = LanguageRegistry::test();
-        language_registry.set_executor(cx.executor());
+        let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
         let app_state = Arc::new(workspace::AppState {
             client: client.clone(),
             user_store: user_store.clone(),
             workspace_store,
-            languages: Arc::new(language_registry),
+            languages: language_registry,
             fs: fs.clone(),
             build_window_options: |_, _| Default::default(),
             node_runtime: FakeNodeRuntime::new(),

crates/collab_ui/src/chat_panel.rs πŸ”—

@@ -1042,8 +1042,8 @@ mod tests {
     use util::test::marked_text_ranges;
 
     #[gpui::test]
-    fn test_render_markdown_with_mentions() {
-        let language_registry = Arc::new(LanguageRegistry::test());
+    fn test_render_markdown_with_mentions(cx: &mut AppContext) {
+        let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
         let (body, ranges) = marked_text_ranges("*hi*, Β«@abcΒ», let's **call** Β«@fghΒ»", false);
         let message = channel::ChannelMessage {
             id: ChannelMessageId::Saved(0),
@@ -1090,8 +1090,8 @@ mod tests {
     }
 
     #[gpui::test]
-    fn test_render_markdown_with_auto_detect_links() {
-        let language_registry = Arc::new(LanguageRegistry::test());
+    fn test_render_markdown_with_auto_detect_links(cx: &mut AppContext) {
+        let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
         let message = channel::ChannelMessage {
             id: ChannelMessageId::Saved(0),
             body: "Here is a link https://zed.dev to zeds website".to_string(),
@@ -1130,8 +1130,8 @@ mod tests {
     }
 
     #[gpui::test]
-    fn test_render_markdown_with_auto_detect_links_and_additional_formatting() {
-        let language_registry = Arc::new(LanguageRegistry::test());
+    fn test_render_markdown_with_auto_detect_links_and_additional_formatting(cx: &mut AppContext) {
+        let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
         let message = channel::ChannelMessage {
             id: ChannelMessageId::Saved(0),
             body: "**Here is a link https://zed.dev to zeds website**".to_string(),

crates/collab_ui/src/chat_panel/message_editor.rs πŸ”—

@@ -624,7 +624,7 @@ mod tests {
             MessageEditorSettings::register(cx);
         });
 
-        let language_registry = Arc::new(LanguageRegistry::test());
+        let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
         language_registry.add(Arc::new(Language::new(
             LanguageConfig {
                 name: "Markdown".into(),

crates/copilot_ui/src/copilot_button.rs πŸ”—

@@ -225,12 +225,14 @@ impl CopilotButton {
         let suggestion_anchor = editor.selections.newest_anchor().start;
         let language = snapshot.language_at(suggestion_anchor);
         let file = snapshot.file_at(suggestion_anchor).cloned();
-
-        self.editor_enabled = Some(
-            file.as_ref().map(|file| !file.is_private()).unwrap_or(true)
-                && all_language_settings(self.file.as_ref(), cx)
-                    .copilot_enabled(language, file.as_ref().map(|file| file.path().as_ref())),
-        );
+        self.editor_enabled = {
+            let file = file.as_ref();
+            Some(
+                file.map(|file| !file.is_private()).unwrap_or(true)
+                    && all_language_settings(file, cx)
+                        .copilot_enabled(language, file.map(|file| file.path().as_ref())),
+            )
+        };
         self.language = language.cloned();
         self.file = file;
 

crates/editor/src/editor.rs πŸ”—

@@ -9302,8 +9302,7 @@ impl Editor {
             .redacted_ranges(search_range, |file| {
                 if let Some(file) = file {
                     file.is_private()
-                        && EditorSettings::get(Some((file.worktree_id(), file.path())), cx)
-                            .redact_private_values
+                        && EditorSettings::get(Some(file.as_ref().into()), cx).redact_private_values
                 } else {
                     false
                 }
@@ -9645,22 +9644,16 @@ impl Editor {
         telemetry.report_copilot_event(suggestion_id, suggestion_accepted, file_extension)
     }
 
-    #[cfg(any(test, feature = "test-support"))]
-    fn report_editor_event(
-        &self,
-        _operation: &'static str,
-        _file_extension: Option<String>,
-        _cx: &AppContext,
-    ) {
-    }
-
-    #[cfg(not(any(test, feature = "test-support")))]
     fn report_editor_event(
         &self,
         operation: &'static str,
         file_extension: Option<String>,
         cx: &AppContext,
     ) {
+        if cfg!(any(test, feature = "test-support")) {
+            return;
+        }
+
         let Some(project) = &self.project else { return };
 
         // If None, we are in a file without an extension

crates/editor/src/editor_tests.rs πŸ”—

@@ -15,8 +15,7 @@ use language::{
     language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
     BracketPairConfig,
     Capability::ReadWrite,
-    FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageRegistry,
-    Override, Point,
+    FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, Override, Point,
 };
 use parking_lot::Mutex;
 use project::project_settings::{LspSettings, ProjectSettings};
@@ -4447,10 +4446,8 @@ async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
         Some(tree_sitter_rust::language()),
     ));
 
-    let registry = Arc::new(LanguageRegistry::test());
-    registry.add(language.clone());
+    cx.language_registry().add(language.clone());
     cx.update_buffer(|buffer, cx| {
-        buffer.set_language_registry(registry);
         buffer.set_language(Some(language), cx);
     });
 
@@ -4649,12 +4646,10 @@ async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
         Some(tree_sitter_typescript::language_tsx()),
     ));
 
-    let registry = Arc::new(LanguageRegistry::test());
-    registry.add(html_language.clone());
-    registry.add(javascript_language.clone());
+    cx.language_registry().add(html_language.clone());
+    cx.language_registry().add(javascript_language.clone());
 
     cx.update_buffer(|buffer, cx| {
-        buffer.set_language_registry(registry);
         buffer.set_language(Some(html_language), cx);
     });
 
@@ -4829,11 +4824,8 @@ async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) {
         .unwrap(),
     );
 
-    let registry = Arc::new(LanguageRegistry::test());
-    registry.add(rust_language.clone());
-
+    cx.language_registry().add(rust_language.clone());
     cx.update_buffer(|buffer, cx| {
-        buffer.set_language_registry(registry);
         buffer.set_language(Some(rust_language), cx);
     });
 
@@ -6139,12 +6131,10 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext)
         Some(tree_sitter_rust::language()),
     ));
 
-    let registry = Arc::new(LanguageRegistry::test());
-    registry.add(language.clone());
-
     let mut cx = EditorTestContext::new(cx).await;
+
+    cx.language_registry().add(language.clone());
     cx.update_buffer(|buffer, cx| {
-        buffer.set_language_registry(registry);
         buffer.set_language(Some(language), cx);
     });
 
@@ -6294,12 +6284,9 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
         Some(tree_sitter_typescript::language_tsx()),
     ));
 
-    let registry = Arc::new(LanguageRegistry::test());
-    registry.add(html_language.clone());
-    registry.add(javascript_language.clone());
-
+    cx.language_registry().add(html_language.clone());
+    cx.language_registry().add(javascript_language.clone());
     cx.update_buffer(|buffer, cx| {
-        buffer.set_language_registry(registry);
         buffer.set_language(Some(html_language), cx);
     });
 

crates/editor/src/hover_popover.rs πŸ”—

@@ -824,6 +824,8 @@ mod tests {
         .next()
         .await;
 
+        let languages = cx.language_registry().clone();
+
         cx.condition(|editor, _| editor.hover_state.visible()).await;
         cx.editor(|editor, _| {
             let blocks = editor.hover_state.info_popover.clone().unwrap().blocks;
@@ -835,7 +837,7 @@ mod tests {
                 }],
             );
 
-            let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
+            let rendered = smol::block_on(parse_blocks(&blocks, &languages, None));
             assert_eq!(
                 rendered.text,
                 code_str.trim(),
@@ -916,6 +918,7 @@ mod tests {
     fn test_render_blocks(cx: &mut gpui::TestAppContext) {
         init_test(cx, |_| {});
 
+        let languages = Arc::new(LanguageRegistry::test(cx.executor()));
         let editor = cx.add_window(|cx| Editor::single_line(cx));
         editor
             .update(cx, |editor, _cx| {
@@ -1028,7 +1031,7 @@ mod tests {
                     expected_styles,
                 } in &rows[0..]
                 {
-                    let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
+                    let rendered = smol::block_on(parse_blocks(&blocks, &languages, None));
 
                     let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
                     let expected_highlights = ranges

crates/editor/src/inlay_hint_cache.rs πŸ”—

@@ -3387,17 +3387,7 @@ pub mod tests {
         let project = Project::test(fs, ["/a".as_ref()], cx).await;
 
         let language_registry = project.read_with(cx, |project, _| project.languages().clone());
-        language_registry.add(Arc::new(Language::new(
-            LanguageConfig {
-                name: "Rust".into(),
-                matcher: LanguageMatcher {
-                    path_suffixes: vec!["rs".to_string()],
-                    ..Default::default()
-                },
-                ..Default::default()
-            },
-            Some(tree_sitter_rust::language()),
-        )));
+        language_registry.add(crate::editor_tests::rust_lang());
         let mut fake_servers = language_registry.register_fake_lsp_adapter(
             "Rust",
             FakeLspAdapter {

crates/editor/src/items.rs πŸ”—

@@ -1247,65 +1247,15 @@ fn path_for_file<'a>(
 mod tests {
     use super::*;
     use gpui::AppContext;
-    use std::{
-        path::{Path, PathBuf},
-        sync::Arc,
-        time::SystemTime,
-    };
+    use language::TestFile;
+    use std::path::Path;
 
     #[gpui::test]
     fn test_path_for_file(cx: &mut AppContext) {
         let file = TestFile {
             path: Path::new("").into(),
-            full_path: PathBuf::from(""),
+            root_name: String::new(),
         };
         assert_eq!(path_for_file(&file, 0, false, cx), None);
     }
-
-    struct TestFile {
-        path: Arc<Path>,
-        full_path: PathBuf,
-    }
-
-    impl language::File for TestFile {
-        fn path(&self) -> &Arc<Path> {
-            &self.path
-        }
-
-        fn full_path(&self, _: &gpui::AppContext) -> PathBuf {
-            self.full_path.clone()
-        }
-
-        fn as_local(&self) -> Option<&dyn language::LocalFile> {
-            unimplemented!()
-        }
-
-        fn mtime(&self) -> Option<SystemTime> {
-            unimplemented!()
-        }
-
-        fn file_name<'a>(&'a self, _: &'a gpui::AppContext) -> &'a std::ffi::OsStr {
-            unimplemented!()
-        }
-
-        fn worktree_id(&self) -> usize {
-            0
-        }
-
-        fn is_deleted(&self) -> bool {
-            unimplemented!()
-        }
-
-        fn as_any(&self) -> &dyn std::any::Any {
-            unimplemented!()
-        }
-
-        fn to_proto(&self) -> rpc::proto::File {
-            unimplemented!()
-        }
-
-        fn is_private(&self) -> bool {
-            false
-        }
-    }
 }

crates/editor/src/test/editor_test_context.rs πŸ”—

@@ -9,7 +9,7 @@ use gpui::{
 };
 use indoc::indoc;
 use itertools::Itertools;
-use language::{Buffer, BufferSnapshot};
+use language::{Buffer, BufferSnapshot, LanguageRegistry};
 use parking_lot::RwLock;
 use project::{FakeFs, Project};
 use std::{
@@ -120,6 +120,18 @@ impl EditorTestContext {
         })
     }
 
+    pub fn language_registry(&mut self) -> Arc<LanguageRegistry> {
+        self.editor(|editor, cx| {
+            editor
+                .project
+                .as_ref()
+                .unwrap()
+                .read(cx)
+                .languages()
+                .clone()
+        })
+    }
+
     pub fn update_buffer<F, T>(&mut self, update: F) -> T
     where
         F: FnOnce(&mut Buffer, &mut ModelContext<Buffer>) -> T,

crates/extension/src/extension_store_test.rs πŸ”—

@@ -249,7 +249,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
         .collect(),
     };
 
-    let language_registry = Arc::new(LanguageRegistry::test());
+    let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
     let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
     let node_runtime = FakeNodeRuntime::new();
 

crates/language/src/buffer.rs πŸ”—

@@ -3527,6 +3527,55 @@ impl Completion {
     }
 }
 
+#[cfg(any(test, feature = "test-support"))]
+pub struct TestFile {
+    pub path: Arc<Path>,
+    pub root_name: String,
+}
+
+#[cfg(any(test, feature = "test-support"))]
+impl File for TestFile {
+    fn path(&self) -> &Arc<Path> {
+        &self.path
+    }
+
+    fn full_path(&self, _: &gpui::AppContext) -> PathBuf {
+        PathBuf::from(&self.root_name).join(self.path.as_ref())
+    }
+
+    fn as_local(&self) -> Option<&dyn LocalFile> {
+        None
+    }
+
+    fn mtime(&self) -> Option<SystemTime> {
+        unimplemented!()
+    }
+
+    fn file_name<'a>(&'a self, _: &'a gpui::AppContext) -> &'a std::ffi::OsStr {
+        self.path().file_name().unwrap_or(self.root_name.as_ref())
+    }
+
+    fn worktree_id(&self) -> usize {
+        0
+    }
+
+    fn is_deleted(&self) -> bool {
+        unimplemented!()
+    }
+
+    fn as_any(&self) -> &dyn std::any::Any {
+        unimplemented!()
+    }
+
+    fn to_proto(&self) -> rpc::proto::File {
+        unimplemented!()
+    }
+
+    fn is_private(&self) -> bool {
+        false
+    }
+}
+
 pub(crate) fn contiguous_ranges(
     values: impl Iterator<Item = u32>,
     max_len: usize,

crates/language/src/buffer_tests.rs πŸ”—

@@ -69,8 +69,10 @@ fn test_line_endings(cx: &mut gpui::AppContext) {
 }
 
 #[gpui::test]
-fn test_select_language() {
-    let registry = Arc::new(LanguageRegistry::test());
+fn test_select_language(cx: &mut AppContext) {
+    init_settings(cx, |_| {});
+
+    let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
     registry.add(Arc::new(Language::new(
         LanguageConfig {
             name: "Rust".into(),
@@ -97,14 +99,14 @@ fn test_select_language() {
     // matching file extension
     assert_eq!(
         registry
-            .language_for_file("zed/lib.rs".as_ref(), None)
+            .language_for_file(&file("src/lib.rs"), None, cx)
             .now_or_never()
             .and_then(|l| Some(l.ok()?.name())),
         Some("Rust".into())
     );
     assert_eq!(
         registry
-            .language_for_file("zed/lib.mk".as_ref(), None)
+            .language_for_file(&file("src/lib.mk"), None, cx)
             .now_or_never()
             .and_then(|l| Some(l.ok()?.name())),
         Some("Make".into())
@@ -113,7 +115,7 @@ fn test_select_language() {
     // matching filename
     assert_eq!(
         registry
-            .language_for_file("zed/Makefile".as_ref(), None)
+            .language_for_file(&file("src/Makefile"), None, cx)
             .now_or_never()
             .and_then(|l| Some(l.ok()?.name())),
         Some("Make".into())
@@ -122,27 +124,132 @@ fn test_select_language() {
     // matching suffix that is not the full file extension or filename
     assert_eq!(
         registry
-            .language_for_file("zed/cars".as_ref(), None)
+            .language_for_file(&file("zed/cars"), None, cx)
             .now_or_never()
             .and_then(|l| Some(l.ok()?.name())),
         None
     );
     assert_eq!(
         registry
-            .language_for_file("zed/a.cars".as_ref(), None)
+            .language_for_file(&file("zed/a.cars"), None, cx)
             .now_or_never()
             .and_then(|l| Some(l.ok()?.name())),
         None
     );
     assert_eq!(
         registry
-            .language_for_file("zed/sumk".as_ref(), None)
+            .language_for_file(&file("zed/sumk"), None, cx)
             .now_or_never()
             .and_then(|l| Some(l.ok()?.name())),
         None
     );
 }
 
+#[gpui::test(iterations = 10)]
+async fn test_first_line_pattern(cx: &mut TestAppContext) {
+    cx.update(|cx| init_settings(cx, |_| {}));
+
+    let languages = LanguageRegistry::test(cx.executor());
+    let languages = Arc::new(languages);
+
+    languages.register_test_language(LanguageConfig {
+        name: "JavaScript".into(),
+        matcher: LanguageMatcher {
+            path_suffixes: vec!["js".into()],
+            first_line_pattern: Some(Regex::new(r"\bnode\b").unwrap()),
+        },
+        ..Default::default()
+    });
+
+    cx.read(|cx| languages.language_for_file(&file("the/script"), None, cx))
+        .await
+        .unwrap_err();
+    cx.read(|cx| languages.language_for_file(&file("the/script"), Some(&"nothing".into()), cx))
+        .await
+        .unwrap_err();
+    assert_eq!(
+        cx.read(|cx| languages.language_for_file(
+            &file("the/script"),
+            Some(&"#!/bin/env node".into()),
+            cx
+        ))
+        .await
+        .unwrap()
+        .name()
+        .as_ref(),
+        "JavaScript"
+    );
+}
+
+#[gpui::test]
+async fn test_language_for_file_with_custom_file_types(cx: &mut TestAppContext) {
+    cx.update(|cx| {
+        init_settings(cx, |settings| {
+            settings.file_types.extend([
+                ("TypeScript".into(), vec!["js".into()]),
+                ("C++".into(), vec!["c".into()]),
+            ]);
+        })
+    });
+
+    let languages = Arc::new(LanguageRegistry::test(cx.executor()));
+
+    for config in [
+        LanguageConfig {
+            name: "JavaScript".into(),
+            matcher: LanguageMatcher {
+                path_suffixes: vec!["js".to_string()],
+                ..Default::default()
+            },
+            ..Default::default()
+        },
+        LanguageConfig {
+            name: "TypeScript".into(),
+            matcher: LanguageMatcher {
+                path_suffixes: vec!["js".to_string()],
+                ..Default::default()
+            },
+            ..Default::default()
+        },
+        LanguageConfig {
+            name: "C++".into(),
+            matcher: LanguageMatcher {
+                path_suffixes: vec!["cpp".to_string()],
+                ..Default::default()
+            },
+            ..Default::default()
+        },
+        LanguageConfig {
+            name: "C".into(),
+            matcher: LanguageMatcher {
+                path_suffixes: vec!["c".to_string()],
+                ..Default::default()
+            },
+            ..Default::default()
+        },
+    ] {
+        languages.add(Arc::new(Language::new(config, None)));
+    }
+
+    let language = cx
+        .read(|cx| languages.language_for_file(&file("foo.js"), None, cx))
+        .await
+        .unwrap();
+    assert_eq!(language.name().as_ref(), "TypeScript");
+    let language = cx
+        .read(|cx| languages.language_for_file(&file("foo.c"), None, cx))
+        .await
+        .unwrap();
+    assert_eq!(language.name().as_ref(), "C++");
+}
+
+fn file(path: &str) -> Arc<dyn File> {
+    Arc::new(TestFile {
+        path: Path::new(path).into(),
+        root_name: "zed".into(),
+    })
+}
+
 #[gpui::test]
 fn test_edit_events(cx: &mut gpui::AppContext) {
     let mut now = Instant::now();
@@ -1575,7 +1682,7 @@ fn test_autoindent_with_injected_languages(cx: &mut AppContext) {
 
     let javascript_language = Arc::new(javascript_lang());
 
-    let language_registry = Arc::new(LanguageRegistry::test());
+    let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
     language_registry.add(html_language.clone());
     language_registry.add(javascript_language.clone());
 
@@ -1895,7 +2002,7 @@ fn test_language_scope_at_with_combined_injections(cx: &mut AppContext) {
         "#
         .unindent();
 
-        let language_registry = Arc::new(LanguageRegistry::test());
+        let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
         language_registry.add(Arc::new(ruby_lang()));
         language_registry.add(Arc::new(html_lang()));
         language_registry.add(Arc::new(erb_lang()));

crates/language/src/language.rs πŸ”—

@@ -852,11 +852,7 @@ struct BracketConfig {
 
 impl Language {
     pub fn new(config: LanguageConfig, ts_language: Option<tree_sitter::Language>) -> Self {
-        Self::new_with_id(
-            LanguageId(NEXT_LANGUAGE_ID.fetch_add(1, SeqCst)),
-            config,
-            ts_language,
-        )
+        Self::new_with_id(LanguageId::new(), config, ts_language)
     }
 
     fn new_with_id(
@@ -1569,44 +1565,9 @@ mod tests {
     use super::*;
     use gpui::TestAppContext;
 
-    #[gpui::test(iterations = 10)]
-    async fn test_first_line_pattern(cx: &mut TestAppContext) {
-        let mut languages = LanguageRegistry::test();
-
-        languages.set_executor(cx.executor());
-        let languages = Arc::new(languages);
-        languages.register_test_language(LanguageConfig {
-            name: "JavaScript".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["js".into()],
-                first_line_pattern: Some(Regex::new(r"\bnode\b").unwrap()),
-            },
-            ..Default::default()
-        });
-
-        languages
-            .language_for_file("the/script".as_ref(), None)
-            .await
-            .unwrap_err();
-        languages
-            .language_for_file("the/script".as_ref(), Some(&"nothing".into()))
-            .await
-            .unwrap_err();
-        assert_eq!(
-            languages
-                .language_for_file("the/script".as_ref(), Some(&"#!/bin/env node".into()))
-                .await
-                .unwrap()
-                .name()
-                .as_ref(),
-            "JavaScript"
-        );
-    }
-
     #[gpui::test(iterations = 10)]
     async fn test_language_loading(cx: &mut TestAppContext) {
-        let mut languages = LanguageRegistry::test();
-        languages.set_executor(cx.executor());
+        let languages = LanguageRegistry::test(cx.executor());
         let languages = Arc::new(languages);
         languages.register_native_grammars([
             ("json", tree_sitter_json::language()),

crates/language/src/language_registry.rs πŸ”—

@@ -1,6 +1,7 @@
 use crate::{
-    CachedLspAdapter, Language, LanguageConfig, LanguageContextProvider, LanguageId,
-    LanguageMatcher, LanguageServerName, LspAdapter, LspAdapterDelegate, PARSER, PLAIN_TEXT,
+    language_settings::all_language_settings, CachedLspAdapter, File, Language, LanguageConfig,
+    LanguageContextProvider, LanguageId, LanguageMatcher, LanguageServerName, LspAdapter,
+    LspAdapterDelegate, PARSER, PLAIN_TEXT,
 };
 use anyhow::{anyhow, Context as _, Result};
 use collections::{hash_map, HashMap};
@@ -10,7 +11,7 @@ use futures::{
     Future, FutureExt as _,
 };
 use gpui::{AppContext, BackgroundExecutor, Task};
-use lsp::{LanguageServerBinary, LanguageServerId};
+use lsp::LanguageServerId;
 use parking_lot::{Mutex, RwLock};
 use postage::watch;
 use std::{
@@ -30,11 +31,7 @@ pub struct LanguageRegistry {
     state: RwLock<LanguageRegistryState>,
     language_server_download_dir: Option<Arc<Path>>,
     login_shell_env_loaded: Shared<Task<()>>,
-    #[allow(clippy::type_complexity)]
-    lsp_binary_paths: Mutex<
-        HashMap<LanguageServerName, Shared<Task<Result<LanguageServerBinary, Arc<anyhow::Error>>>>>,
-    >,
-    executor: Option<BackgroundExecutor>,
+    executor: BackgroundExecutor,
     lsp_binary_status_tx: LspBinaryStatusSender,
 }
 
@@ -121,12 +118,12 @@ struct LspBinaryStatusSender {
 }
 
 impl LanguageRegistry {
-    pub fn new(login_shell_env_loaded: Task<()>) -> Self {
-        Self {
+    pub fn new(login_shell_env_loaded: Task<()>, executor: BackgroundExecutor) -> Self {
+        let this = Self {
             state: RwLock::new(LanguageRegistryState {
                 next_language_server_id: 0,
-                languages: vec![PLAIN_TEXT.clone()],
-                available_languages: Default::default(),
+                languages: Vec::new(),
+                available_languages: Vec::new(),
                 grammars: Default::default(),
                 loading_languages: Default::default(),
                 lsp_adapters: Default::default(),
@@ -140,23 +137,20 @@ impl LanguageRegistry {
             }),
             language_server_download_dir: None,
             login_shell_env_loaded: login_shell_env_loaded.shared(),
-            lsp_binary_paths: Default::default(),
-            executor: None,
             lsp_binary_status_tx: Default::default(),
-        }
+            executor,
+        };
+        this.add(PLAIN_TEXT.clone());
+        this
     }
 
     #[cfg(any(test, feature = "test-support"))]
-    pub fn test() -> Self {
-        let mut this = Self::new(Task::ready(()));
+    pub fn test(executor: BackgroundExecutor) -> Self {
+        let mut this = Self::new(Task::ready(()), executor);
         this.language_server_download_dir = Some(Path::new("/the-download-dir").into());
         this
     }
 
-    pub fn set_executor(&mut self, executor: BackgroundExecutor) {
-        self.executor = Some(executor);
-    }
-
     /// Clears out all of the loaded languages and reload them from scratch.
     pub fn reload(&self) {
         self.state.write().reload();
@@ -317,8 +311,19 @@ impl LanguageRegistry {
         result
     }
 
+    /// Add a pre-loaded language to the registry.
     pub fn add(&self, language: Arc<Language>) {
-        self.state.write().add(language);
+        let mut state = self.state.write();
+        state.available_languages.push(AvailableLanguage {
+            id: language.id,
+            name: language.name(),
+            grammar: language.config.grammar.clone(),
+            matcher: language.config.matcher.clone(),
+            load: Arc::new(|| Err(anyhow!("already loaded"))),
+            loaded: true,
+            context_provider: language.context_provider.clone(),
+        });
+        state.add(language);
     }
 
     pub fn subscribe(&self) -> watch::Receiver<()> {
@@ -353,7 +358,13 @@ impl LanguageRegistry {
         name: &str,
     ) -> impl Future<Output = Result<Arc<Language>>> {
         let name = UniCase::new(name);
-        let rx = self.get_or_load_language(|language_name, _| UniCase::new(language_name) == name);
+        let rx = self.get_or_load_language(|language_name, _| {
+            if UniCase::new(language_name) == name {
+                1
+            } else {
+                0
+            }
+        });
         async move { rx.await? }
     }
 
@@ -363,28 +374,62 @@ impl LanguageRegistry {
     ) -> impl Future<Output = Result<Arc<Language>>> {
         let string = UniCase::new(string);
         let rx = self.get_or_load_language(|name, config| {
-            UniCase::new(name) == string
+            if UniCase::new(name) == string
                 || config
                     .path_suffixes
                     .iter()
                     .any(|suffix| UniCase::new(suffix) == string)
+            {
+                1
+            } else {
+                0
+            }
         });
         async move { rx.await? }
     }
 
     pub fn language_for_file(
+        self: &Arc<Self>,
+        file: &Arc<dyn File>,
+        content: Option<&Rope>,
+        cx: &AppContext,
+    ) -> impl Future<Output = Result<Arc<Language>>> {
+        let user_file_types = all_language_settings(Some(file), cx);
+        self.language_for_file_internal(
+            &file.full_path(cx),
+            content,
+            Some(&user_file_types.file_types),
+        )
+    }
+
+    pub fn language_for_file_path(
+        self: &Arc<Self>,
+        path: &Path,
+    ) -> impl Future<Output = Result<Arc<Language>>> {
+        self.language_for_file_internal(path, None, None)
+    }
+
+    fn language_for_file_internal(
         self: &Arc<Self>,
         path: &Path,
         content: Option<&Rope>,
+        user_file_types: Option<&HashMap<Arc<str>, Vec<String>>>,
     ) -> impl Future<Output = Result<Arc<Language>>> {
         let filename = path.file_name().and_then(|name| name.to_str());
         let extension = path.extension_or_hidden_file_name();
         let path_suffixes = [extension, filename];
-        let rx = self.get_or_load_language(move |_, config| {
-            let path_matches = config
+        let empty = Vec::new();
+
+        let rx = self.get_or_load_language(move |language_name, config| {
+            let path_matches_default_suffix = config
                 .path_suffixes
                 .iter()
                 .any(|suffix| path_suffixes.contains(&Some(suffix.as_str())));
+            let path_matches_custom_suffix = user_file_types
+                .and_then(|types| types.get(language_name))
+                .unwrap_or(&empty)
+                .iter()
+                .any(|suffix| path_suffixes.contains(&Some(suffix.as_str())));
             let content_matches = content.zip(config.first_line_pattern.as_ref()).map_or(
                 false,
                 |(content, pattern)| {
@@ -394,93 +439,110 @@ impl LanguageRegistry {
                     pattern.is_match(&text)
                 },
             );
-            path_matches || content_matches
+            if path_matches_custom_suffix {
+                2
+            } else if path_matches_default_suffix || content_matches {
+                1
+            } else {
+                0
+            }
         });
         async move { rx.await? }
     }
 
     fn get_or_load_language(
         self: &Arc<Self>,
-        callback: impl Fn(&str, &LanguageMatcher) -> bool,
+        callback: impl Fn(&str, &LanguageMatcher) -> usize,
     ) -> oneshot::Receiver<Result<Arc<Language>>> {
         let (tx, rx) = oneshot::channel();
 
         let mut state = self.state.write();
-        if let Some(language) = state
-            .languages
+        let Some((language, _)) = state
+            .available_languages
             .iter()
-            .find(|language| callback(language.config.name.as_ref(), &language.config.matcher))
-        {
-            let _ = tx.send(Ok(language.clone()));
-        } else if let Some(executor) = self.executor.clone() {
-            if let Some(language) = state
-                .available_languages
-                .iter()
-                .rfind(|l| !l.loaded && callback(&l.name, &l.matcher))
-                .cloned()
-            {
-                match state.loading_languages.entry(language.id) {
-                    hash_map::Entry::Occupied(mut entry) => entry.get_mut().push(tx),
-                    hash_map::Entry::Vacant(entry) => {
-                        let this = self.clone();
-                        executor
-                            .spawn(async move {
-                                let id = language.id;
-                                let name = language.name.clone();
-                                let provider = language.context_provider.clone();
-                                let language = async {
-                                    let (config, queries) = (language.load)()?;
-
-                                    let grammar = if let Some(grammar) = config.grammar.clone() {
-                                        Some(this.get_or_load_grammar(grammar).await?)
-                                    } else {
-                                        None
-                                    };
-
-                                    Language::new_with_id(id, config, grammar)
-                                        .with_context_provider(provider)
-                                        .with_queries(queries)
-                                }
-                                .await;
-
-                                match language {
-                                    Ok(language) => {
-                                        let language = Arc::new(language);
-                                        let mut state = this.state.write();
-
-                                        state.add(language.clone());
-                                        state.mark_language_loaded(id);
-                                        if let Some(mut txs) = state.loading_languages.remove(&id) {
-                                            for tx in txs.drain(..) {
-                                                let _ = tx.send(Ok(language.clone()));
-                                            }
-                                        }
+            .filter_map(|language| {
+                let score = callback(&language.name, &language.matcher);
+                if score > 0 {
+                    Some((language.clone(), score))
+                } else {
+                    None
+                }
+            })
+            .max_by_key(|e| e.1)
+            .clone()
+        else {
+            let _ = tx.send(Err(anyhow!("language not found")));
+            return rx;
+        };
+
+        // If the language is already loaded, resolve with it immediately.
+        for loaded_language in state.languages.iter() {
+            if loaded_language.id == language.id {
+                let _ = tx.send(Ok(loaded_language.clone()));
+                return rx;
+            }
+        }
+
+        match state.loading_languages.entry(language.id) {
+            // If the language is already being loaded, then add this
+            // channel to a list that will be sent to when the load completes.
+            hash_map::Entry::Occupied(mut entry) => entry.get_mut().push(tx),
+
+            // Otherwise, start loading the language.
+            hash_map::Entry::Vacant(entry) => {
+                let this = self.clone();
+                self.executor
+                    .spawn(async move {
+                        let id = language.id;
+                        let name = language.name.clone();
+                        let provider = language.context_provider.clone();
+                        let language = async {
+                            let (config, queries) = (language.load)()?;
+
+                            let grammar = if let Some(grammar) = config.grammar.clone() {
+                                Some(this.get_or_load_grammar(grammar).await?)
+                            } else {
+                                None
+                            };
+
+                            Language::new_with_id(id, config, grammar)
+                                .with_context_provider(provider)
+                                .with_queries(queries)
+                        }
+                        .await;
+
+                        match language {
+                            Ok(language) => {
+                                let language = Arc::new(language);
+                                let mut state = this.state.write();
+
+                                state.add(language.clone());
+                                state.mark_language_loaded(id);
+                                if let Some(mut txs) = state.loading_languages.remove(&id) {
+                                    for tx in txs.drain(..) {
+                                        let _ = tx.send(Ok(language.clone()));
                                     }
-                                    Err(e) => {
-                                        log::error!("failed to load language {name}:\n{:?}", e);
-                                        let mut state = this.state.write();
-                                        state.mark_language_loaded(id);
-                                        if let Some(mut txs) = state.loading_languages.remove(&id) {
-                                            for tx in txs.drain(..) {
-                                                let _ = tx.send(Err(anyhow!(
-                                                    "failed to load language {}: {}",
-                                                    name,
-                                                    e
-                                                )));
-                                            }
-                                        }
+                                }
+                            }
+                            Err(e) => {
+                                log::error!("failed to load language {name}:\n{:?}", e);
+                                let mut state = this.state.write();
+                                state.mark_language_loaded(id);
+                                if let Some(mut txs) = state.loading_languages.remove(&id) {
+                                    for tx in txs.drain(..) {
+                                        let _ = tx.send(Err(anyhow!(
+                                            "failed to load language {}: {}",
+                                            name,
+                                            e
+                                        )));
                                     }
-                                };
-                            })
-                            .detach();
-                        entry.insert(vec![tx]);
-                    }
-                }
-            } else {
-                let _ = tx.send(Err(anyhow!("language not found")));
+                                }
+                            }
+                        };
+                    })
+                    .detach();
+                entry.insert(vec![tx]);
             }
-        } else {
-            let _ = tx.send(Err(anyhow!("executor does not exist")));
         }
 
         rx
@@ -502,43 +564,40 @@ impl LanguageRegistry {
                     txs.push(tx);
                 }
                 AvailableGrammar::Unloaded(wasm_path) => {
-                    if let Some(executor) = &self.executor {
-                        let this = self.clone();
-                        executor
-                            .spawn({
-                                let wasm_path = wasm_path.clone();
-                                async move {
-                                    let wasm_bytes = std::fs::read(&wasm_path)?;
-                                    let grammar_name = wasm_path
-                                        .file_stem()
-                                        .and_then(OsStr::to_str)
-                                        .ok_or_else(|| anyhow!("invalid grammar filename"))?;
-                                    let grammar = PARSER.with(|parser| {
-                                        let mut parser = parser.borrow_mut();
-                                        let mut store = parser.take_wasm_store().unwrap();
-                                        let grammar =
-                                            store.load_language(&grammar_name, &wasm_bytes);
-                                        parser.set_wasm_store(store).unwrap();
-                                        grammar
-                                    })?;
-
-                                    if let Some(AvailableGrammar::Loading(_, txs)) =
-                                        this.state.write().grammars.insert(
-                                            name,
-                                            AvailableGrammar::Loaded(wasm_path, grammar.clone()),
-                                        )
-                                    {
-                                        for tx in txs {
-                                            tx.send(Ok(grammar.clone())).ok();
-                                        }
+                    let this = self.clone();
+                    self.executor
+                        .spawn({
+                            let wasm_path = wasm_path.clone();
+                            async move {
+                                let wasm_bytes = std::fs::read(&wasm_path)?;
+                                let grammar_name = wasm_path
+                                    .file_stem()
+                                    .and_then(OsStr::to_str)
+                                    .ok_or_else(|| anyhow!("invalid grammar filename"))?;
+                                let grammar = PARSER.with(|parser| {
+                                    let mut parser = parser.borrow_mut();
+                                    let mut store = parser.take_wasm_store().unwrap();
+                                    let grammar = store.load_language(&grammar_name, &wasm_bytes);
+                                    parser.set_wasm_store(store).unwrap();
+                                    grammar
+                                })?;
+
+                                if let Some(AvailableGrammar::Loading(_, txs)) =
+                                    this.state.write().grammars.insert(
+                                        name,
+                                        AvailableGrammar::Loaded(wasm_path, grammar.clone()),
+                                    )
+                                {
+                                    for tx in txs {
+                                        tx.send(Ok(grammar.clone())).ok();
                                     }
-
-                                    anyhow::Ok(())
                                 }
-                            })
-                            .detach();
-                        *grammar = AvailableGrammar::Loading(wasm_path.clone(), vec![tx]);
-                    }
+
+                                anyhow::Ok(())
+                            }
+                        })
+                        .detach();
+                    *grammar = AvailableGrammar::Loading(wasm_path.clone(), vec![tx]);
                 }
             }
         } else {
@@ -694,9 +753,6 @@ impl LanguageRegistry {
     ) -> Task<()> {
         log::info!("deleting server container");
 
-        let mut lock = self.lsp_binary_paths.lock();
-        lock.remove(&adapter.name);
-
         let download_dir = self
             .language_server_download_dir
             .clone()
@@ -716,13 +772,6 @@ impl LanguageRegistry {
     }
 }
 
-#[cfg(any(test, feature = "test-support"))]
-impl Default for LanguageRegistry {
-    fn default() -> Self {
-        Self::test()
-    }
-}
-
 impl LanguageRegistryState {
     fn next_language_server_id(&mut self) -> LanguageServerId {
         LanguageServerId(post_inc(&mut self.next_language_server_id))

crates/language/src/language_settings.rs πŸ”—

@@ -10,9 +10,18 @@ use schemars::{
     JsonSchema,
 };
 use serde::{Deserialize, Serialize};
-use settings::Settings;
+use settings::{Settings, SettingsLocation};
 use std::{num::NonZeroU32, path::Path, sync::Arc};
 
+impl<'a> Into<SettingsLocation<'a>> for &'a dyn File {
+    fn into(self) -> SettingsLocation<'a> {
+        SettingsLocation {
+            worktree_id: self.worktree_id(),
+            path: self.path().as_ref(),
+        }
+    }
+}
+
 /// Initializes the language settings.
 pub fn init(cx: &mut AppContext) {
     AllLanguageSettings::register(cx);
@@ -33,7 +42,7 @@ pub fn all_language_settings<'a>(
     file: Option<&Arc<dyn File>>,
     cx: &'a AppContext,
 ) -> &'a AllLanguageSettings {
-    let location = file.map(|f| (f.worktree_id(), f.path().as_ref()));
+    let location = file.map(|f| f.as_ref().into());
     AllLanguageSettings::get(location, cx)
 }
 
@@ -44,6 +53,7 @@ pub struct AllLanguageSettings {
     pub copilot: CopilotSettings,
     defaults: LanguageSettings,
     languages: HashMap<Arc<str>, LanguageSettings>,
+    pub(crate) file_types: HashMap<Arc<str>, Vec<String>>,
 }
 
 /// The settings for a particular language.
@@ -121,6 +131,10 @@ pub struct AllLanguageSettingsContent {
     /// The settings for individual languages.
     #[serde(default, alias = "language_overrides")]
     pub languages: HashMap<Arc<str>, LanguageSettingsContent>,
+    /// Settings for associating file extensions and filenames
+    /// with languages.
+    #[serde(default)]
+    pub file_types: HashMap<Arc<str>, Vec<String>>,
 }
 
 /// The settings for a particular language.
@@ -502,6 +516,16 @@ impl settings::Settings for AllLanguageSettings {
             }
         }
 
+        let mut file_types: HashMap<Arc<str>, Vec<String>> = HashMap::default();
+        for user_file_types in user_settings.iter().map(|s| &s.file_types) {
+            for (language, suffixes) in user_file_types {
+                file_types
+                    .entry(language.clone())
+                    .or_default()
+                    .extend_from_slice(suffixes);
+            }
+        }
+
         Ok(Self {
             copilot: CopilotSettings {
                 feature_enabled: copilot_enabled,
@@ -512,6 +536,7 @@ impl settings::Settings for AllLanguageSettings {
             },
             defaults,
             languages,
+            file_types,
         })
     }
 

crates/language/src/syntax_map/syntax_map_tests.rs πŸ”—

@@ -1,5 +1,6 @@
 use super::*;
 use crate::{LanguageConfig, LanguageMatcher};
+use gpui::AppContext;
 use rand::rngs::StdRng;
 use std::{env, ops::Range, sync::Arc};
 use text::{Buffer, BufferId};
@@ -79,8 +80,8 @@ fn test_splice_included_ranges() {
 }
 
 #[gpui::test]
-fn test_syntax_map_layers_for_range() {
-    let registry = Arc::new(LanguageRegistry::test());
+fn test_syntax_map_layers_for_range(cx: &mut AppContext) {
+    let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
     let language = Arc::new(rust_lang());
     registry.add(language.clone());
 
@@ -176,8 +177,8 @@ fn test_syntax_map_layers_for_range() {
 }
 
 #[gpui::test]
-fn test_dynamic_language_injection() {
-    let registry = Arc::new(LanguageRegistry::test());
+fn test_dynamic_language_injection(cx: &mut AppContext) {
+    let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
     let markdown = Arc::new(markdown_lang());
     registry.add(markdown.clone());
     registry.add(Arc::new(rust_lang()));
@@ -254,7 +255,7 @@ fn test_dynamic_language_injection() {
 }
 
 #[gpui::test]
-fn test_typing_multiple_new_injections() {
+fn test_typing_multiple_new_injections(cx: &mut AppContext) {
     let (buffer, syntax_map) = test_edit_sequence(
         "Rust",
         &[
@@ -272,6 +273,7 @@ fn test_typing_multiple_new_injections() {
             "fn a() { test_macro!(b.c(vec![dΒ«.Β»])) }",
             "fn a() { test_macro!(b.c(vec![d.Β«eΒ»])) }",
         ],
+        cx,
     );
 
     assert_capture_ranges(
@@ -283,7 +285,7 @@ fn test_typing_multiple_new_injections() {
 }
 
 #[gpui::test]
-fn test_pasting_new_injection_line_between_others() {
+fn test_pasting_new_injection_line_between_others(cx: &mut AppContext) {
     let (buffer, syntax_map) = test_edit_sequence(
         "Rust",
         &[
@@ -309,6 +311,7 @@ fn test_pasting_new_injection_line_between_others() {
                 }
             ",
         ],
+        cx,
     );
 
     assert_capture_ranges(
@@ -330,7 +333,7 @@ fn test_pasting_new_injection_line_between_others() {
 }
 
 #[gpui::test]
-fn test_joining_injections_with_child_injections() {
+fn test_joining_injections_with_child_injections(cx: &mut AppContext) {
     let (buffer, syntax_map) = test_edit_sequence(
         "Rust",
         &[
@@ -355,6 +358,7 @@ fn test_joining_injections_with_child_injections() {
                 }
             ",
         ],
+        cx,
     );
 
     assert_capture_ranges(
@@ -374,7 +378,7 @@ fn test_joining_injections_with_child_injections() {
 }
 
 #[gpui::test]
-fn test_editing_edges_of_injection() {
+fn test_editing_edges_of_injection(cx: &mut AppContext) {
     test_edit_sequence(
         "Rust",
         &[
@@ -399,11 +403,12 @@ fn test_editing_edges_of_injection() {
                 }
             ",
         ],
+        cx,
     );
 }
 
 #[gpui::test]
-fn test_edits_preceding_and_intersecting_injection() {
+fn test_edits_preceding_and_intersecting_injection(cx: &mut AppContext) {
     test_edit_sequence(
         "Rust",
         &[
@@ -411,11 +416,12 @@ fn test_edits_preceding_and_intersecting_injection() {
             "const aaaaaaaaaaaa: B = c!(d(e.f));",
             "const aˇa: B = c!(d(eˇ));",
         ],
+        cx,
     );
 }
 
 #[gpui::test]
-fn test_non_local_changes_create_injections() {
+fn test_non_local_changes_create_injections(cx: &mut AppContext) {
     test_edit_sequence(
         "Rust",
         &[
@@ -430,11 +436,12 @@ fn test_non_local_changes_create_injections() {
                 Λ‡}
             ",
         ],
+        cx,
     );
 }
 
 #[gpui::test]
-fn test_creating_many_injections_in_one_edit() {
+fn test_creating_many_injections_in_one_edit(cx: &mut AppContext) {
     test_edit_sequence(
         "Rust",
         &[
@@ -460,11 +467,12 @@ fn test_creating_many_injections_in_one_edit() {
                 }
             ",
         ],
+        cx,
     );
 }
 
 #[gpui::test]
-fn test_editing_across_injection_boundary() {
+fn test_editing_across_injection_boundary(cx: &mut AppContext) {
     test_edit_sequence(
         "Rust",
         &[
@@ -488,11 +496,12 @@ fn test_editing_across_injection_boundary() {
                 }
             ",
         ],
+        cx,
     );
 }
 
 #[gpui::test]
-fn test_removing_injection_by_replacing_across_boundary() {
+fn test_removing_injection_by_replacing_across_boundary(cx: &mut AppContext) {
     test_edit_sequence(
         "Rust",
         &[
@@ -514,11 +523,12 @@ fn test_removing_injection_by_replacing_across_boundary() {
                 }
             ",
         ],
+        cx,
     );
 }
 
 #[gpui::test]
-fn test_combined_injections_simple() {
+fn test_combined_injections_simple(cx: &mut AppContext) {
     let (buffer, syntax_map) = test_edit_sequence(
         "ERB",
         &[
@@ -549,6 +559,7 @@ fn test_combined_injections_simple() {
                 </body>
             ",
         ],
+        cx,
     );
 
     assert_capture_ranges(
@@ -565,7 +576,7 @@ fn test_combined_injections_simple() {
 }
 
 #[gpui::test]
-fn test_combined_injections_empty_ranges() {
+fn test_combined_injections_empty_ranges(cx: &mut AppContext) {
     test_edit_sequence(
         "ERB",
         &[
@@ -579,11 +590,12 @@ fn test_combined_injections_empty_ranges() {
                 Λ‡<% end %>
             ",
         ],
+        cx,
     );
 }
 
 #[gpui::test]
-fn test_combined_injections_edit_edges_of_ranges() {
+fn test_combined_injections_edit_edges_of_ranges(cx: &mut AppContext) {
     let (buffer, syntax_map) = test_edit_sequence(
         "ERB",
         &[
@@ -600,6 +612,7 @@ fn test_combined_injections_edit_edges_of_ranges() {
                 <%= three @four %>
             ",
         ],
+        cx,
     );
 
     assert_capture_ranges(
@@ -614,7 +627,7 @@ fn test_combined_injections_edit_edges_of_ranges() {
 }
 
 #[gpui::test]
-fn test_combined_injections_splitting_some_injections() {
+fn test_combined_injections_splitting_some_injections(cx: &mut AppContext) {
     let (_buffer, _syntax_map) = test_edit_sequence(
         "ERB",
         &[
@@ -635,11 +648,12 @@ fn test_combined_injections_splitting_some_injections() {
                 <% f %>
             "#,
         ],
+        cx,
     );
 }
 
 #[gpui::test]
-fn test_combined_injections_editing_after_last_injection() {
+fn test_combined_injections_editing_after_last_injection(cx: &mut AppContext) {
     test_edit_sequence(
         "ERB",
         &[
@@ -655,11 +669,12 @@ fn test_combined_injections_editing_after_last_injection() {
                 more textΒ»
             "#,
         ],
+        cx,
     );
 }
 
 #[gpui::test]
-fn test_combined_injections_inside_injections() {
+fn test_combined_injections_inside_injections(cx: &mut AppContext) {
     let (buffer, syntax_map) = test_edit_sequence(
         "Markdown",
         &[
@@ -709,6 +724,7 @@ fn test_combined_injections_inside_injections() {
                 ```
             "#,
         ],
+        cx,
     );
 
     // Check that the code directive below the ruby comment is
@@ -735,7 +751,7 @@ fn test_combined_injections_inside_injections() {
 }
 
 #[gpui::test]
-fn test_empty_combined_injections_inside_injections() {
+fn test_empty_combined_injections_inside_injections(cx: &mut AppContext) {
     let (buffer, syntax_map) = test_edit_sequence(
         "Markdown",
         &[r#"
@@ -745,6 +761,7 @@ fn test_empty_combined_injections_inside_injections() {
 
             goodbye
         "#],
+        cx,
     );
 
     assert_layers_for_range(
@@ -763,7 +780,7 @@ fn test_empty_combined_injections_inside_injections() {
 }
 
 #[gpui::test(iterations = 50)]
-fn test_random_syntax_map_edits_rust_macros(rng: StdRng) {
+fn test_random_syntax_map_edits_rust_macros(rng: StdRng, cx: &mut AppContext) {
     let text = r#"
         fn test_something() {
             let vec = vec![5, 1, 3, 8];
@@ -781,7 +798,7 @@ fn test_random_syntax_map_edits_rust_macros(rng: StdRng) {
     .unindent()
     .repeat(2);
 
-    let registry = Arc::new(LanguageRegistry::test());
+    let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
     let language = Arc::new(rust_lang());
     registry.add(language.clone());
 
@@ -789,7 +806,7 @@ fn test_random_syntax_map_edits_rust_macros(rng: StdRng) {
 }
 
 #[gpui::test(iterations = 50)]
-fn test_random_syntax_map_edits_with_erb(rng: StdRng) {
+fn test_random_syntax_map_edits_with_erb(rng: StdRng, cx: &mut AppContext) {
     let text = r#"
         <div id="main">
         <% if one?(:two) %>
@@ -808,7 +825,7 @@ fn test_random_syntax_map_edits_with_erb(rng: StdRng) {
     .unindent()
     .repeat(5);
 
-    let registry = Arc::new(LanguageRegistry::test());
+    let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
     let language = Arc::new(erb_lang());
     registry.add(language.clone());
     registry.add(Arc::new(ruby_lang()));
@@ -818,7 +835,7 @@ fn test_random_syntax_map_edits_with_erb(rng: StdRng) {
 }
 
 #[gpui::test(iterations = 50)]
-fn test_random_syntax_map_edits_with_heex(rng: StdRng) {
+fn test_random_syntax_map_edits_with_heex(rng: StdRng, cx: &mut AppContext) {
     let text = r#"
         defmodule TheModule do
             def the_method(assigns) do
@@ -841,7 +858,7 @@ fn test_random_syntax_map_edits_with_heex(rng: StdRng) {
     .unindent()
     .repeat(3);
 
-    let registry = Arc::new(LanguageRegistry::test());
+    let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
     let language = Arc::new(elixir_lang());
     registry.add(language.clone());
     registry.add(Arc::new(heex_lang()));
@@ -1025,8 +1042,12 @@ fn check_interpolation(
     }
 }
 
-fn test_edit_sequence(language_name: &str, steps: &[&str]) -> (Buffer, SyntaxMap) {
-    let registry = Arc::new(LanguageRegistry::test());
+fn test_edit_sequence(
+    language_name: &str,
+    steps: &[&str],
+    cx: &mut AppContext,
+) -> (Buffer, SyntaxMap) {
+    let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
     registry.add(Arc::new(elixir_lang()));
     registry.add(Arc::new(heex_lang()));
     registry.add(Arc::new(rust_lang()));

crates/markdown_preview/src/markdown_parser.rs πŸ”—

@@ -1079,9 +1079,7 @@ fn main() {
 
     #[gpui::test]
     async fn test_code_block_with_language(executor: BackgroundExecutor) {
-        let mut language_registry = LanguageRegistry::test();
-        language_registry.set_executor(executor);
-        let language_registry = Arc::new(language_registry);
+        let language_registry = Arc::new(LanguageRegistry::test(executor.clone()));
         language_registry.add(rust_lang());
 
         let parsed = parse_markdown(

crates/project/src/project.rs πŸ”—

@@ -64,7 +64,7 @@ use worktree::LocalSnapshot;
 use rpc::{ErrorCode, ErrorExt as _};
 use search::SearchQuery;
 use serde::Serialize;
-use settings::{watch_config_file, Settings, SettingsStore};
+use settings::{watch_config_file, Settings, SettingsLocation, SettingsStore};
 use sha2::{Digest, Sha256};
 use similar::{ChangeTag, TextDiff};
 use smol::channel::{Receiver, Sender};
@@ -861,8 +861,7 @@ impl Project {
     ) -> Model<Project> {
         use clock::FakeSystemClock;
 
-        let mut languages = LanguageRegistry::test();
-        languages.set_executor(cx.executor());
+        let languages = LanguageRegistry::test(cx.executor());
         let clock = Arc::new(FakeSystemClock::default());
         let http_client = util::http::FakeHttpClient::with_404_response();
         let client = cx.update(|cx| client::Client::new(clock, http_client.clone(), cx));
@@ -2776,11 +2775,11 @@ impl Project {
     ) -> Option<()> {
         // If the buffer has a language, set it and start the language server if we haven't already.
         let buffer = buffer_handle.read(cx);
-        let full_path = buffer.file()?.full_path(cx);
+        let file = buffer.file()?;
         let content = buffer.as_rope();
         let new_language = self
             .languages
-            .language_for_file(&full_path, Some(content))
+            .language_for_file(file, Some(content), cx)
             .now_or_never()?
             .ok()?;
         self.set_language_for_buffer(buffer_handle, new_language, cx);
@@ -2869,8 +2868,13 @@ impl Project {
             None => return,
         };
 
-        let project_settings =
-            ProjectSettings::get(Some((worktree_id.to_proto() as usize, Path::new(""))), cx);
+        let project_settings = ProjectSettings::get(
+            Some(SettingsLocation {
+                worktree_id: worktree_id.to_proto() as usize,
+                path: Path::new(""),
+            }),
+            cx,
+        );
         let lsp = project_settings.lsp.get(&adapter.name.0);
         let override_options = lsp.and_then(|s| s.initialization_options.clone());
 
@@ -3553,14 +3557,14 @@ impl Project {
             .into_iter()
             .filter_map(|buffer| {
                 let buffer = buffer.read(cx);
-                let file = File::from_dyn(buffer.file())?;
-                let full_path = file.full_path(cx);
+                let file = buffer.file()?;
+                let worktree = File::from_dyn(Some(file))?.worktree.clone();
                 let language = self
                     .languages
-                    .language_for_file(&full_path, Some(buffer.as_rope()))
+                    .language_for_file(file, Some(buffer.as_rope()), cx)
                     .now_or_never()?
                     .ok()?;
-                Some((file.worktree.clone(), language))
+                Some((worktree, language))
             })
             .collect();
         for (worktree, language) in language_server_lookup_info {
@@ -4900,11 +4904,15 @@ impl Project {
         if self.is_local() {
             let mut requests = Vec::new();
             for ((worktree_id, _), server_id) in self.language_server_ids.iter() {
-                let worktree_id = *worktree_id;
-                let worktree_handle = self.worktree_for_id(worktree_id, cx);
-                let worktree = match worktree_handle.and_then(|tree| tree.read(cx).as_local()) {
-                    Some(worktree) => worktree,
-                    None => continue,
+                let Some(worktree_handle) = self.worktree_for_id(*worktree_id, cx) else {
+                    continue;
+                };
+                let worktree = worktree_handle.read(cx);
+                if !worktree.is_visible() {
+                    continue;
+                }
+                let Some(worktree) = worktree.as_local() else {
+                    continue;
                 };
                 let worktree_abs_path = worktree.abs_path().clone();
 
@@ -4952,7 +4960,7 @@ impl Project {
                             (
                                 adapter,
                                 language,
-                                worktree_id,
+                                worktree_handle.downgrade(),
                                 worktree_abs_path,
                                 lsp_symbols,
                             )
@@ -4972,7 +4980,7 @@ impl Project {
                     for (
                         adapter,
                         adapter_language,
-                        source_worktree_id,
+                        source_worktree,
                         worktree_abs_path,
                         lsp_symbols,
                     ) in responses
@@ -4980,17 +4988,22 @@ impl Project {
                         symbols.extend(lsp_symbols.into_iter().filter_map(
                             |(symbol_name, symbol_kind, symbol_location)| {
                                 let abs_path = symbol_location.uri.to_file_path().ok()?;
-                                let mut worktree_id = source_worktree_id;
+                                let source_worktree = source_worktree.upgrade()?;
+                                let source_worktree_id = source_worktree.read(cx).id();
+
                                 let path;
-                                if let Some((worktree, rel_path)) =
+                                let worktree;
+                                if let Some((tree, rel_path)) =
                                     this.find_local_worktree(&abs_path, cx)
                                 {
-                                    worktree_id = worktree.read(cx).id();
+                                    worktree = tree;
                                     path = rel_path;
                                 } else {
+                                    worktree = source_worktree.clone();
                                     path = relativize_path(&worktree_abs_path, &abs_path);
                                 }
 
+                                let worktree_id = worktree.read(cx).id();
                                 let project_path = ProjectPath {
                                     worktree_id,
                                     path: path.into(),
@@ -4999,7 +5012,7 @@ impl Project {
                                 let adapter_language = adapter_language.clone();
                                 let language = this
                                     .languages
-                                    .language_for_file(&project_path.path, None)
+                                    .language_for_file_path(&project_path.path)
                                     .unwrap_or_else(move |_| adapter_language);
                                 let adapter = adapter.clone();
                                 Some(async move {
@@ -8538,7 +8551,7 @@ impl Project {
             .symbol
             .ok_or_else(|| anyhow!("invalid symbol"))?;
         let symbol = this
-            .update(&mut cx, |this, _| this.deserialize_symbol(symbol))?
+            .update(&mut cx, |this, _cx| this.deserialize_symbol(symbol))?
             .await?;
         let symbol = this.update(&mut cx, |this, _| {
             let signature = this.symbol_signature(&symbol.path);
@@ -8928,27 +8941,26 @@ impl Project {
         serialized_symbol: proto::Symbol,
     ) -> impl Future<Output = Result<Symbol>> {
         let languages = self.languages.clone();
+        let source_worktree_id = WorktreeId::from_proto(serialized_symbol.source_worktree_id);
+        let worktree_id = WorktreeId::from_proto(serialized_symbol.worktree_id);
+        let kind = unsafe { mem::transmute(serialized_symbol.kind) };
+        let path = ProjectPath {
+            worktree_id,
+            path: PathBuf::from(serialized_symbol.path).into(),
+        };
+        let language = languages.language_for_file_path(&path.path);
+
         async move {
-            let source_worktree_id = WorktreeId::from_proto(serialized_symbol.source_worktree_id);
-            let worktree_id = WorktreeId::from_proto(serialized_symbol.worktree_id);
+            let language = language.await.log_err();
+            let adapter = language
+                .as_ref()
+                .and_then(|language| languages.lsp_adapters(language).first().cloned());
             let start = serialized_symbol
                 .start
                 .ok_or_else(|| anyhow!("invalid start"))?;
             let end = serialized_symbol
                 .end
                 .ok_or_else(|| anyhow!("invalid end"))?;
-            let kind = unsafe { mem::transmute(serialized_symbol.kind) };
-            let path = ProjectPath {
-                worktree_id,
-                path: PathBuf::from(serialized_symbol.path).into(),
-            };
-            let language = languages
-                .language_for_file(&path.path, None)
-                .await
-                .log_err();
-            let adapter = language
-                .as_ref()
-                .and_then(|language| languages.lsp_adapters(language).first().cloned());
             Ok(Symbol {
                 language_server_name: LanguageServerName(
                     serialized_symbol.language_server_name.into(),
@@ -9419,6 +9431,15 @@ impl<'a> Iterator for PathMatchCandidateSetIter<'a> {
 
 impl EventEmitter<Event> for Project {}
 
+impl<'a> Into<SettingsLocation<'a>> for &'a ProjectPath {
+    fn into(self) -> SettingsLocation<'a> {
+        SettingsLocation {
+            worktree_id: self.worktree_id.to_usize(),
+            path: self.path.as_ref(),
+        }
+    }
+}
+
 impl<P: AsRef<Path>> From<(WorktreeId, P)> for ProjectPath {
     fn from((worktree_id, path): (WorktreeId, P)) -> Self {
         Self {

crates/semantic_index/src/semantic_index.rs πŸ”—

@@ -583,7 +583,7 @@ impl SemanticIndex {
                                 }
 
                                 if let Ok(language) = language_registry
-                                    .language_for_file(&absolute_path, None)
+                                    .language_for_file_path(&absolute_path)
                                     .await
                                 {
                                     // Test if file is valid parseable file
@@ -1144,7 +1144,7 @@ impl SemanticIndex {
 
                     for mut pending_file in pending_files {
                         if let Ok(language) = language_registry
-                            .language_for_file(&pending_file.relative_path, None)
+                            .language_for_file_path(&pending_file.relative_path)
                             .await
                         {
                             if !PARSEABLE_ENTIRE_FILE_TYPES.contains(&language.name().as_ref())

crates/semantic_index/src/semantic_index_tests.rs πŸ”—

@@ -5,8 +5,7 @@ use crate::{
     FileToEmbed, JobHandle, SearchResult, SemanticIndex, EMBEDDING_QUEUE_FLUSH_TIMEOUT,
 };
 use ai::test::FakeEmbeddingProvider;
-
-use gpui::{Task, TestAppContext};
+use gpui::TestAppContext;
 use language::{Language, LanguageConfig, LanguageMatcher, LanguageRegistry, ToOffset};
 use parking_lot::Mutex;
 use pretty_assertions::assert_eq;
@@ -57,7 +56,7 @@ async fn test_semantic_index(cx: &mut TestAppContext) {
     )
     .await;
 
-    let languages = Arc::new(LanguageRegistry::new(Task::ready(())));
+    let languages = Arc::new(LanguageRegistry::test(cx.executor().clone()));
     let rust_language = rust_lang();
     let toml_language = toml_lang();
     languages.add(rust_language);
@@ -1720,6 +1719,7 @@ fn init_test(cx: &mut TestAppContext) {
         let settings_store = SettingsStore::test(cx);
         cx.set_global(settings_store);
         SemanticIndexSettings::register(cx);
+        language::init(cx);
         Project::init_settings(cx);
     });
 }

crates/settings/src/settings.rs πŸ”—

@@ -8,7 +8,7 @@ use util::asset_str;
 
 pub use keymap_file::KeymapFile;
 pub use settings_file::*;
-pub use settings_store::{Settings, SettingsJsonSchemaParams, SettingsStore};
+pub use settings_store::{Settings, SettingsJsonSchemaParams, SettingsLocation, SettingsStore};
 
 #[derive(RustEmbed)]
 #[folder = "../../assets"]

crates/settings/src/settings_store.rs πŸ”—

@@ -86,9 +86,8 @@ pub trait Settings: 'static + Send + Sync {
         });
     }
 
-    /// path is a (worktree ID, Path)
     #[track_caller]
-    fn get<'a>(path: Option<(usize, &Path)>, cx: &'a AppContext) -> &'a Self
+    fn get<'a>(path: Option<SettingsLocation>, cx: &'a AppContext) -> &'a Self
     where
         Self: Sized,
     {
@@ -120,6 +119,12 @@ pub trait Settings: 'static + Send + Sync {
     }
 }
 
+#[derive(Clone, Copy)]
+pub struct SettingsLocation<'a> {
+    pub worktree_id: usize,
+    pub path: &'a Path,
+}
+
 pub struct SettingsJsonSchemaParams<'a> {
     pub staff_mode: bool,
     pub language_names: &'a [String],
@@ -168,7 +173,7 @@ trait AnySettingValue: 'static + Send + Sync {
         custom: &[DeserializedSetting],
         cx: &mut AppContext,
     ) -> Result<Box<dyn Any>>;
-    fn value_for_path(&self, path: Option<(usize, &Path)>) -> &dyn Any;
+    fn value_for_path(&self, path: Option<SettingsLocation>) -> &dyn Any;
     fn set_global_value(&mut self, value: Box<dyn Any>);
     fn set_local_value(&mut self, root_id: usize, path: Arc<Path>, value: Box<dyn Any>);
     fn json_schema(
@@ -234,7 +239,7 @@ impl SettingsStore {
     ///
     /// Panics if the given setting type has not been registered, or if there is no
     /// value for this setting.
-    pub fn get<T: Settings>(&self, path: Option<(usize, &Path)>) -> &T {
+    pub fn get<T: Settings>(&self, path: Option<SettingsLocation>) -> &T {
         self.setting_values
             .get(&TypeId::of::<T>())
             .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
@@ -659,10 +664,10 @@ impl<T: Settings> AnySettingValue for SettingValue<T> {
         Ok(DeserializedSetting(Box::new(value)))
     }
 
-    fn value_for_path(&self, path: Option<(usize, &Path)>) -> &dyn Any {
-        if let Some((root_id, path)) = path {
+    fn value_for_path(&self, path: Option<SettingsLocation>) -> &dyn Any {
+        if let Some(SettingsLocation { worktree_id, path }) = path {
             for (settings_root_id, settings_path, value) in self.local_values.iter().rev() {
-                if root_id == *settings_root_id && path.starts_with(settings_path) {
+                if worktree_id == *settings_root_id && path.starts_with(settings_path) {
                     return value;
                 }
             }
@@ -1010,7 +1015,10 @@ mod tests {
             .unwrap();
 
         assert_eq!(
-            store.get::<UserSettings>(Some((1, Path::new("/root1/something")))),
+            store.get::<UserSettings>(Some(SettingsLocation {
+                worktree_id: 1,
+                path: Path::new("/root1/something"),
+            })),
             &UserSettings {
                 name: "John Doe".to_string(),
                 age: 31,
@@ -1018,7 +1026,10 @@ mod tests {
             }
         );
         assert_eq!(
-            store.get::<UserSettings>(Some((1, Path::new("/root1/subdir/something")))),
+            store.get::<UserSettings>(Some(SettingsLocation {
+                worktree_id: 1,
+                path: Path::new("/root1/subdir/something")
+            })),
             &UserSettings {
                 name: "Jane Doe".to_string(),
                 age: 31,
@@ -1026,7 +1037,10 @@ mod tests {
             }
         );
         assert_eq!(
-            store.get::<UserSettings>(Some((1, Path::new("/root2/something")))),
+            store.get::<UserSettings>(Some(SettingsLocation {
+                worktree_id: 1,
+                path: Path::new("/root2/something")
+            })),
             &UserSettings {
                 name: "John Doe".to_string(),
                 age: 42,
@@ -1034,7 +1048,10 @@ mod tests {
             }
         );
         assert_eq!(
-            store.get::<MultiKeySettings>(Some((1, Path::new("/root2/something")))),
+            store.get::<MultiKeySettings>(Some(SettingsLocation {
+                worktree_id: 1,
+                path: Path::new("/root2/something")
+            })),
             &MultiKeySettings {
                 key1: "a".to_string(),
                 key2: "b".to_string(),

crates/workspace/src/workspace.rs πŸ”—

@@ -405,7 +405,7 @@ impl AppState {
         }
 
         let fs = fs::FakeFs::new(cx.background_executor().clone());
-        let languages = Arc::new(LanguageRegistry::test());
+        let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
         let clock = Arc::new(clock::FakeSystemClock::default());
         let http_client = util::http::FakeHttpClient::with_404_response();
         let client = Client::new(clock, http_client.clone(), cx);

crates/worktree/src/worktree.rs πŸ”—

@@ -46,7 +46,7 @@ use postage::{
     watch,
 };
 use serde::Serialize;
-use settings::{Settings, SettingsStore};
+use settings::{Settings, SettingsLocation, SettingsStore};
 use smol::channel::{self, Sender};
 use std::{
     any::Any,
@@ -352,7 +352,10 @@ impl Worktree {
                         "file_scan_exclusions",
                     );
                     let new_private_files = path_matchers(
-                        WorktreeSettings::get(Some((cx.handle().entity_id().as_u64() as usize, &Path::new(""))), cx).private_files.as_deref(),
+                        WorktreeSettings::get(Some(settings::SettingsLocation {
+                            worktree_id: cx.handle().entity_id().as_u64() as usize,
+                            path: Path::new("")
+                        }), cx).private_files.as_deref(),
                         "private_files",
                     );
 
@@ -408,7 +411,10 @@ impl Worktree {
                     "file_scan_exclusions",
                 ),
                 private_files: path_matchers(
-                    WorktreeSettings::get(Some((cx.handle().entity_id().as_u64() as usize, &Path::new(""))), cx).private_files.as_deref(),
+                    WorktreeSettings::get(Some(SettingsLocation {
+                        worktree_id: cx.handle().entity_id().as_u64() as usize,
+                        path: Path::new(""),
+                    }), cx).private_files.as_deref(),
                     "private_files",
                 ),
                 ignores_by_parent_abs_path: Default::default(),

crates/zed/src/main.rs πŸ”—

@@ -142,9 +142,9 @@ fn main() {
         ));
 
         let client = client::Client::new(clock, http.clone(), cx);
-        let mut languages = LanguageRegistry::new(login_shell_env_loaded);
+        let mut languages =
+            LanguageRegistry::new(login_shell_env_loaded, cx.background_executor().clone());
         let copilot_language_server_id = languages.next_language_server_id();
-        languages.set_executor(cx.background_executor().clone());
         languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone());
         let languages = Arc::new(languages);
         let node_runtime = RealNodeRuntime::new(http.clone());

crates/zed/src/zed.rs πŸ”—

@@ -3024,15 +3024,18 @@ mod tests {
     async fn test_bundled_languages(cx: &mut TestAppContext) {
         let settings = cx.update(|cx| SettingsStore::test(cx));
         cx.set_global(settings);
-        let mut languages = LanguageRegistry::test();
-        languages.set_executor(cx.executor().clone());
+        let languages = LanguageRegistry::test(cx.executor());
         let languages = Arc::new(languages);
         let node_runtime = node_runtime::FakeNodeRuntime::new();
         cx.update(|cx| {
             languages::init(languages.clone(), node_runtime, cx);
         });
         for name in languages.language_names() {
-            languages.language_for_name(&name).await.unwrap();
+            languages
+                .language_for_name(&name)
+                .await
+                .with_context(|| format!("language name {name}"))
+                .unwrap();
         }
         cx.run_until_parked();
     }

docs/src/configuring_zed.md πŸ”—

@@ -380,6 +380,25 @@ To override settings for a language, add an entry for that language server's nam
 
 `boolean` values
 
+## File Types
+
+- Setting: `file_types`
+- Description: Configure how Zed selects a language for a file based on its filename or extension.
+- Default: `{}`
+
+**Examples**
+
+To interpret all `.c` files as C++, and files called `MyLockFile` as TOML:
+
+```json
+{
+  "file_types": {
+    "C++": ["c"],
+    "TOML": ["MyLockFile"]
+  }
+}
+```
+
 ## Git
 
 - Description: Configuration for git-related features.