From 724c19a223b17d6cf135c4eb9a89e85d2ab86f37 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 13 Mar 2024 10:23:30 -0700 Subject: [PATCH] Add a setting for custom associations between languages and files (#9290) 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 --- 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 +- .../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 | 341 ++++++++++-------- crates/language/src/language_settings.rs | 29 +- .../src/syntax_map/syntax_map_tests.rs | 77 ++-- .../markdown_preview/src/markdown_parser.rs | 4 +- crates/project/src/project.rs | 95 +++-- crates/semantic_index/src/semantic_index.rs | 4 +- .../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, 640 insertions(+), 415 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index d0c9663ef0..e3fdadafd8 100644 --- a/assets/settings/default.json +++ b/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": { diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 98d47809e2..ffa7b975aa 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/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)); diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 652faa5c53..80b8769412 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/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(), diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 8a39054df8..d2410c90e0 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/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(), diff --git a/crates/collab_ui/src/chat_panel/message_editor.rs b/crates/collab_ui/src/chat_panel/message_editor.rs index d429590608..cf77a77d47 100644 --- a/crates/collab_ui/src/chat_panel/message_editor.rs +++ b/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(), diff --git a/crates/copilot_ui/src/copilot_button.rs b/crates/copilot_ui/src/copilot_button.rs index 1ca66dda36..93e99dd919 100644 --- a/crates/copilot_ui/src/copilot_button.rs +++ b/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; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index fe401d4e9d..76ad4a6eac 100644 --- a/crates/editor/src/editor.rs +++ b/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, - _cx: &AppContext, - ) { - } - - #[cfg(not(any(test, feature = "test-support")))] fn report_editor_event( &self, operation: &'static str, file_extension: Option, 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 diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 9a096c347c..1e1c13d9ab 100644 --- a/crates/editor/src/editor_tests.rs +++ b/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); }); diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 02be847f6d..e64a0a4314 100644 --- a/crates/editor/src/hover_popover.rs +++ b/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 diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 55de313bcc..8a934ecbd7 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/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 { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index d256b0eb16..f5f52e86cf 100644 --- a/crates/editor/src/items.rs +++ b/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, - full_path: PathBuf, - } - - impl language::File for TestFile { - fn path(&self) -> &Arc { - &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 { - 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 - } - } } diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 9543e46f96..be372525d4 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/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 { + self.editor(|editor, cx| { + editor + .project + .as_ref() + .unwrap() + .read(cx) + .languages() + .clone() + }) + } + pub fn update_buffer(&mut self, update: F) -> T where F: FnOnce(&mut Buffer, &mut ModelContext) -> T, diff --git a/crates/extension/src/extension_store_test.rs b/crates/extension/src/extension_store_test.rs index 4443cf022c..6755332524 100644 --- a/crates/extension/src/extension_store_test.rs +++ b/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(); diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 13a81a52cc..9bbba14474 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -3527,6 +3527,55 @@ impl Completion { } } +#[cfg(any(test, feature = "test-support"))] +pub struct TestFile { + pub path: Arc, + pub root_name: String, +} + +#[cfg(any(test, feature = "test-support"))] +impl File for TestFile { + fn path(&self) -> &Arc { + &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 { + 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, max_len: usize, diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index bc3eb692fa..ce0160f280 100644 --- a/crates/language/src/buffer_tests.rs +++ b/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 { + 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())); diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 2360aecd78..37e28d5cd6 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -852,11 +852,7 @@ struct BracketConfig { impl Language { pub fn new(config: LanguageConfig, ts_language: Option) -> 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()), diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index d32b0f3346..77f76517a5 100644 --- a/crates/language/src/language_registry.rs +++ b/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, language_server_download_dir: Option>, login_shell_env_loaded: Shared>, - #[allow(clippy::type_complexity)] - lsp_binary_paths: Mutex< - HashMap>>>>, - >, - executor: Option, + 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,21 +137,18 @@ 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(), - } - } - - #[cfg(any(test, feature = "test-support"))] - pub fn test() -> Self { - let mut this = Self::new(Task::ready(())); - this.language_server_download_dir = Some(Path::new("/the-download-dir").into()); + executor, + }; + this.add(PLAIN_TEXT.clone()); this } - pub fn set_executor(&mut self, executor: BackgroundExecutor) { - self.executor = Some(executor); + #[cfg(any(test, feature = "test-support"))] + 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 } /// Clears out all of the loaded languages and reload them from scratch. @@ -317,8 +311,19 @@ impl LanguageRegistry { result } + /// Add a pre-loaded language to the registry. pub fn add(&self, language: Arc) { - 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>> { 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>> { 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, + file: &Arc, + content: Option<&Rope>, + cx: &AppContext, + ) -> impl Future>> { + 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, + path: &Path, + ) -> impl Future>> { + self.language_for_file_internal(path, None, None) + } + + fn language_for_file_internal( self: &Arc, path: &Path, content: Option<&Rope>, + user_file_types: Option<&HashMap, Vec>>, ) -> impl Future>> { 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, - callback: impl Fn(&str, &LanguageMatcher) -> bool, + callback: impl Fn(&str, &LanguageMatcher) -> usize, ) -> oneshot::Receiver>> { 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())); - } - } - } - 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]); - } + .filter_map(|language| { + let score = callback(&language.name, &language.matcher); + if score > 0 { + Some((language.clone(), score)) + } else { + None } - } else { - let _ = tx.send(Err(anyhow!("language not found"))); + }) + .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 + ))); + } + } + } + }; + }) + .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 - })?; + 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(); - } + 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)) diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 4babe6344c..6ee6e137f6 100644 --- a/crates/language/src/language_settings.rs +++ b/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> 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>, 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, LanguageSettings>, + pub(crate) file_types: HashMap, Vec>, } /// 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, LanguageSettingsContent>, + /// Settings for associating file extensions and filenames + /// with languages. + #[serde(default)] + pub file_types: HashMap, Vec>, } /// The settings for a particular language. @@ -502,6 +516,16 @@ impl settings::Settings for AllLanguageSettings { } } + let mut file_types: HashMap, Vec> = 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, }) } diff --git a/crates/language/src/syntax_map/syntax_map_tests.rs b/crates/language/src/syntax_map/syntax_map_tests.rs index 0601a9f3c1..84c9ab72f3 100644 --- a/crates/language/src/syntax_map/syntax_map_tests.rs +++ b/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() { ", ], + 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#"
<% 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())); diff --git a/crates/markdown_preview/src/markdown_parser.rs b/crates/markdown_preview/src/markdown_parser.rs index c3733300e2..474cc0ac0f 100644 --- a/crates/markdown_preview/src/markdown_parser.rs +++ b/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( diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 46c2ccd2a4..d18c14fa6c 100644 --- a/crates/project/src/project.rs +++ b/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 { 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> { 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 for Project {} +impl<'a> Into> for &'a ProjectPath { + fn into(self) -> SettingsLocation<'a> { + SettingsLocation { + worktree_id: self.worktree_id.to_usize(), + path: self.path.as_ref(), + } + } +} + impl> From<(WorktreeId, P)> for ProjectPath { fn from((worktree_id, path): (WorktreeId, P)) -> Self { Self { diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 8f5da4ffd8..b93bd433cd 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/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()) diff --git a/crates/semantic_index/src/semantic_index_tests.rs b/crates/semantic_index/src/semantic_index_tests.rs index 48ceabdb63..728e12f0bc 100644 --- a/crates/semantic_index/src/semantic_index_tests.rs +++ b/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); }); } diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 3004100e50..4febec6542 100644 --- a/crates/settings/src/settings.rs +++ b/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"] diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 2cde1187ab..32522eca01 100644 --- a/crates/settings/src/settings_store.rs +++ b/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, 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>; - fn value_for_path(&self, path: Option<(usize, &Path)>) -> &dyn Any; + fn value_for_path(&self, path: Option) -> &dyn Any; fn set_global_value(&mut self, value: Box); fn set_local_value(&mut self, root_id: usize, path: Arc, value: Box); 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(&self, path: Option<(usize, &Path)>) -> &T { + pub fn get(&self, path: Option) -> &T { self.setting_values .get(&TypeId::of::()) .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::())) @@ -659,10 +664,10 @@ impl AnySettingValue for SettingValue { 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) -> &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::(Some((1, Path::new("/root1/something")))), + store.get::(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::(Some((1, Path::new("/root1/subdir/something")))), + store.get::(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::(Some((1, Path::new("/root2/something")))), + store.get::(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::(Some((1, Path::new("/root2/something")))), + store.get::(Some(SettingsLocation { + worktree_id: 1, + path: Path::new("/root2/something") + })), &MultiKeySettings { key1: "a".to_string(), key2: "b".to_string(), diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 01c7c5e3dd..edf7f12fb9 100644 --- a/crates/workspace/src/workspace.rs +++ b/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); diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index f4aef93b4d..4d12b558cf 100644 --- a/crates/worktree/src/worktree.rs +++ b/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(), diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index adfc0fd60d..73350bcda7 100644 --- a/crates/zed/src/main.rs +++ b/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()); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 68d6b385e0..06ddf2e278 100644 --- a/crates/zed/src/zed.rs +++ b/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(); } diff --git a/docs/src/configuring_zed.md b/docs/src/configuring_zed.md index b2ae9ab86a..06d41098f6 100644 --- a/docs/src/configuring_zed.md +++ b/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.