diff --git a/Cargo.lock b/Cargo.lock index 04f18936cd..2bbc846044 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2559,6 +2559,24 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "extension" +version = "0.1.0" +dependencies = [ + "anyhow", + "collections", + "fs", + "futures 0.3.28", + "gpui", + "language", + "parking_lot 0.11.2", + "serde", + "serde_json", + "theme", + "toml", + "util", +] + [[package]] name = "fallible-iterator" version = "0.2.0" @@ -3969,6 +3987,7 @@ dependencies = [ "sum_tree", "text", "theme", + "toml", "tree-sitter", "tree-sitter-elixir", "tree-sitter-embedded-template", @@ -10327,6 +10346,7 @@ dependencies = [ "diagnostics", "editor", "env_logger", + "extension", "feature_flags", "feedback", "file_finder", diff --git a/Cargo.toml b/Cargo.toml index 4587e947c6..c7e40436f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ members = [ "crates/db", "crates/diagnostics", "crates/editor", + "crates/extension", "crates/feature_flags", "crates/feedback", "crates/file_finder", @@ -30,12 +31,9 @@ members = [ "crates/git", "crates/go_to_line", "crates/gpui", - "crates/gpui", - "crates/gpui_macros", "crates/gpui_macros", "crates/install_cli", "crates/journal", - "crates/journal", "crates/language", "crates/language_selector", "crates/language_tools", @@ -114,6 +112,7 @@ copilot_ui = { path = "crates/copilot_ui" } db = { path = "crates/db" } diagnostics = { path = "crates/diagnostics" } editor = { path = "crates/editor" } +extension = { path = "crates/extension" } feature_flags = { path = "crates/feature_flags" } feedback = { path = "crates/feedback" } file_finder = { path = "crates/file_finder" } diff --git a/crates/assistant/src/codegen.rs b/crates/assistant/src/codegen.rs index d592ce88ae..c1a663d7ef 100644 --- a/crates/assistant/src/codegen.rs +++ b/crates/assistant/src/codegen.rs @@ -366,7 +366,8 @@ mod tests { use gpui::{Context, TestAppContext}; use indoc::indoc; use language::{ - language_settings, tree_sitter_rust, Buffer, BufferId, Language, LanguageConfig, Point, + language_settings, tree_sitter_rust, Buffer, BufferId, Language, LanguageConfig, + LanguageMatcher, Point, }; use rand::prelude::*; use serde::Serialize; @@ -675,7 +676,10 @@ mod tests { Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs index 9042fda5be..f40a841f4c 100644 --- a/crates/assistant/src/prompts.rs +++ b/crates/assistant/src/prompts.rs @@ -172,22 +172,24 @@ pub fn generate_content_prompt( #[cfg(test)] pub(crate) mod tests { - use super::*; - use std::sync::Arc; - use gpui::{AppContext, Context}; use indoc::indoc; use language::{ - language_settings, tree_sitter_rust, Buffer, BufferId, Language, LanguageConfig, Point, + language_settings, tree_sitter_rust, Buffer, BufferId, Language, LanguageConfig, + LanguageMatcher, Point, }; use settings::SettingsStore; + use std::sync::Arc; pub(crate) fn rust_lang() -> Language { Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 0e11ec1684..458f347efb 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -19,7 +19,7 @@ use gpui::{TestAppContext, VisualContext, VisualTestContext}; use indoc::indoc; use language::{ language_settings::{AllLanguageSettings, InlayHintSettings}, - tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, + tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, }; use rpc::RECEIVE_TIMEOUT; use serde_json::json; @@ -269,7 +269,10 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -455,7 +458,10 @@ async fn test_collaborating_with_code_actions( let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -668,7 +674,10 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -853,7 +862,10 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -1144,7 +1156,10 @@ async fn test_on_input_format_from_host_to_guest( let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -1272,7 +1287,10 @@ async fn test_on_input_format_from_guest_to_host( let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -1431,7 +1449,10 @@ async fn test_mutual_editor_inlay_hint_cache_update( let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -1697,7 +1718,10 @@ async fn test_inlay_hint_refresh_is_forwarded( let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index eb0beb6f66..746f5aeeaf 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -14,7 +14,7 @@ use gpui::{ use language::{ language_settings::{AllLanguageSettings, Formatter}, tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, LanguageConfig, - LineEnding, OffsetRangeExt, Point, Rope, + LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope, }; use live_kit_client::MacOSDisplay; use lsp::LanguageServerId; @@ -2246,7 +2246,10 @@ async fn test_propagate_saves_and_fs_changes( let rust = Arc::new(Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -2254,7 +2257,10 @@ async fn test_propagate_saves_and_fs_changes( let javascript = Arc::new(Language::new( LanguageConfig { name: "JavaScript".into(), - path_suffixes: vec!["js".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["js".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -3783,7 +3789,10 @@ async fn test_collaborating_with_diagnostics( let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -4061,7 +4070,10 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering( let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -4290,7 +4302,10 @@ async fn test_formatting_buffer( let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -4395,7 +4410,10 @@ async fn test_prettier_formatting_buffer( let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, prettier_parser_name: Some("test_parser".to_string()), ..Default::default() }, @@ -4511,7 +4529,10 @@ async fn test_definition( let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -4655,7 +4676,10 @@ async fn test_references( let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -4852,7 +4876,10 @@ async fn test_document_highlights( let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -4955,7 +4982,10 @@ async fn test_lsp_hover( let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -5051,7 +5081,10 @@ async fn test_project_symbols( let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -5160,7 +5193,10 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it( let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), diff --git a/crates/collab/src/tests/random_project_collaboration_tests.rs b/crates/collab/src/tests/random_project_collaboration_tests.rs index 53d47eb6b5..37103a3382 100644 --- a/crates/collab/src/tests/random_project_collaboration_tests.rs +++ b/crates/collab/src/tests/random_project_collaboration_tests.rs @@ -8,7 +8,9 @@ use editor::Bias; use fs::{repository::GitFileStatus, FakeFs, Fs as _}; use futures::StreamExt; use gpui::{BackgroundExecutor, Model, TestAppContext}; -use language::{range_to_lsp, FakeLspAdapter, Language, LanguageConfig, PointUtf16}; +use language::{ + range_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, PointUtf16, +}; use lsp::FakeLanguageServer; use pretty_assertions::assert_eq; use project::{search::SearchQuery, Project, ProjectPath}; @@ -1022,7 +1024,10 @@ impl RandomizedTest for ProjectCollaborationTest { let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, None, diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 23507eab3e..f975a833c1 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -1003,7 +1003,7 @@ pub mod tests { use gpui::{div, font, observe, px, AppContext, Context, Element, Hsla}; use language::{ language_settings::{AllLanguageSettings, AllLanguageSettingsContent}, - Buffer, Language, LanguageConfig, SelectionGoal, + Buffer, Language, LanguageConfig, LanguageMatcher, SelectionGoal, }; use project::Project; use rand::{prelude::*, Rng}; @@ -1453,7 +1453,10 @@ pub mod tests { Language::new( LanguageConfig { name: "Test".into(), - path_suffixes: vec![".test".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec![".test".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -1540,7 +1543,10 @@ pub mod tests { Language::new( LanguageConfig { name: "Test".into(), - path_suffixes: vec![".test".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec![".test".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -1608,7 +1614,10 @@ pub mod tests { Language::new( LanguageConfig { name: "Test".into(), - path_suffixes: vec![".test".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec![".test".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 6b23e13d15..865fcb1a5c 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -15,7 +15,8 @@ use language::{ language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent}, BracketPairConfig, Capability::ReadWrite, - FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry, Override, Point, + FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageRegistry, + Override, Point, }; use parking_lot::Mutex; use project::project_settings::{LspSettings, ProjectSettings}; @@ -5077,7 +5078,10 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -5196,7 +5200,10 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -5318,7 +5325,10 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, // Enable Prettier formatting for the same buffer, and ensure // LSP is called instead of Prettier. prettier_parser_name: Some("test_parser".to_string()), @@ -7747,7 +7757,10 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) { let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, brackets: BracketPairConfig { pairs: vec![BracketPair { start: "{".to_string(), @@ -7859,7 +7872,10 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test let mut language = Language::new( LanguageConfig { name: Arc::clone(&language_name), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -8086,7 +8102,10 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui: let mut cx = EditorLspTestContext::new( Language::new( LanguageConfig { - path_suffixes: vec!["jsx".into()], + matcher: LanguageMatcher { + path_suffixes: vec!["jsx".into()], + ..Default::default() + }, overrides: [( "element".into(), LanguageConfigOverride { @@ -8187,7 +8206,10 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) { let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, prettier_parser_name: Some("test_parser".to_string()), ..Default::default() }, diff --git a/crates/editor/src/highlight_matching_bracket.rs b/crates/editor/src/highlight_matching_bracket.rs index 1ed7700f37..787be1999e 100644 --- a/crates/editor/src/highlight_matching_bracket.rs +++ b/crates/editor/src/highlight_matching_bracket.rs @@ -35,7 +35,7 @@ mod tests { use super::*; use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext}; use indoc::indoc; - use language::{BracketPair, BracketPairConfig, Language, LanguageConfig}; + use language::{BracketPair, BracketPairConfig, Language, LanguageConfig, LanguageMatcher}; #[gpui::test] async fn test_matching_bracket_highlights(cx: &mut gpui::TestAppContext) { @@ -45,7 +45,10 @@ mod tests { Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, brackets: BracketPairConfig { pairs: vec![ BracketPair { diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index bc90b6face..dc2616c6ef 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -1240,7 +1240,7 @@ pub mod tests { use itertools::Itertools; use language::{ language_settings::AllLanguageSettingsContent, Capability, FakeLspAdapter, Language, - LanguageConfig, + LanguageConfig, LanguageMatcher, }; use lsp::FakeLanguageServer; use parking_lot::Mutex; @@ -1529,7 +1529,10 @@ pub mod tests { let mut language = Language::new( LanguageConfig { name: name.into(), - path_suffixes: vec![path_suffix.to_string()], + matcher: LanguageMatcher { + path_suffixes: vec![path_suffix.to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -2209,7 +2212,10 @@ pub mod tests { let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -2500,7 +2506,10 @@ pub mod tests { let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -2848,7 +2857,10 @@ pub mod tests { let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -3079,7 +3091,10 @@ pub mod tests { let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -3319,7 +3334,10 @@ pub mod tests { let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index 70c1699b83..b083e63890 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -12,7 +12,9 @@ use collections::HashSet; use futures::Future; use gpui::{View, ViewContext, VisualTestContext}; use indoc::indoc; -use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageQueries}; +use language::{ + point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageQueries, +}; use lsp::{notification, request}; use multi_buffer::ToPointUtf16; use project::Project; @@ -115,7 +117,10 @@ impl EditorLspTestContext { let language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -160,7 +165,10 @@ impl EditorLspTestContext { let language = Language::new( LanguageConfig { name: "Typescript".into(), - path_suffixes: vec!["ts".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["ts".to_string()], + ..Default::default() + }, brackets: language::BracketPairConfig { pairs: vec![language::BracketPair { start: "{".to_string(), diff --git a/crates/extension/Cargo.toml b/crates/extension/Cargo.toml new file mode 100644 index 0000000000..092f7b0bec --- /dev/null +++ b/crates/extension/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "extension" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lib] +path = "src/extension_store.rs" + +[dependencies] +anyhow.workspace = true +collections.workspace = true +fs.workspace = true +futures.workspace = true +gpui.workspace = true +language.workspace = true +parking_lot.workspace = true +serde.workspace = true +serde_json.workspace = true +theme.workspace = true +toml.workspace = true +util.workspace = true + +[dev-dependencies] +fs = { workspace = true, features = ["test-support"] } +gpui = { workspace = true, features = ["test-support"] } +language = { workspace = true, features = ["test-support"] } diff --git a/crates/extension/LICENSE-GPL b/crates/extension/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/extension/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/extension/src/extension_store.rs b/crates/extension/src/extension_store.rs new file mode 100644 index 0000000000..9b9892641d --- /dev/null +++ b/crates/extension/src/extension_store.rs @@ -0,0 +1,344 @@ +use anyhow::Result; +use collections::{HashMap, HashSet}; +use fs::Fs; +use futures::StreamExt as _; +use gpui::{actions, AppContext, Context, Global, Model, ModelContext, Task}; +use language::{ + LanguageConfig, LanguageMatcher, LanguageQueries, LanguageRegistry, QUERY_FILENAME_PREFIXES, +}; +use parking_lot::RwLock; +use serde::{Deserialize, Serialize}; +use std::{ + ffi::OsStr, + path::{Path, PathBuf}, + sync::Arc, + time::Duration, +}; +use theme::ThemeRegistry; +use util::{paths::EXTENSIONS_DIR, ResultExt}; + +#[cfg(test)] +mod extension_store_test; + +pub struct ExtensionStore { + manifest: Arc>, + fs: Arc, + extensions_dir: PathBuf, + manifest_path: PathBuf, + language_registry: Arc, + theme_registry: Arc, + _watch_extensions_dir: Task<()>, +} + +struct GlobalExtensionStore(Model); + +impl Global for GlobalExtensionStore {} + +#[derive(Deserialize, Serialize, Default)] +pub struct Manifest { + pub grammars: HashMap, + pub languages: HashMap, LanguageManifestEntry>, + pub themes: HashMap, +} + +#[derive(PartialEq, Eq, Debug, PartialOrd, Ord, Deserialize, Serialize)] +pub struct GrammarManifestEntry { + extension: String, + path: PathBuf, +} + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)] +pub struct LanguageManifestEntry { + extension: String, + path: PathBuf, + matcher: LanguageMatcher, +} + +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct ThemeManifestEntry { + extension: String, + path: PathBuf, +} + +actions!(zed, [ReloadExtensions]); + +pub fn init( + fs: Arc, + language_registry: Arc, + theme_registry: Arc, + cx: &mut AppContext, +) { + let store = cx.new_model(|cx| { + ExtensionStore::new( + EXTENSIONS_DIR.clone(), + fs.clone(), + language_registry.clone(), + theme_registry, + cx, + ) + }); + + cx.on_action(|_: &ReloadExtensions, cx| { + let store = cx.global::().0.clone(); + store + .update(cx, |store, cx| store.reload(cx)) + .detach_and_log_err(cx); + }); + + cx.set_global(GlobalExtensionStore(store)); +} + +impl ExtensionStore { + pub fn new( + extensions_dir: PathBuf, + fs: Arc, + language_registry: Arc, + theme_registry: Arc, + cx: &mut ModelContext, + ) -> Self { + let mut this = Self { + manifest: Default::default(), + extensions_dir: extensions_dir.join("installed"), + manifest_path: extensions_dir.join("manifest.json"), + fs, + language_registry, + theme_registry, + _watch_extensions_dir: Task::ready(()), + }; + this._watch_extensions_dir = this.watch_extensions_dir(cx); + this.load(cx); + this + } + + pub fn load(&mut self, cx: &mut ModelContext) { + let (manifest_content, manifest_metadata, extensions_metadata) = + cx.background_executor().block(async { + futures::join!( + self.fs.load(&self.manifest_path), + self.fs.metadata(&self.manifest_path), + self.fs.metadata(&self.extensions_dir), + ) + }); + + if let Some(manifest_content) = manifest_content.log_err() { + if let Some(manifest) = serde_json::from_str(&manifest_content).log_err() { + self.manifest_updated(manifest, cx); + } + } + + let should_reload = if let (Ok(Some(manifest_metadata)), Ok(Some(extensions_metadata))) = + (manifest_metadata, extensions_metadata) + { + extensions_metadata.mtime > manifest_metadata.mtime + } else { + true + }; + + if should_reload { + self.reload(cx).detach_and_log_err(cx); + } + } + + fn manifest_updated(&mut self, manifest: Manifest, cx: &mut ModelContext) { + for (grammar_name, grammar) in &manifest.grammars { + let mut grammar_path = self.extensions_dir.clone(); + grammar_path.extend([grammar.extension.as_ref(), grammar.path.as_path()]); + self.language_registry + .register_grammar(grammar_name.clone(), grammar_path); + } + for (language_name, language) in &manifest.languages { + let mut language_path = self.extensions_dir.clone(); + language_path.extend([language.extension.as_ref(), language.path.as_path()]); + self.language_registry.register_extension( + language_path.into(), + language_name.clone(), + language.matcher.clone(), + load_plugin_queries, + ); + } + let fs = self.fs.clone(); + let root_dir = self.extensions_dir.clone(); + let theme_registry = self.theme_registry.clone(); + let themes = manifest.themes.clone(); + cx.background_executor() + .spawn(async move { + for theme in themes.values() { + let mut theme_path = root_dir.clone(); + theme_path.extend([theme.extension.as_ref(), theme.path.as_path()]); + + theme_registry + .load_user_theme(&theme_path, fs.clone()) + .await + .log_err(); + } + }) + .detach(); + *self.manifest.write() = manifest; + } + + fn watch_extensions_dir(&self, cx: &mut ModelContext) -> Task<()> { + let manifest = self.manifest.clone(); + let fs = self.fs.clone(); + let language_registry = self.language_registry.clone(); + let extensions_dir = self.extensions_dir.clone(); + cx.background_executor().spawn(async move { + let mut changed_languages = HashSet::default(); + let mut events = fs.watch(&extensions_dir, Duration::from_millis(250)).await; + while let Some(events) = events.next().await { + changed_languages.clear(); + let manifest = manifest.read(); + for event in events { + for (language_name, language) in &manifest.languages { + let mut language_path = extensions_dir.clone(); + language_path + .extend([language.extension.as_ref(), language.path.as_path()]); + if event.path.starts_with(&language_path) || event.path == language_path { + changed_languages.insert(language_name.clone()); + } + } + } + language_registry.reload_languages(&changed_languages); + } + }) + } + + pub fn reload(&mut self, cx: &mut ModelContext) -> Task> { + let fs = self.fs.clone(); + let extensions_dir = self.extensions_dir.clone(); + let manifest_path = self.manifest_path.clone(); + cx.spawn(|this, mut cx| async move { + let manifest = cx + .background_executor() + .spawn(async move { + let mut manifest = Manifest::default(); + + let mut extension_paths = fs.read_dir(&extensions_dir).await?; + while let Some(extension_dir) = extension_paths.next().await { + let extension_dir = extension_dir?; + let Some(extension_name) = + extension_dir.file_name().and_then(OsStr::to_str) + else { + continue; + }; + + if let Ok(mut grammar_paths) = + fs.read_dir(&extension_dir.join("grammars")).await + { + while let Some(grammar_path) = grammar_paths.next().await { + let grammar_path = grammar_path?; + let Ok(relative_path) = grammar_path.strip_prefix(&extension_dir) + else { + continue; + }; + let Some(grammar_name) = + grammar_path.file_stem().and_then(OsStr::to_str) + else { + continue; + }; + + manifest.grammars.insert( + grammar_name.into(), + GrammarManifestEntry { + extension: extension_name.into(), + path: relative_path.into(), + }, + ); + } + } + + if let Ok(mut language_paths) = + fs.read_dir(&extension_dir.join("languages")).await + { + while let Some(language_path) = language_paths.next().await { + let language_path = language_path?; + let Ok(relative_path) = language_path.strip_prefix(&extension_dir) + else { + continue; + }; + let config = fs.load(&language_path.join("config.toml")).await?; + let config = ::toml::from_str::(&config)?; + + manifest.languages.insert( + config.name.clone(), + LanguageManifestEntry { + extension: extension_name.into(), + path: relative_path.into(), + matcher: config.matcher, + }, + ); + } + } + + if let Ok(mut theme_paths) = + fs.read_dir(&extension_dir.join("themes")).await + { + while let Some(theme_path) = theme_paths.next().await { + let theme_path = theme_path?; + let Ok(relative_path) = theme_path.strip_prefix(&extension_dir) + else { + continue; + }; + + let Some(theme_family) = + ThemeRegistry::read_user_theme(&theme_path, fs.clone()) + .await + .log_err() + else { + continue; + }; + + for theme in theme_family.themes { + let location = ThemeManifestEntry { + extension: extension_name.into(), + path: relative_path.into(), + }; + + manifest.themes.insert(theme.name, location); + } + } + } + } + + fs.save( + &manifest_path, + &serde_json::to_string_pretty(&manifest)?.as_str().into(), + Default::default(), + ) + .await?; + + anyhow::Ok(manifest) + }) + .await?; + this.update(&mut cx, |this, cx| this.manifest_updated(manifest, cx)) + }) + } +} + +fn load_plugin_queries(root_path: &Path) -> LanguageQueries { + let mut result = LanguageQueries::default(); + if let Some(entries) = std::fs::read_dir(root_path).log_err() { + for entry in entries { + let Some(entry) = entry.log_err() else { + continue; + }; + let path = entry.path(); + if let Some(remainder) = path.strip_prefix(root_path).ok().and_then(|p| p.to_str()) { + if !remainder.ends_with(".scm") { + continue; + } + for (name, query) in QUERY_FILENAME_PREFIXES { + if remainder.starts_with(name) { + if let Some(contents) = std::fs::read_to_string(&path).log_err() { + match query(&mut result) { + None => *query(&mut result) = Some(contents.into()), + Some(r) => r.to_mut().push_str(contents.as_ref()), + } + } + break; + } + } + } + } + } + result +} diff --git a/crates/extension/src/extension_store_test.rs b/crates/extension/src/extension_store_test.rs new file mode 100644 index 0000000000..e95496b529 --- /dev/null +++ b/crates/extension/src/extension_store_test.rs @@ -0,0 +1,295 @@ +use crate::{ + ExtensionStore, GrammarManifestEntry, LanguageManifestEntry, Manifest, ThemeManifestEntry, +}; +use fs::FakeFs; +use gpui::{Context, TestAppContext}; +use language::{LanguageMatcher, LanguageRegistry}; +use serde_json::json; +use std::{path::PathBuf, sync::Arc}; +use theme::ThemeRegistry; + +#[gpui::test] +async fn test_extension_store(cx: &mut TestAppContext) { + let fs = FakeFs::new(cx.executor()); + + fs.insert_tree( + "/the-extension-dir", + json!({ + "installed": { + "zed-monokai": { + "themes": { + "monokai.json": r#"{ + "name": "Monokai", + "author": "Someone", + "themes": [ + { + "name": "Monokai Dark", + "appearance": "dark", + "style": {} + }, + { + "name": "Monokai Light", + "appearance": "light", + "style": {} + } + ] + }"#, + "monokai-pro.json": r#"{ + "name": "Monokai Pro", + "author": "Someone", + "themes": [ + { + "name": "Monokai Pro Dark", + "appearance": "dark", + "style": {} + }, + { + "name": "Monokai Pro Light", + "appearance": "light", + "style": {} + } + ] + }"#, + } + }, + "zed-ruby": { + "grammars": { + "ruby.wasm": "", + "embedded_template.wasm": "", + }, + "languages": { + "ruby": { + "config.toml": r#" + name = "Ruby" + grammar = "ruby" + path_suffixes = ["rb"] + "#, + "highlights.scm": "", + }, + "erb": { + "config.toml": r#" + name = "ERB" + grammar = "embedded_template" + path_suffixes = ["erb"] + "#, + "highlights.scm": "", + } + }, + } + } + }), + ) + .await; + + let mut expected_manifest = Manifest { + grammars: [ + ( + "embedded_template".into(), + GrammarManifestEntry { + extension: "zed-ruby".into(), + path: "grammars/embedded_template.wasm".into(), + }, + ), + ( + "ruby".into(), + GrammarManifestEntry { + extension: "zed-ruby".into(), + path: "grammars/ruby.wasm".into(), + }, + ), + ] + .into_iter() + .collect(), + languages: [ + ( + "ERB".into(), + LanguageManifestEntry { + extension: "zed-ruby".into(), + path: "languages/erb".into(), + matcher: LanguageMatcher { + path_suffixes: vec!["erb".into()], + first_line_pattern: None, + }, + }, + ), + ( + "Ruby".into(), + LanguageManifestEntry { + extension: "zed-ruby".into(), + path: "languages/ruby".into(), + matcher: LanguageMatcher { + path_suffixes: vec!["rb".into()], + first_line_pattern: None, + }, + }, + ), + ] + .into_iter() + .collect(), + themes: [ + ( + "Monokai Dark".into(), + ThemeManifestEntry { + extension: "zed-monokai".into(), + path: "themes/monokai.json".into(), + }, + ), + ( + "Monokai Light".into(), + ThemeManifestEntry { + extension: "zed-monokai".into(), + path: "themes/monokai.json".into(), + }, + ), + ( + "Monokai Pro Dark".into(), + ThemeManifestEntry { + extension: "zed-monokai".into(), + path: "themes/monokai-pro.json".into(), + }, + ), + ( + "Monokai Pro Light".into(), + ThemeManifestEntry { + extension: "zed-monokai".into(), + path: "themes/monokai-pro.json".into(), + }, + ), + ] + .into_iter() + .collect(), + }; + + let language_registry = Arc::new(LanguageRegistry::test()); + let theme_registry = Arc::new(ThemeRegistry::new(Box::new(()))); + + let store = cx.new_model(|cx| { + ExtensionStore::new( + PathBuf::from("/the-extension-dir"), + fs.clone(), + language_registry.clone(), + theme_registry.clone(), + cx, + ) + }); + + cx.executor().run_until_parked(); + store.read_with(cx, |store, _| { + let manifest = store.manifest.read(); + assert_eq!(manifest.grammars, expected_manifest.grammars); + assert_eq!(manifest.languages, expected_manifest.languages); + assert_eq!(manifest.themes, expected_manifest.themes); + + assert_eq!( + language_registry.language_names(), + ["ERB", "Plain Text", "Ruby"] + ); + assert_eq!( + theme_registry.list_names(false), + [ + "Monokai Dark", + "Monokai Light", + "Monokai Pro Dark", + "Monokai Pro Light", + "One Dark", + ] + ); + }); + + fs.insert_tree( + "/the-extension-dir/installed/zed-gruvbox", + json!({ + "themes": { + "gruvbox.json": r#"{ + "name": "Gruvbox", + "author": "Someone Else", + "themes": [ + { + "name": "Gruvbox", + "appearance": "dark", + "style": {} + } + ] + }"#, + } + }), + ) + .await; + + expected_manifest.themes.insert( + "Gruvbox".into(), + ThemeManifestEntry { + extension: "zed-gruvbox".into(), + path: "themes/gruvbox.json".into(), + }, + ); + + store + .update(cx, |store, cx| store.reload(cx)) + .await + .unwrap(); + + cx.executor().run_until_parked(); + store.read_with(cx, |store, _| { + let manifest = store.manifest.read(); + assert_eq!(manifest.grammars, expected_manifest.grammars); + assert_eq!(manifest.languages, expected_manifest.languages); + assert_eq!(manifest.themes, expected_manifest.themes); + + assert_eq!( + theme_registry.list_names(false), + [ + "Gruvbox", + "Monokai Dark", + "Monokai Light", + "Monokai Pro Dark", + "Monokai Pro Light", + "One Dark", + ] + ); + }); + + let prev_fs_metadata_call_count = fs.metadata_call_count(); + let prev_fs_read_dir_call_count = fs.read_dir_call_count(); + + // Create new extension store, as if Zed were restarting. + drop(store); + let store = cx.new_model(|cx| { + ExtensionStore::new( + PathBuf::from("/the-extension-dir"), + fs.clone(), + language_registry.clone(), + theme_registry.clone(), + cx, + ) + }); + + cx.executor().run_until_parked(); + store.read_with(cx, |store, _| { + let manifest = store.manifest.read(); + assert_eq!(manifest.grammars, expected_manifest.grammars); + assert_eq!(manifest.languages, expected_manifest.languages); + assert_eq!(manifest.themes, expected_manifest.themes); + + assert_eq!( + language_registry.language_names(), + ["ERB", "Plain Text", "Ruby"] + ); + assert_eq!( + theme_registry.list_names(false), + [ + "Gruvbox", + "Monokai Dark", + "Monokai Light", + "Monokai Pro Dark", + "Monokai Pro Light", + "One Dark", + ] + ); + + // The on-disk manifest limits the number of FS calls that need to be made + // on startup. + assert_eq!(fs.read_dir_call_count(), prev_fs_read_dir_call_count); + assert_eq!(fs.metadata_call_count(), prev_fs_metadata_call_count + 2); + }); +} diff --git a/crates/gpui/src/shared_string.rs b/crates/gpui/src/shared_string.rs index 8c12c1c970..1aa1bcae95 100644 --- a/crates/gpui/src/shared_string.rs +++ b/crates/gpui/src/shared_string.rs @@ -5,7 +5,7 @@ use util::arc_cow::ArcCow; /// A shared string is an immutable string that can be cheaply cloned in GPUI /// tasks. Essentially an abstraction over an `Arc` and `&'static str`, -#[derive(Deref, DerefMut, Eq, PartialEq, Hash, Clone)] +#[derive(Deref, DerefMut, Eq, PartialEq, PartialOrd, Ord, Hash, Clone)] pub struct SharedString(ArcCow<'static, str>); impl Default for SharedString { diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 821fa80840..328327a212 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -52,6 +52,7 @@ smol.workspace = true sum_tree.workspace = true text.workspace = true theme.workspace = true +toml.workspace = true tree-sitter-rust = { workspace = true, optional = true } tree-sitter-typescript = { workspace = true, optional = true } pulldown-cmark.workspace = true diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 05cec88126..ccbab9d6d1 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -73,7 +73,10 @@ fn test_select_language() { registry.add(Arc::new(Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -81,7 +84,10 @@ fn test_select_language() { registry.add(Arc::new(Language::new( LanguageConfig { name: "Make".into(), - path_suffixes: vec!["Makefile".to_string(), "mk".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["Makefile".to_string(), "mk".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -2322,7 +2328,10 @@ fn ruby_lang() -> Language { Language::new( LanguageConfig { name: "Ruby".into(), - path_suffixes: vec!["rb".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rb".to_string()], + ..Default::default() + }, line_comments: vec!["# ".into()], ..Default::default() }, @@ -2370,7 +2379,10 @@ fn erb_lang() -> Language { Language::new( LanguageConfig { name: "ERB".into(), - path_suffixes: vec!["erb".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["erb".to_string()], + ..Default::default() + }, block_comment: Some(("<%#".into(), "%>".into())), ..Default::default() }, @@ -2398,7 +2410,10 @@ fn rust_lang() -> Language { Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -2450,7 +2465,10 @@ fn json_lang() -> Language { Language::new( LanguageConfig { name: "Json".into(), - path_suffixes: vec!["js".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["js".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_json::language()), diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 70e85761d2..eee05ea8e2 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -20,7 +20,7 @@ pub mod markdown; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; -use collections::{HashMap, HashSet}; +use collections::{hash_map, HashMap, HashSet}; use futures::{ channel::{mpsc, oneshot}, future::Shared, @@ -33,12 +33,13 @@ use lsp::{CodeActionKind, LanguageServerBinary}; use parking_lot::{Mutex, RwLock}; use postage::watch; use regex::Regex; -use serde::{de, Deserialize, Deserializer}; +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use serde_json::Value; use std::{ any::Any, borrow::Cow, cell::RefCell, + ffi::OsStr, fmt::Debug, hash::Hash, mem, @@ -392,14 +393,13 @@ pub struct LanguageConfig { /// Human-readable name of the language. pub name: Arc, // The name of the grammar in a WASM bundle (experimental). - pub grammar_name: Option>, - /// Given a list of `LanguageConfig`'s, the language of a file can be determined based on the path extension matching any of the `path_suffixes`. - pub path_suffixes: Vec, + pub grammar: Option>, + /// The criteria for matching this language to a given file. + #[serde(flatten)] + pub matcher: LanguageMatcher, /// List of bracket types in a language. + #[serde(default)] pub brackets: BracketPairConfig, - /// A regex pattern that determines whether the language should be assigned to a file or not. - #[serde(default, deserialize_with = "deserialize_regex")] - pub first_line_pattern: Option, /// If set to true, auto indentation uses last non empty line to determine /// the indentation level for a new line. #[serde(default = "auto_indent_using_last_non_empty_line_default")] @@ -443,6 +443,34 @@ pub struct LanguageConfig { pub prettier_parser_name: Option, } +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct LanguageMatcher { + /// Given a list of `LanguageConfig`'s, the language of a file can be determined based on the path extension matching any of the `path_suffixes`. + #[serde(default)] + pub path_suffixes: Vec, + /// A regex pattern that determines whether the language should be assigned to a file or not. + #[serde( + default, + serialize_with = "serialize_regex", + deserialize_with = "deserialize_regex" + )] + pub first_line_pattern: Option, +} + +pub const QUERY_FILENAME_PREFIXES: &[( + &str, + fn(&mut LanguageQueries) -> &mut Option>, +)] = &[ + ("highlights", |q| &mut q.highlights), + ("brackets", |q| &mut q.brackets), + ("outline", |q| &mut q.outline), + ("indents", |q| &mut q.indents), + ("embedding", |q| &mut q.embedding), + ("injections", |q| &mut q.injections), + ("overrides", |q| &mut q.overrides), + ("redactions", |q| &mut q.redactions), +]; + /// Tree-sitter language queries for a given language. #[derive(Debug, Default)] pub struct LanguageQueries { @@ -506,11 +534,10 @@ impl Default for LanguageConfig { fn default() -> Self { Self { name: "".into(), - grammar_name: None, - path_suffixes: Default::default(), + grammar: None, + matcher: LanguageMatcher::default(), brackets: Default::default(), auto_indent_using_last_non_empty_line: auto_indent_using_last_non_empty_line_default(), - first_line_pattern: Default::default(), increase_indent_pattern: Default::default(), decrease_indent_pattern: Default::default(), autoclose_before: Default::default(), @@ -538,6 +565,16 @@ fn deserialize_regex<'de, D: Deserializer<'de>>(d: D) -> Result, D } } +fn serialize_regex(regex: &Option, serializer: S) -> Result +where + S: Serializer, +{ + match regex { + Some(regex) => serializer.serialize_str(regex.as_str()), + None => serializer.serialize_none(), + } +} + #[doc(hidden)] #[cfg(any(test, feature = "test-support"))] pub struct FakeLspAdapter { @@ -702,22 +739,29 @@ type AvailableLanguageId = usize; #[derive(Clone)] struct AvailableLanguage { id: AvailableLanguageId, - config: LanguageConfig, - grammar: AvailableGrammar, + name: Arc, + source: AvailableLanguageSource, lsp_adapters: Vec>, loaded: bool, } -#[derive(Clone)] enum AvailableGrammar { - Native { - grammar: tree_sitter::Language, + Loaded(tree_sitter::Language), + Loading(Vec>>), + Unloaded(PathBuf), +} + +#[derive(Clone)] +enum AvailableLanguageSource { + BuiltIn { asset_dir: &'static str, get_queries: fn(&str) -> LanguageQueries, + config: LanguageConfig, }, - Wasm { + Extension { path: Arc, get_queries: fn(&Path) -> LanguageQueries, + matcher: LanguageMatcher, }, } @@ -737,6 +781,7 @@ struct LanguageRegistryState { next_language_server_id: usize, languages: Vec>, available_languages: Vec, + grammars: HashMap, next_available_language_id: AvailableLanguageId, loading_languages: HashMap>>>>, subscription: (watch::Sender<()>, watch::Receiver<()>), @@ -758,6 +803,7 @@ impl LanguageRegistry { next_language_server_id: 0, languages: vec![PLAIN_TEXT.clone()], available_languages: Default::default(), + grammars: Default::default(), next_available_language_id: 0, loading_languages: Default::default(), subscription: watch::channel(), @@ -787,20 +833,24 @@ impl LanguageRegistry { self.state.write().reload(); } + /// Clear out the given languages and reload them from scratch. + pub fn reload_languages(&self, languages: &HashSet>) { + self.state.write().reload_languages(languages); + } + pub fn register( &self, asset_dir: &'static str, config: LanguageConfig, - grammar: tree_sitter::Language, lsp_adapters: Vec>, get_queries: fn(&str) -> LanguageQueries, ) { let state = &mut *self.state.write(); state.available_languages.push(AvailableLanguage { id: post_inc(&mut state.next_available_language_id), - config, - grammar: AvailableGrammar::Native { - grammar, + name: config.name.clone(), + source: AvailableLanguageSource::BuiltIn { + config, get_queries, asset_dir, }, @@ -809,28 +859,63 @@ impl LanguageRegistry { }); } - pub fn register_wasm( + pub fn register_extension( &self, path: Arc, - config: LanguageConfig, + name: Arc, + matcher: LanguageMatcher, get_queries: fn(&Path) -> LanguageQueries, ) { let state = &mut *self.state.write(); + let source = AvailableLanguageSource::Extension { + path, + get_queries, + matcher, + }; + for existing_language in &mut state.available_languages { + if existing_language.name == name + && matches!( + existing_language.source, + AvailableLanguageSource::Extension { .. } + ) + { + existing_language.source = source; + return; + } + } state.available_languages.push(AvailableLanguage { id: post_inc(&mut state.next_available_language_id), - config, - grammar: AvailableGrammar::Wasm { path, get_queries }, + name, + source, lsp_adapters: Vec::new(), loaded: false, }); } + pub fn add_grammars( + &self, + grammars: impl IntoIterator, tree_sitter::Language)>, + ) { + self.state.write().grammars.extend( + grammars + .into_iter() + .map(|(name, grammar)| (name.into(), AvailableGrammar::Loaded(grammar))), + ); + } + + pub fn register_grammar(&self, name: String, path: PathBuf) { + self.state + .write() + .grammars + .insert(name, AvailableGrammar::Unloaded(path)); + } + pub fn language_names(&self) -> Vec { let state = self.state.read(); let mut result = state .available_languages .iter() - .filter_map(|l| l.loaded.not().then_some(l.config.name.to_string())) + .filter_map(|l| l.loaded.not().then_some(l.name.to_string())) .chain(state.languages.iter().map(|l| l.config.name.to_string())) .collect::>(); result.sort_unstable_by_key(|language_name| language_name.to_lowercase()); @@ -873,7 +958,7 @@ impl LanguageRegistry { name: &str, ) -> UnwrapFuture>>> { let name = UniCase::new(name); - self.get_or_load_language(|config| UniCase::new(config.name.as_ref()) == name) + self.get_or_load_language(|language_name, _| UniCase::new(language_name) == name) } pub fn language_for_name_or_extension( @@ -881,8 +966,8 @@ impl LanguageRegistry { string: &str, ) -> UnwrapFuture>>> { let string = UniCase::new(string); - self.get_or_load_language(|config| { - UniCase::new(config.name.as_ref()) == string + self.get_or_load_language(|name, config| { + UniCase::new(name) == string || config .path_suffixes .iter() @@ -899,7 +984,7 @@ impl LanguageRegistry { let filename = path.file_name().and_then(|name| name.to_str()); let extension = path.extension_or_hidden_file_name(); let path_suffixes = [extension, filename]; - self.get_or_load_language(|config| { + self.get_or_load_language(|_, config| { let path_matches = config .path_suffixes .iter() @@ -919,7 +1004,7 @@ impl LanguageRegistry { fn get_or_load_language( self: &Arc, - callback: impl Fn(&LanguageConfig) -> bool, + callback: impl Fn(&str, &LanguageMatcher) -> bool, ) -> UnwrapFuture>>> { let (tx, rx) = oneshot::channel(); @@ -927,52 +1012,60 @@ impl LanguageRegistry { if let Some(language) = state .languages .iter() - .find(|language| callback(&language.config)) + .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() - .find(|l| !l.loaded && callback(&l.config)) + .rfind(|l| { + !l.loaded + && match &l.source { + AvailableLanguageSource::BuiltIn { config, .. } => { + callback(l.name.as_ref(), &config.matcher) + } + AvailableLanguageSource::Extension { matcher, .. } => { + callback(l.name.as_ref(), &matcher) + } + } + }) .cloned() { - let txs = state - .loading_languages - .entry(language.id) - .or_insert_with(|| { + 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.config.name.clone(); + let name = language.name.clone(); let language = async { - let (grammar, queries) = match language.grammar { - AvailableGrammar::Native { - grammar, + let (config, queries) = match language.source { + AvailableLanguageSource::BuiltIn { asset_dir, get_queries, - } => (grammar, (get_queries)(asset_dir)), - AvailableGrammar::Wasm { path, get_queries } => { - let grammar_name = - &language.config.grammar_name.as_ref().ok_or_else( - || anyhow!("missing grammar name"), - )?; - let mut wasm_path = path.join(grammar_name.as_ref()); - wasm_path.set_extension("wasm"); - let wasm_bytes = std::fs::read(&wasm_path)?; - 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 - })?; - (grammar, get_queries(path.as_ref())) + config, + } => (config, (get_queries)(asset_dir)), + AvailableLanguageSource::Extension { + path, + get_queries, + .. + } => { + let config = std::fs::read(path.join("config.toml")); + let config: LanguageConfig = + ::toml::from_slice(&config?)?; + (config, get_queries(path.as_ref())) } }; - Language::new(language.config, Some(grammar)) + + let grammar = if let Some(grammar) = config.grammar.clone() { + Some(this.get_or_load_grammar(grammar).await?) + } else { + None + }; + + Language::new(config, grammar) .with_lsp_adapters(language.lsp_adapters) .await .with_queries(queries) @@ -1009,10 +1102,9 @@ impl LanguageRegistry { }; }) .detach(); - - Vec::new() - }); - txs.push(tx); + entry.insert(vec![tx]); + } + } } else { let _ = tx.send(Err(anyhow!("language not found"))); } @@ -1023,6 +1115,65 @@ impl LanguageRegistry { rx.unwrap() } + fn get_or_load_grammar( + self: &Arc, + name: Arc, + ) -> UnwrapFuture>> { + let (tx, rx) = oneshot::channel(); + let mut state = self.state.write(); + + if let Some(grammar) = state.grammars.get_mut(name.as_ref()) { + match grammar { + AvailableGrammar::Loaded(grammar) => { + tx.send(Ok(grammar.clone())).ok(); + } + AvailableGrammar::Loading(txs) => { + txs.push(tx); + } + AvailableGrammar::Unloaded(wasm_path) => { + if let Some(executor) = &self.executor { + let this = self.clone(); + let wasm_path = wasm_path.clone(); + executor + .spawn(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.to_string(), + AvailableGrammar::Loaded(grammar.clone()), + ) + { + for tx in txs { + tx.send(Ok(grammar.clone())).ok(); + } + } + + anyhow::Ok(()) + }) + .detach(); + *grammar = AvailableGrammar::Loading(vec![tx]); + } + } + } + } else { + tx.send(Err(anyhow!("no such grammar {}", name))).ok(); + } + + rx.unwrap() + } + pub fn to_vec(&self) -> Vec> { self.state.read().languages.iter().cloned().collect() } @@ -1206,6 +1357,19 @@ impl LanguageRegistryState { *self.subscription.0.borrow_mut() = (); } + fn reload_languages(&mut self, languages: &HashSet>) { + self.languages + .retain(|language| !languages.contains(&language.config.name)); + self.version += 1; + self.reload_count += 1; + for language in &mut self.available_languages { + if languages.contains(&language.name) { + language.loaded = false; + } + } + *self.subscription.0.borrow_mut() = (); + } + /// Mark the given language a having been loaded, so that the /// language registry won't try to load it again. fn mark_language_loaded(&mut self, id: AvailableLanguageId) { @@ -1720,7 +1884,7 @@ impl Language { } pub fn path_suffixes(&self) -> &[String] { - &self.config.path_suffixes + &self.config.matcher.path_suffixes } pub fn should_autoclose_before(&self, c: char) -> bool { @@ -1911,6 +2075,33 @@ impl CodeLabel { } } +impl Ord for LanguageMatcher { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.path_suffixes.cmp(&other.path_suffixes).then_with(|| { + self.first_line_pattern + .as_ref() + .map(Regex::as_str) + .cmp(&other.first_line_pattern.as_ref().map(Regex::as_str)) + }) + } +} + +impl PartialOrd for LanguageMatcher { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Eq for LanguageMatcher {} + +impl PartialEq for LanguageMatcher { + fn eq(&self, other: &Self) -> bool { + self.path_suffixes == other.path_suffixes + && self.first_line_pattern.as_ref().map(Regex::as_str) + == other.first_line_pattern.as_ref().map(Regex::as_str) + } +} + #[cfg(any(test, feature = "test-support"))] impl Default for FakeLspAdapter { fn default() -> Self { @@ -2034,11 +2225,12 @@ mod tests { "/javascript", LanguageConfig { name: "JavaScript".into(), - path_suffixes: vec!["js".into()], - first_line_pattern: Some(Regex::new(r"\bnode\b").unwrap()), + matcher: LanguageMatcher { + path_suffixes: vec!["js".into()], + first_line_pattern: Some(Regex::new(r"\bnode\b").unwrap()), + }, ..Default::default() }, - tree_sitter_typescript::language_tsx(), vec![], |_| Default::default(), ); @@ -2067,14 +2259,21 @@ mod tests { let mut languages = LanguageRegistry::test(); languages.set_executor(cx.executor()); let languages = Arc::new(languages); + languages.add_grammars([ + ("json", tree_sitter_json::language()), + ("rust", tree_sitter_rust::language()), + ]); languages.register( "/JSON", LanguageConfig { name: "JSON".into(), - path_suffixes: vec!["json".into()], + grammar: Some("json".into()), + matcher: LanguageMatcher { + path_suffixes: vec!["json".into()], + ..Default::default() + }, ..Default::default() }, - tree_sitter_json::language(), vec![], |_| Default::default(), ); @@ -2082,10 +2281,13 @@ mod tests { "/rust", LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".into()], + grammar: Some("rust".into()), + matcher: LanguageMatcher { + path_suffixes: vec!["rs".into()], + ..Default::default() + }, ..Default::default() }, - tree_sitter_rust::language(), vec![], |_| Default::default(), ); diff --git a/crates/language/src/syntax_map/syntax_map_tests.rs b/crates/language/src/syntax_map/syntax_map_tests.rs index abf52c64e5..0601a9f3c1 100644 --- a/crates/language/src/syntax_map/syntax_map_tests.rs +++ b/crates/language/src/syntax_map/syntax_map_tests.rs @@ -1,5 +1,5 @@ use super::*; -use crate::LanguageConfig; +use crate::{LanguageConfig, LanguageMatcher}; use rand::rngs::StdRng; use std::{env, ops::Range, sync::Arc}; use text::{Buffer, BufferId}; @@ -1092,7 +1092,10 @@ fn html_lang() -> Language { Language::new( LanguageConfig { name: "HTML".into(), - path_suffixes: vec!["html".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["html".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_html::language()), @@ -1111,7 +1114,10 @@ fn ruby_lang() -> Language { Language::new( LanguageConfig { name: "Ruby".into(), - path_suffixes: vec!["rb".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rb".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_ruby::language()), @@ -1130,7 +1136,10 @@ fn erb_lang() -> Language { Language::new( LanguageConfig { name: "ERB".into(), - path_suffixes: vec!["erb".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["erb".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_embedded_template::language()), @@ -1163,7 +1172,10 @@ fn rust_lang() -> Language { Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -1189,7 +1201,10 @@ fn markdown_lang() -> Language { Language::new( LanguageConfig { name: "Markdown".into(), - path_suffixes: vec!["md".into()], + matcher: LanguageMatcher { + path_suffixes: vec!["md".into()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_markdown::language()), @@ -1209,7 +1224,10 @@ fn elixir_lang() -> Language { Language::new( LanguageConfig { name: "Elixir".into(), - path_suffixes: vec!["ex".into()], + matcher: LanguageMatcher { + path_suffixes: vec!["ex".into()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_elixir::language()), @@ -1226,7 +1244,10 @@ fn heex_lang() -> Language { Language::new( LanguageConfig { name: "HEEx".into(), - path_suffixes: vec!["heex".into()], + matcher: LanguageMatcher { + path_suffixes: vec!["heex".into()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_heex::language()), diff --git a/crates/language_tools/src/lsp_log_tests.rs b/crates/language_tools/src/lsp_log_tests.rs index 24b23df880..b00d1bb79a 100644 --- a/crates/language_tools/src/lsp_log_tests.rs +++ b/crates/language_tools/src/lsp_log_tests.rs @@ -5,7 +5,9 @@ use crate::lsp_log::LogMenuItem; use super::*; use futures::StreamExt; use gpui::{Context, TestAppContext, VisualTestContext}; -use language::{tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, LanguageServerName}; +use language::{ + tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageServerName, +}; use project::{FakeFs, Project}; use serde_json::json; use settings::SettingsStore; @@ -21,7 +23,10 @@ async fn test_lsp_logs(cx: &mut TestAppContext) { let mut rust_language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 2e222c24fb..4cb321f9c0 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -5,7 +5,7 @@ use gpui::AppContext; use language::{ language_settings::{AllLanguageSettings, LanguageSettingsContent}, tree_sitter_rust, tree_sitter_typescript, Diagnostic, FakeLspAdapter, LanguageConfig, - LineEnding, OffsetRangeExt, Point, ToPoint, + LanguageMatcher, LineEnding, OffsetRangeExt, Point, ToPoint, }; use lsp::Url; use parking_lot::Mutex; @@ -149,7 +149,10 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { let mut rust_language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -157,7 +160,10 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { let mut json_language = Language::new( LanguageConfig { name: "JSON".into(), - path_suffixes: vec!["json".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["json".to_string()], + ..Default::default() + }, ..Default::default() }, None, @@ -535,7 +541,10 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -970,7 +979,10 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -1102,7 +1114,10 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC let progress_token = "the-progress-token"; let mut language = Language::new( LanguageConfig { - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, None, @@ -1183,7 +1198,10 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp let mut language = Language::new( LanguageConfig { - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, None, @@ -1272,7 +1290,10 @@ async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::T let mut language = Language::new( LanguageConfig { - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, None, @@ -1322,7 +1343,10 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) { let mut rust = Language::new( LanguageConfig { name: Arc::from("Rust"), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, None, @@ -1336,7 +1360,10 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) { let mut js = Language::new( LanguageConfig { name: Arc::from("JavaScript"), - path_suffixes: vec!["js".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["js".to_string()], + ..Default::default() + }, ..Default::default() }, None, @@ -1451,7 +1478,10 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -1862,7 +1892,10 @@ async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui::TestAppContext) { let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -2249,7 +2282,10 @@ async fn test_definition(cx: &mut gpui::TestAppContext) { let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -2350,7 +2386,10 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) { let mut language = Language::new( LanguageConfig { name: "TypeScript".into(), - path_suffixes: vec!["ts".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["ts".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_typescript::language_typescript()), @@ -2447,7 +2486,10 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) { let mut language = Language::new( LanguageConfig { name: "TypeScript".into(), - path_suffixes: vec!["ts".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["ts".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_typescript::language_typescript()), @@ -2513,7 +2555,10 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { let mut language = Language::new( LanguageConfig { name: "TypeScript".into(), - path_suffixes: vec!["ts".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["ts".to_string()], + ..Default::default() + }, ..Default::default() }, None, @@ -2816,14 +2861,18 @@ async fn test_save_as(cx: &mut gpui::TestAppContext) { let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; let languages = project.update(cx, |project, _| project.languages().clone()); + languages.add_grammars([("rust", tree_sitter_rust::language())]); languages.register( "/some/path", LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".into()], + grammar: Some("rust".into()), + matcher: LanguageMatcher { + path_suffixes: vec!["rs".into()], + ..Default::default() + }, ..Default::default() }, - tree_sitter_rust::language(), vec![], |_| Default::default(), ); @@ -3649,7 +3698,10 @@ async fn test_rename(cx: &mut gpui::TestAppContext) { let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 8aa7fac359..bb09741f07 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -260,7 +260,7 @@ mod tests { use super::*; use futures::StreamExt; use gpui::{TestAppContext, VisualContext}; - use language::{FakeLspAdapter, Language, LanguageConfig}; + use language::{FakeLspAdapter, Language, LanguageConfig, LanguageMatcher}; use project::FakeFs; use serde_json::json; use settings::SettingsStore; @@ -273,7 +273,10 @@ mod tests { let mut language = Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, None, diff --git a/crates/semantic_index/src/semantic_index_tests.rs b/crates/semantic_index/src/semantic_index_tests.rs index 979151d4b2..23ed45ff1d 100644 --- a/crates/semantic_index/src/semantic_index_tests.rs +++ b/crates/semantic_index/src/semantic_index_tests.rs @@ -7,7 +7,7 @@ use crate::{ use ai::test::FakeEmbeddingProvider; use gpui::{Task, TestAppContext}; -use language::{Language, LanguageConfig, LanguageRegistry, ToOffset}; +use language::{Language, LanguageConfig, LanguageMatcher, LanguageRegistry, ToOffset}; use parking_lot::Mutex; use pretty_assertions::assert_eq; use project::{project_settings::ProjectSettings, FakeFs, Fs, Project}; @@ -1251,7 +1251,10 @@ fn js_lang() -> Arc { Language::new( LanguageConfig { name: "Javascript".into(), - path_suffixes: vec!["js".into()], + matcher: LanguageMatcher { + path_suffixes: vec!["js".into()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_typescript::language_tsx()), @@ -1343,7 +1346,10 @@ fn rust_lang() -> Arc { Language::new( LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".into()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".into()], + ..Default::default() + }, collapsed_placeholder: " /* ... */ ".to_string(), ..Default::default() }, @@ -1393,7 +1399,10 @@ fn json_lang() -> Arc { Language::new( LanguageConfig { name: "JSON".into(), - path_suffixes: vec!["json".into()], + matcher: LanguageMatcher { + path_suffixes: vec!["json".into()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_json::language()), @@ -1421,7 +1430,10 @@ fn toml_lang() -> Arc { Arc::new(Language::new( LanguageConfig { name: "TOML".into(), - path_suffixes: vec!["toml".into()], + matcher: LanguageMatcher { + path_suffixes: vec!["toml".into()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_toml::language()), @@ -1433,7 +1445,10 @@ fn cpp_lang() -> Arc { Language::new( LanguageConfig { name: "CPP".into(), - path_suffixes: vec!["cpp".into()], + matcher: LanguageMatcher { + path_suffixes: vec!["cpp".into()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_cpp::language()), @@ -1513,7 +1528,10 @@ fn lua_lang() -> Arc { Language::new( LanguageConfig { name: "Lua".into(), - path_suffixes: vec!["lua".into()], + matcher: LanguageMatcher { + path_suffixes: vec!["lua".into()], + ..Default::default() + }, collapsed_placeholder: "--[ ... ]--".to_string(), ..Default::default() }, @@ -1542,7 +1560,10 @@ fn php_lang() -> Arc { Language::new( LanguageConfig { name: "PHP".into(), - path_suffixes: vec!["php".into()], + matcher: LanguageMatcher { + path_suffixes: vec!["php".into()], + ..Default::default() + }, collapsed_placeholder: "/* ... */".into(), ..Default::default() }, @@ -1597,7 +1618,10 @@ fn ruby_lang() -> Arc { Language::new( LanguageConfig { name: "Ruby".into(), - path_suffixes: vec!["rb".into()], + matcher: LanguageMatcher { + path_suffixes: vec!["rb".into()], + ..Default::default() + }, collapsed_placeholder: "# ...".to_string(), ..Default::default() }, @@ -1638,7 +1662,10 @@ fn elixir_lang() -> Arc { Language::new( LanguageConfig { name: "Elixir".into(), - path_suffixes: vec!["rs".into()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".into()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_elixir::language()), diff --git a/crates/theme/src/registry.rs b/crates/theme/src/registry.rs index 2cc4d902a2..5a86c877f9 100644 --- a/crates/theme/src/registry.rs +++ b/crates/theme/src/registry.rs @@ -194,7 +194,9 @@ impl ThemeRegistry { } pub fn list_names(&self, _staff: bool) -> Vec { - self.state.read().themes.keys().cloned().collect() + let mut names = self.state.read().themes.keys().cloned().collect::>(); + names.sort(); + names } pub fn list(&self, _staff: bool) -> Vec { @@ -263,11 +265,17 @@ impl ThemeRegistry { Ok(()) } - /// Loads the user theme from the specified path and adds it to the registry. - pub async fn load_user_theme(&self, theme_path: &Path, fs: Arc) -> Result<()> { + pub async fn read_user_theme(theme_path: &Path, fs: Arc) -> Result { let reader = fs.open_sync(&theme_path).await?; let theme = serde_json_lenient::from_reader(reader)?; + Ok(theme) + } + + /// Loads the user theme from the specified path and adds it to the registry. + pub async fn load_user_theme(&self, theme_path: &Path, fs: Arc) -> Result<()> { + let theme = Self::read_user_theme(theme_path, fs).await?; + self.insert_user_theme_families([theme]); Ok(()) diff --git a/crates/util/src/arc_cow.rs b/crates/util/src/arc_cow.rs index c6afabbbaa..02ad1fa1f0 100644 --- a/crates/util/src/arc_cow.rs +++ b/crates/util/src/arc_cow.rs @@ -1,5 +1,6 @@ use std::{ borrow::Cow, + cmp::Ordering, fmt::{self, Debug}, hash::{Hash, Hasher}, sync::Arc, @@ -18,6 +19,18 @@ impl<'a, T: ?Sized + PartialEq> PartialEq for ArcCow<'a, T> { } } +impl<'a, T: ?Sized + PartialOrd> PartialOrd for ArcCow<'a, T> { + fn partial_cmp(&self, other: &Self) -> Option { + self.as_ref().partial_cmp(other.as_ref()) + } +} + +impl<'a, T: ?Sized + Ord> Ord for ArcCow<'a, T> { + fn cmp(&self, other: &Self) -> Ordering { + self.as_ref().cmp(other.as_ref()) + } +} + impl<'a, T: ?Sized + Eq> Eq for ArcCow<'a, T> {} impl<'a, T: ?Sized + Hash> Hash for ArcCow<'a, T> { diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index cd839ae50e..dcf9270fb1 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -14,7 +14,7 @@ lazy_static::lazy_static! { pub static ref THEMES_DIR: PathBuf = HOME.join(".config/zed/themes"); pub static ref LOGS_DIR: PathBuf = HOME.join("Library/Logs/Zed"); pub static ref SUPPORT_DIR: PathBuf = HOME.join("Library/Application Support/Zed"); - pub static ref PLUGINS_DIR: PathBuf = HOME.join("Library/Application Support/Zed/plugins"); + pub static ref EXTENSIONS_DIR: PathBuf = HOME.join("Library/Application Support/Zed/extensions"); pub static ref LANGUAGES_DIR: PathBuf = HOME.join("Library/Application Support/Zed/languages"); pub static ref COPILOT_DIR: PathBuf = HOME.join("Library/Application Support/Zed/copilot"); pub static ref DEFAULT_PRETTIER_DIR: PathBuf = HOME.join("Library/Application Support/Zed/prettier"); diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index c023faf6f7..1e9a5ba31e 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -44,6 +44,7 @@ db.workspace = true diagnostics.workspace = true editor.workspace = true env_logger.workspace = true +extension.workspace = true feature_flags.workspace = true feedback.workspace = true file_finder.workspace = true diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 1250aa92c7..2931d53763 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -4,8 +4,8 @@ pub use language::*; use node_runtime::NodeRuntime; use rust_embed::RustEmbed; use settings::Settings; -use std::{borrow::Cow, fs, path::Path, str, sync::Arc}; -use util::{asset_str, paths::PLUGINS_DIR, ResultExt}; +use std::{str, sync::Arc}; +use util::asset_str; use self::{deno::DenoSettings, elixir::ElixirSettings}; @@ -62,30 +62,69 @@ pub fn init( ElixirSettings::register(cx); DenoSettings::register(cx); - let language = |name, grammar, adapters| { - languages.register(name, load_config(name), grammar, adapters, load_queries) + languages.add_grammars([ + ("bash", tree_sitter_bash::language()), + ("beancount", tree_sitter_beancount::language()), + ("c", tree_sitter_c::language()), + ("c_sharp", tree_sitter_c_sharp::language()), + ("cpp", tree_sitter_cpp::language()), + ("css", tree_sitter_css::language()), + ("elixir", tree_sitter_elixir::language()), + ("elm", tree_sitter_elm::language()), + ( + "embedded_template", + tree_sitter_embedded_template::language(), + ), + ("erlang", tree_sitter_erlang::language()), + ("gitcommit", tree_sitter_gitcommit::language()), + ("gleam", tree_sitter_gleam::language()), + ("glsl", tree_sitter_glsl::language()), + ("go", tree_sitter_go::language()), + ("gomod", tree_sitter_gomod::language()), + ("gowork", tree_sitter_gowork::language()), + ("haskell", tree_sitter_haskell::language()), + ("hcl", tree_sitter_hcl::language()), + ("heex", tree_sitter_heex::language()), + ("html", tree_sitter_html::language()), + ("json", tree_sitter_json::language()), + ("lua", tree_sitter_lua::language()), + ("markdown", tree_sitter_markdown::language()), + ("nix", tree_sitter_nix::language()), + ("nu", tree_sitter_nu::language()), + ("ocaml", tree_sitter_ocaml::language_ocaml()), + ( + "ocaml_interface", + tree_sitter_ocaml::language_ocaml_interface(), + ), + ("php", tree_sitter_php::language_php()), + ("proto", tree_sitter_proto::language()), + ("purescript", tree_sitter_purescript::language()), + ("python", tree_sitter_python::language()), + ("racket", tree_sitter_racket::language()), + ("ruby", tree_sitter_ruby::language()), + ("rust", tree_sitter_rust::language()), + ("scheme", tree_sitter_scheme::language()), + ("svelte", tree_sitter_svelte::language()), + ("toml", tree_sitter_toml::language()), + ("tsx", tree_sitter_typescript::language_tsx()), + ("typescript", tree_sitter_typescript::language_typescript()), + ("uiua", tree_sitter_uiua::language()), + ("vue", tree_sitter_vue::language()), + ("yaml", tree_sitter_yaml::language()), + ("zig", tree_sitter_zig::language()), + ]); + + let language = |name: &'static str, adapters| { + languages.register(name, load_config(name), adapters, load_queries) }; - language("bash", tree_sitter_bash::language(), vec![]); - language("beancount", tree_sitter_beancount::language(), vec![]); - language( - "c", - tree_sitter_c::language(), - vec![Arc::new(c::CLspAdapter) as Arc], - ); - language( - "cpp", - tree_sitter_cpp::language(), - vec![Arc::new(c::CLspAdapter)], - ); - language( - "csharp", - tree_sitter_c_sharp::language(), - vec![Arc::new(csharp::OmniSharpAdapter {})], - ); + language("bash", vec![]); + language("beancount", vec![]); + language("c", vec![Arc::new(c::CLspAdapter) as Arc]); + language("cpp", vec![Arc::new(c::CLspAdapter)]); + language("csharp", vec![Arc::new(csharp::OmniSharpAdapter {})]); language( "css", - tree_sitter_css::language(), vec![ Arc::new(css::CssLspAdapter::new(node_runtime.clone())), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), @@ -95,53 +134,32 @@ pub fn init( match &ElixirSettings::get(None, cx).lsp { elixir::ElixirLspSetting::ElixirLs => language( "elixir", - tree_sitter_elixir::language(), vec![ Arc::new(elixir::ElixirLspAdapter), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), ], ), - elixir::ElixirLspSetting::NextLs => language( - "elixir", - tree_sitter_elixir::language(), - vec![Arc::new(elixir::NextLspAdapter)], - ), + elixir::ElixirLspSetting::NextLs => { + language("elixir", vec![Arc::new(elixir::NextLspAdapter)]) + } elixir::ElixirLspSetting::Local { path, arguments } => language( "elixir", - tree_sitter_elixir::language(), vec![Arc::new(elixir::LocalLspAdapter { path: path.clone(), arguments: arguments.clone(), })], ), } - language("gitcommit", tree_sitter_gitcommit::language(), vec![]); - language( - "erlang", - tree_sitter_erlang::language(), - vec![Arc::new(erlang::ErlangLspAdapter)], - ); + language("gitcommit", vec![]); + language("erlang", vec![Arc::new(erlang::ErlangLspAdapter)]); - language( - "gleam", - tree_sitter_gleam::language(), - vec![Arc::new(gleam::GleamLspAdapter)], - ); - language( - "go", - tree_sitter_go::language(), - vec![Arc::new(go::GoLspAdapter)], - ); - language("gomod", tree_sitter_gomod::language(), vec![]); - language("gowork", tree_sitter_gowork::language(), vec![]); - language( - "zig", - tree_sitter_zig::language(), - vec![Arc::new(zig::ZlsAdapter)], - ); + language("gleam", vec![Arc::new(gleam::GleamLspAdapter)]); + language("go", vec![Arc::new(go::GoLspAdapter)]); + language("gomod", vec![]); + language("gowork", vec![]); + language("zig", vec![Arc::new(zig::ZlsAdapter)]); language( "heex", - tree_sitter_heex::language(), vec![ Arc::new(elixir::ElixirLspAdapter), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), @@ -149,48 +167,32 @@ pub fn init( ); language( "json", - tree_sitter_json::language(), vec![Arc::new(json::JsonLspAdapter::new( node_runtime.clone(), languages.clone(), ))], ); - language("markdown", tree_sitter_markdown::language(), vec![]); + language("markdown", vec![]); language( "python", - tree_sitter_python::language(), vec![Arc::new(python::PythonLspAdapter::new( node_runtime.clone(), ))], ); - language( - "rust", - tree_sitter_rust::language(), - vec![Arc::new(rust::RustLspAdapter)], - ); - language( - "toml", - tree_sitter_toml::language(), - vec![Arc::new(toml::TaploLspAdapter)], - ); + language("rust", vec![Arc::new(rust::RustLspAdapter)]); + language("toml", vec![Arc::new(toml::TaploLspAdapter)]); match &DenoSettings::get(None, cx).enable { true => { language( "tsx", - tree_sitter_typescript::language_tsx(), vec![ Arc::new(deno::DenoLspAdapter::new()), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), ], ); - language( - "typescript", - tree_sitter_typescript::language_typescript(), - vec![Arc::new(deno::DenoLspAdapter::new())], - ); + language("typescript", vec![Arc::new(deno::DenoLspAdapter::new())]); language( "javascript", - tree_sitter_typescript::language_tsx(), vec![ Arc::new(deno::DenoLspAdapter::new()), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), @@ -200,7 +202,6 @@ pub fn init( false => { language( "tsx", - tree_sitter_typescript::language_tsx(), vec![ Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), @@ -209,7 +210,6 @@ pub fn init( ); language( "typescript", - tree_sitter_typescript::language_typescript(), vec![ Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), @@ -217,7 +217,6 @@ pub fn init( ); language( "javascript", - tree_sitter_typescript::language_tsx(), vec![ Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), @@ -227,47 +226,31 @@ pub fn init( } } - language( - "haskell", - tree_sitter_haskell::language(), - vec![Arc::new(haskell::HaskellLanguageServer {})], - ); + language("haskell", vec![Arc::new(haskell::HaskellLanguageServer {})]); language( "html", - tree_sitter_html::language(), vec![ Arc::new(html::HtmlLspAdapter::new(node_runtime.clone())), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), ], ); - language( - "ruby", - tree_sitter_ruby::language(), - vec![Arc::new(ruby::RubyLanguageServer)], - ); + language("ruby", vec![Arc::new(ruby::RubyLanguageServer)]); language( "erb", - tree_sitter_embedded_template::language(), vec![ Arc::new(ruby::RubyLanguageServer), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), ], ); - language("scheme", tree_sitter_scheme::language(), vec![]); - language("racket", tree_sitter_racket::language(), vec![]); - language( - "lua", - tree_sitter_lua::language(), - vec![Arc::new(lua::LuaLspAdapter)], - ); + language("scheme", vec![]); + language("racket", vec![]); + language("lua", vec![Arc::new(lua::LuaLspAdapter)]); language( "yaml", - tree_sitter_yaml::language(), vec![Arc::new(yaml::YamlLspAdapter::new(node_runtime.clone()))], ); language( "svelte", - tree_sitter_svelte::language(), vec![ Arc::new(svelte::SvelteLspAdapter::new(node_runtime.clone())), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), @@ -275,7 +258,6 @@ pub fn init( ); language( "php", - tree_sitter_php::language_php(), vec![ Arc::new(php::IntelephenseLspAdapter::new(node_runtime.clone())), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), @@ -284,62 +266,24 @@ pub fn init( language( "purescript", - tree_sitter_purescript::language(), vec![Arc::new(purescript::PurescriptLspAdapter::new( node_runtime.clone(), ))], ); language( "elm", - tree_sitter_elm::language(), vec![Arc::new(elm::ElmLspAdapter::new(node_runtime.clone()))], ); - language("glsl", tree_sitter_glsl::language(), vec![]); - language("nix", tree_sitter_nix::language(), vec![]); - language( - "nu", - tree_sitter_nu::language(), - vec![Arc::new(nu::NuLanguageServer {})], - ); - language( - "ocaml", - tree_sitter_ocaml::language_ocaml(), - vec![Arc::new(ocaml::OCamlLspAdapter)], - ); - language( - "ocaml-interface", - tree_sitter_ocaml::language_ocaml_interface(), - vec![Arc::new(ocaml::OCamlLspAdapter)], - ); - language( - "vue", - tree_sitter_vue::language(), - vec![Arc::new(vue::VueLspAdapter::new(node_runtime))], - ); - language( - "uiua", - tree_sitter_uiua::language(), - vec![Arc::new(uiua::UiuaLanguageServer {})], - ); - language("proto", tree_sitter_proto::language(), vec![]); - language("terraform", tree_sitter_hcl::language(), vec![]); - language("hcl", tree_sitter_hcl::language(), vec![]); - - if let Ok(children) = std::fs::read_dir(&*PLUGINS_DIR) { - for child in children { - if let Ok(child) = child { - let path = child.path(); - let config_path = path.join("config.toml"); - if let Ok(config) = std::fs::read(&config_path) { - languages.register_wasm( - path.into(), - ::toml::from_slice(&config).unwrap(), - load_plugin_queries, - ); - } - } - } - } + language("glsl", vec![]); + language("nix", vec![]); + language("nu", vec![Arc::new(nu::NuLanguageServer {})]); + language("ocaml", vec![Arc::new(ocaml::OCamlLspAdapter)]); + language("ocaml-interface", vec![Arc::new(ocaml::OCamlLspAdapter)]); + language("vue", vec![Arc::new(vue::VueLspAdapter::new(node_runtime))]); + language("uiua", vec![Arc::new(uiua::UiuaLanguageServer {})]); + language("proto", vec![]); + language("terraform", vec![]); + language("hcl", vec![]); } #[cfg(any(test, feature = "test-support"))] @@ -367,20 +311,6 @@ fn load_config(name: &str) -> LanguageConfig { .unwrap() } -const QUERY_FILENAME_PREFIXES: &[( - &str, - fn(&mut LanguageQueries) -> &mut Option>, -)] = &[ - ("highlights", |q| &mut q.highlights), - ("brackets", |q| &mut q.brackets), - ("outline", |q| &mut q.outline), - ("indents", |q| &mut q.indents), - ("embedding", |q| &mut q.embedding), - ("injections", |q| &mut q.injections), - ("overrides", |q| &mut q.overrides), - ("redactions", |q| &mut q.redactions), -]; - fn load_queries(name: &str) -> LanguageQueries { let mut result = LanguageQueries::default(); for path in LanguageDir::iter() { @@ -401,32 +331,3 @@ fn load_queries(name: &str) -> LanguageQueries { } result } - -fn load_plugin_queries(root_path: &Path) -> LanguageQueries { - let mut result = LanguageQueries::default(); - if let Some(entries) = fs::read_dir(root_path).log_err() { - for entry in entries { - let Some(entry) = entry.log_err() else { - continue; - }; - let path = entry.path(); - if let Some(remainder) = path.strip_prefix(root_path).ok().and_then(|p| p.to_str()) { - if !remainder.ends_with(".scm") { - continue; - } - for (name, query) in QUERY_FILENAME_PREFIXES { - if remainder.starts_with(name) { - if let Some(contents) = fs::read_to_string(&path).log_err() { - match query(&mut result) { - None => *query(&mut result) = Some(contents.into()), - Some(r) => r.to_mut().push_str(contents.as_ref()), - } - } - break; - } - } - } - } - } - result -} diff --git a/crates/zed/src/languages/bash/config.toml b/crates/zed/src/languages/bash/config.toml index 8b86318ecd..abbb95bda5 100644 --- a/crates/zed/src/languages/bash/config.toml +++ b/crates/zed/src/languages/bash/config.toml @@ -1,4 +1,5 @@ name = "Shell Script" +grammar = "bash" path_suffixes = ["sh", "bash", "bashrc", "bash_profile", "bash_aliases", "bash_logout", "profile", "zsh", "zshrc", "zshenv", "zsh_profile", "zsh_aliases", "zsh_histfile", "zlogin", "zprofile", ".env"] line_comments = ["# "] first_line_pattern = "^#!.*\\b(?:ba|z)?sh\\b" diff --git a/crates/zed/src/languages/beancount/config.toml b/crates/zed/src/languages/beancount/config.toml index 722b8a39b7..fff6411e9d 100644 --- a/crates/zed/src/languages/beancount/config.toml +++ b/crates/zed/src/languages/beancount/config.toml @@ -1,3 +1,4 @@ name = "Beancount" +grammar = "beancount" path_suffixes = ["beancount"] brackets = [{ start = "\"", end = "\"", close = false, newline = false }] diff --git a/crates/zed/src/languages/c/config.toml b/crates/zed/src/languages/c/config.toml index f99c0416cd..b41f469bd5 100644 --- a/crates/zed/src/languages/c/config.toml +++ b/crates/zed/src/languages/c/config.toml @@ -1,4 +1,5 @@ name = "C" +grammar = "c" path_suffixes = ["c"] line_comments = ["// "] autoclose_before = ";:.,=}])>" diff --git a/crates/zed/src/languages/cpp/config.toml b/crates/zed/src/languages/cpp/config.toml index 7630f1dd79..eecb09bc20 100644 --- a/crates/zed/src/languages/cpp/config.toml +++ b/crates/zed/src/languages/cpp/config.toml @@ -1,4 +1,5 @@ name = "C++" +grammar = "cpp" path_suffixes = ["cc", "cpp", "h", "hpp", "cxx", "hxx", "inl"] line_comments = ["// "] autoclose_before = ";:.,=}])>" diff --git a/crates/zed/src/languages/csharp/config.toml b/crates/zed/src/languages/csharp/config.toml index 7835283df4..51a10c70e0 100644 --- a/crates/zed/src/languages/csharp/config.toml +++ b/crates/zed/src/languages/csharp/config.toml @@ -1,4 +1,5 @@ name = "CSharp" +grammar = "c_sharp" path_suffixes = ["cs"] line_comments = ["// "] autoclose_before = ";:.,=}])>" diff --git a/crates/zed/src/languages/css/config.toml b/crates/zed/src/languages/css/config.toml index 24a844c239..e22abe6d70 100644 --- a/crates/zed/src/languages/css/config.toml +++ b/crates/zed/src/languages/css/config.toml @@ -1,4 +1,5 @@ name = "CSS" +grammar = "css" path_suffixes = ["css"] autoclose_before = ";:.,=}])>" brackets = [ diff --git a/crates/zed/src/languages/elixir/config.toml b/crates/zed/src/languages/elixir/config.toml index a3aab0ee8a..81e92d45b0 100644 --- a/crates/zed/src/languages/elixir/config.toml +++ b/crates/zed/src/languages/elixir/config.toml @@ -1,4 +1,5 @@ name = "Elixir" +grammar = "elixir" path_suffixes = ["ex", "exs"] line_comments = ["# "] autoclose_before = ";:.,=}])>" diff --git a/crates/zed/src/languages/elm/config.toml b/crates/zed/src/languages/elm/config.toml index b95c85d444..96fb2989ad 100644 --- a/crates/zed/src/languages/elm/config.toml +++ b/crates/zed/src/languages/elm/config.toml @@ -1,4 +1,5 @@ name = "Elm" +grammar = "elm" path_suffixes = ["elm"] line_comments = ["-- "] block_comment = ["{- ", " -}"] diff --git a/crates/zed/src/languages/erb/config.toml b/crates/zed/src/languages/erb/config.toml index ebc45e9984..5ec987e139 100644 --- a/crates/zed/src/languages/erb/config.toml +++ b/crates/zed/src/languages/erb/config.toml @@ -1,4 +1,5 @@ name = "ERB" +grammar = "embedded_template" path_suffixes = ["erb"] autoclose_before = ">})" brackets = [ diff --git a/crates/zed/src/languages/erlang/config.toml b/crates/zed/src/languages/erlang/config.toml index 5f92c0fe27..01eba93ff6 100644 --- a/crates/zed/src/languages/erlang/config.toml +++ b/crates/zed/src/languages/erlang/config.toml @@ -1,4 +1,5 @@ name = "Erlang" +grammar = "erlang" # TODO: support parsing rebar.config files # # https://github.com/WhatsApp/tree-sitter-erlang/issues/3 path_suffixes = ["erl", "hrl", "app.src", "escript", "xrl", "yrl", "Emakefile", "rebar.config"] diff --git a/crates/zed/src/languages/gitcommit/config.toml b/crates/zed/src/languages/gitcommit/config.toml index 70c68d9385..0b4d84069e 100644 --- a/crates/zed/src/languages/gitcommit/config.toml +++ b/crates/zed/src/languages/gitcommit/config.toml @@ -1,4 +1,5 @@ name = "Git Commit" +grammar = "git_commit" path_suffixes = [ # Refer to https://github.com/neovim/neovim/blob/master/runtime/lua/vim/filetype.lua#L1286-L1290 "TAG_EDITMSG", diff --git a/crates/zed/src/languages/gleam/config.toml b/crates/zed/src/languages/gleam/config.toml index a841862709..0a472172ad 100644 --- a/crates/zed/src/languages/gleam/config.toml +++ b/crates/zed/src/languages/gleam/config.toml @@ -1,4 +1,5 @@ name = "Gleam" +grammar = "gleam" path_suffixes = ["gleam"] line_comments = ["// ", "/// "] autoclose_before = ";:.,=}])>" diff --git a/crates/zed/src/languages/glsl/config.toml b/crates/zed/src/languages/glsl/config.toml index 9b60179662..01d7c88214 100644 --- a/crates/zed/src/languages/glsl/config.toml +++ b/crates/zed/src/languages/glsl/config.toml @@ -1,4 +1,5 @@ name = "GLSL" +grammar = "glsl" path_suffixes = ["vert", "frag", "tesc", "tese", "geom", "comp"] line_comments = ["// "] block_comment = ["/* ", " */"] diff --git a/crates/zed/src/languages/go/config.toml b/crates/zed/src/languages/go/config.toml index d07c46731a..cf29bfbb23 100644 --- a/crates/zed/src/languages/go/config.toml +++ b/crates/zed/src/languages/go/config.toml @@ -1,4 +1,5 @@ name = "Go" +grammar = "go" path_suffixes = ["go"] line_comments = ["// "] autoclose_before = ";:.,=}])>" diff --git a/crates/zed/src/languages/gomod/config.toml b/crates/zed/src/languages/gomod/config.toml index 80c252949d..19c7b20e8c 100644 --- a/crates/zed/src/languages/gomod/config.toml +++ b/crates/zed/src/languages/gomod/config.toml @@ -1,4 +1,5 @@ name = "Go Mod" +grammar = "go" path_suffixes = ["mod"] line_comments = ["//"] autoclose_before = ")" diff --git a/crates/zed/src/languages/gowork/config.toml b/crates/zed/src/languages/gowork/config.toml index 919f1db512..79621ecdaf 100644 --- a/crates/zed/src/languages/gowork/config.toml +++ b/crates/zed/src/languages/gowork/config.toml @@ -1,4 +1,5 @@ name = "Go Work" +grammar = "go_work" path_suffixes = ["work"] line_comments = ["//"] autoclose_before = ")" diff --git a/crates/zed/src/languages/haskell/config.toml b/crates/zed/src/languages/haskell/config.toml index 7b1454431e..3d83ab907d 100644 --- a/crates/zed/src/languages/haskell/config.toml +++ b/crates/zed/src/languages/haskell/config.toml @@ -1,4 +1,5 @@ name = "Haskell" +grammar = "haskell" path_suffixes = ["hs"] autoclose_before = ",=)}]" line_comments = ["-- "] diff --git a/crates/zed/src/languages/hcl/config.toml b/crates/zed/src/languages/hcl/config.toml index c7c2ebf6ba..891b2f38d4 100644 --- a/crates/zed/src/languages/hcl/config.toml +++ b/crates/zed/src/languages/hcl/config.toml @@ -1,4 +1,5 @@ name = "HCL" +grammar = "hcl" path_suffixes = ["hcl"] line_comments = ["# ", "// "] block_comment = ["/*", "*/"] diff --git a/crates/zed/src/languages/heex/config.toml b/crates/zed/src/languages/heex/config.toml index 74cb5ac9ff..c28ffa16b0 100644 --- a/crates/zed/src/languages/heex/config.toml +++ b/crates/zed/src/languages/heex/config.toml @@ -1,4 +1,5 @@ name = "HEEX" +grammar = "heex" path_suffixes = ["heex"] autoclose_before = ">})" brackets = [ diff --git a/crates/zed/src/languages/html/config.toml b/crates/zed/src/languages/html/config.toml index 3c24490fc4..389020b89d 100644 --- a/crates/zed/src/languages/html/config.toml +++ b/crates/zed/src/languages/html/config.toml @@ -1,4 +1,5 @@ name = "HTML" +grammar = "html" path_suffixes = ["html", "htm", "shtml"] autoclose_before = ">})" block_comment = [""] diff --git a/crates/zed/src/languages/javascript/config.toml b/crates/zed/src/languages/javascript/config.toml index a19d2588ed..a3acd1aac0 100644 --- a/crates/zed/src/languages/javascript/config.toml +++ b/crates/zed/src/languages/javascript/config.toml @@ -1,4 +1,5 @@ name = "JavaScript" +grammar = "tsx" path_suffixes = ["js", "jsx", "mjs", "cjs"] first_line_pattern = '^#!.*\bnode\b' line_comments = ["// "] diff --git a/crates/zed/src/languages/json/config.toml b/crates/zed/src/languages/json/config.toml index eed86826d5..34b3bc8bc6 100644 --- a/crates/zed/src/languages/json/config.toml +++ b/crates/zed/src/languages/json/config.toml @@ -1,4 +1,5 @@ name = "JSON" +grammar = "json" path_suffixes = ["json"] line_comments = ["// "] autoclose_before = ",]}" diff --git a/crates/zed/src/languages/lua/config.toml b/crates/zed/src/languages/lua/config.toml index 31946f46e9..19577fed91 100644 --- a/crates/zed/src/languages/lua/config.toml +++ b/crates/zed/src/languages/lua/config.toml @@ -1,4 +1,5 @@ name = "Lua" +grammar = "lua" path_suffixes = ["lua"] line_comments = ["-- "] autoclose_before = ",]}" diff --git a/crates/zed/src/languages/markdown/config.toml b/crates/zed/src/languages/markdown/config.toml index 98691a4ef7..e44ba6ec1a 100644 --- a/crates/zed/src/languages/markdown/config.toml +++ b/crates/zed/src/languages/markdown/config.toml @@ -1,4 +1,5 @@ name = "Markdown" +grammar = "markdown" path_suffixes = ["md", "mdx"] word_characters = ["-"] brackets = [ diff --git a/crates/zed/src/languages/nix/config.toml b/crates/zed/src/languages/nix/config.toml index 20b3921271..b62ead99b3 100644 --- a/crates/zed/src/languages/nix/config.toml +++ b/crates/zed/src/languages/nix/config.toml @@ -1,4 +1,5 @@ name = "Nix" +grammar = "nix" path_suffixes = ["nix"] line_comments = ["# "] block_comment = ["/* ", " */"] diff --git a/crates/zed/src/languages/nu/config.toml b/crates/zed/src/languages/nu/config.toml index 63f5a61591..23f19cdcd2 100644 --- a/crates/zed/src/languages/nu/config.toml +++ b/crates/zed/src/languages/nu/config.toml @@ -1,4 +1,5 @@ name = "Nu" +grammar = "nu" path_suffixes = ["nu"] line_comments = ["# "] autoclose_before = ";:.,=}])>` \n\t\"" diff --git a/crates/zed/src/languages/ocaml-interface/config.toml b/crates/zed/src/languages/ocaml-interface/config.toml index f7401f774c..fdbf1aad81 100644 --- a/crates/zed/src/languages/ocaml-interface/config.toml +++ b/crates/zed/src/languages/ocaml-interface/config.toml @@ -1,4 +1,5 @@ name = "OCaml Interface" +grammar = "ocaml_interface" path_suffixes = ["mli"] block_comment = ["(* ", "*)"] autoclose_before = ";,=)}" @@ -8,6 +9,6 @@ brackets = [ { start = "[", end = "]", close = true, newline = true }, { start = "(", end = ")", close = true, newline = true }, { start = "sig", end = " end", close = true, newline = true }, - # HACK: For some reason `object` alone does not work + # HACK: For some reason `object` alone does not work { start = "object ", end = "end", close = true, newline = true }, ] diff --git a/crates/zed/src/languages/ocaml/config.toml b/crates/zed/src/languages/ocaml/config.toml index 522db6dae1..313cbb46df 100644 --- a/crates/zed/src/languages/ocaml/config.toml +++ b/crates/zed/src/languages/ocaml/config.toml @@ -1,8 +1,9 @@ name = "OCaml" +grammar = "ocaml" path_suffixes = ["ml"] block_comment = ["(* ", "*)"] autoclose_before = ";,=)}]" -brackets = [ +brackets = [ { start = "{", end = "}", close = true, newline = true }, { start = "<", end = ">", close = true, newline = true }, { start = "[", end = "]", close = true, newline = true }, diff --git a/crates/zed/src/languages/php/config.toml b/crates/zed/src/languages/php/config.toml index 9c6e9b767c..db594f8a18 100644 --- a/crates/zed/src/languages/php/config.toml +++ b/crates/zed/src/languages/php/config.toml @@ -1,4 +1,5 @@ name = "PHP" +grammar = "php" path_suffixes = ["php"] first_line_pattern = '^#!.*php' line_comments = ["// ", "# "] diff --git a/crates/zed/src/languages/proto/config.toml b/crates/zed/src/languages/proto/config.toml index 81fe1bdfb4..b8bccfd39b 100644 --- a/crates/zed/src/languages/proto/config.toml +++ b/crates/zed/src/languages/proto/config.toml @@ -1,4 +1,5 @@ name = "proto" +grammar = "proto" path_suffixes = ["proto"] line_comments = ["// "] autoclose_before = ";:.,=}])>" diff --git a/crates/zed/src/languages/purescript/config.toml b/crates/zed/src/languages/purescript/config.toml index ee9810dbb7..350f476978 100644 --- a/crates/zed/src/languages/purescript/config.toml +++ b/crates/zed/src/languages/purescript/config.toml @@ -1,4 +1,5 @@ name = "PureScript" +grammar = "purescript" path_suffixes = ["purs"] autoclose_before = ",=)}]" line_comments = ["-- "] diff --git a/crates/zed/src/languages/python/config.toml b/crates/zed/src/languages/python/config.toml index 3496f845dc..d5254eac90 100644 --- a/crates/zed/src/languages/python/config.toml +++ b/crates/zed/src/languages/python/config.toml @@ -1,4 +1,5 @@ name = "Python" +grammar = "python" path_suffixes = ["py", "pyi", "mpy"] first_line_pattern = '^#!.*\bpython[0-9.]*\b' line_comments = ["# "] diff --git a/crates/zed/src/languages/racket/config.toml b/crates/zed/src/languages/racket/config.toml index aea5ec5fce..d5975a36e5 100644 --- a/crates/zed/src/languages/racket/config.toml +++ b/crates/zed/src/languages/racket/config.toml @@ -1,4 +1,5 @@ name = "Racket" +grammar = "racket" path_suffixes = ["rkt"] line_comments = ["; "] autoclose_before = "])" diff --git a/crates/zed/src/languages/ruby/config.toml b/crates/zed/src/languages/ruby/config.toml index 8f2f1fab97..d3285d48d2 100644 --- a/crates/zed/src/languages/ruby/config.toml +++ b/crates/zed/src/languages/ruby/config.toml @@ -1,4 +1,5 @@ name = "Ruby" +grammar = "ruby" path_suffixes = [ "rb", "Gemfile", diff --git a/crates/zed/src/languages/rust/config.toml b/crates/zed/src/languages/rust/config.toml index 9382c9d78b..d01f62e354 100644 --- a/crates/zed/src/languages/rust/config.toml +++ b/crates/zed/src/languages/rust/config.toml @@ -1,4 +1,5 @@ name = "Rust" +grammar = "rust" path_suffixes = ["rs"] line_comments = ["// ", "/// ", "//! "] autoclose_before = ";:.,=}])>" diff --git a/crates/zed/src/languages/scheme/config.toml b/crates/zed/src/languages/scheme/config.toml index c080f6d2ee..abe6d043e9 100644 --- a/crates/zed/src/languages/scheme/config.toml +++ b/crates/zed/src/languages/scheme/config.toml @@ -1,4 +1,5 @@ name = "Scheme" +grammar = "scheme" path_suffixes = ["scm", "ss"] line_comments = ["; "] autoclose_before = "])" diff --git a/crates/zed/src/languages/svelte/config.toml b/crates/zed/src/languages/svelte/config.toml index ff450f3f84..a05f4f9a4f 100644 --- a/crates/zed/src/languages/svelte/config.toml +++ b/crates/zed/src/languages/svelte/config.toml @@ -1,4 +1,5 @@ name = "Svelte" +grammar = "svelte" path_suffixes = ["svelte"] block_comment = [""] autoclose_before = ";:.,=}])>" diff --git a/crates/zed/src/languages/terraform/config.toml b/crates/zed/src/languages/terraform/config.toml index 9ea5896001..09e52621a9 100644 --- a/crates/zed/src/languages/terraform/config.toml +++ b/crates/zed/src/languages/terraform/config.toml @@ -1,4 +1,5 @@ name = "Terraform" +grammar = "terraform" path_suffixes = ["tf", "tfvars"] line_comments = ["# ", "// "] block_comment = ["/*", "*/"] diff --git a/crates/zed/src/languages/toml/config.toml b/crates/zed/src/languages/toml/config.toml index 701dbbd65a..660d893ec2 100644 --- a/crates/zed/src/languages/toml/config.toml +++ b/crates/zed/src/languages/toml/config.toml @@ -1,4 +1,5 @@ name = "TOML" +grammar = "toml" path_suffixes = ["Cargo.lock", "toml"] line_comments = ["# "] autoclose_before = ",]}" diff --git a/crates/zed/src/languages/tsx/config.toml b/crates/zed/src/languages/tsx/config.toml index 6806924d4a..666c55d9a7 100644 --- a/crates/zed/src/languages/tsx/config.toml +++ b/crates/zed/src/languages/tsx/config.toml @@ -1,4 +1,5 @@ name = "TSX" +grammar = "tsx" path_suffixes = ["tsx"] line_comments = ["// "] autoclose_before = ";:.,=}])>" diff --git a/crates/zed/src/languages/typescript/config.toml b/crates/zed/src/languages/typescript/config.toml index 4d51a74098..8a9f2bc8f0 100644 --- a/crates/zed/src/languages/typescript/config.toml +++ b/crates/zed/src/languages/typescript/config.toml @@ -1,4 +1,5 @@ name = "TypeScript" +grammar = "typescript" path_suffixes = ["ts", "cts", "d.cts", "d.mts", "mts"] line_comments = ["// "] autoclose_before = ";:.,=}])>" diff --git a/crates/zed/src/languages/uiua/config.toml b/crates/zed/src/languages/uiua/config.toml index 88cd8c7ad0..31f9303659 100644 --- a/crates/zed/src/languages/uiua/config.toml +++ b/crates/zed/src/languages/uiua/config.toml @@ -1,4 +1,5 @@ name = "Uiua" +grammar = "uiua" path_suffixes = ["ua"] line_comments = ["# "] autoclose_before = ")]}\"" diff --git a/crates/zed/src/languages/vue/config.toml b/crates/zed/src/languages/vue/config.toml index c41a667b75..cf966d02d7 100644 --- a/crates/zed/src/languages/vue/config.toml +++ b/crates/zed/src/languages/vue/config.toml @@ -1,4 +1,5 @@ name = "Vue.js" +grammar = "vue" path_suffixes = ["vue"] block_comment = [""] autoclose_before = ";:.,=}])>" diff --git a/crates/zed/src/languages/yaml/config.toml b/crates/zed/src/languages/yaml/config.toml index dce8d68d66..a4588275c1 100644 --- a/crates/zed/src/languages/yaml/config.toml +++ b/crates/zed/src/languages/yaml/config.toml @@ -1,4 +1,5 @@ name = "YAML" +grammar = "yaml" path_suffixes = ["yml", "yaml"] line_comments = ["# "] autoclose_before = ",]}" diff --git a/crates/zed/src/languages/zig/config.toml b/crates/zed/src/languages/zig/config.toml index 6f72663280..2cf9cf79bc 100644 --- a/crates/zed/src/languages/zig/config.toml +++ b/crates/zed/src/languages/zig/config.toml @@ -1,4 +1,5 @@ name = "Zig" +grammar = "zig" path_suffixes = ["zig"] line_comments = ["// "] autoclose_before = ";:.,=}])>" diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index f010468d26..83eaa1de10 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -47,7 +47,7 @@ use theme::{ActiveTheme, SystemAppearance, ThemeRegistry, ThemeSettings}; use util::{ async_maybe, http::{self, HttpClient, ZedHttpClient}, - paths::{self, CRASHES_DIR, CRASHES_RETIRED_DIR, PLUGINS_DIR}, + paths::{self, CRASHES_DIR, CRASHES_RETIRED_DIR}, ResultExt, }; use uuid::Uuid; @@ -173,6 +173,8 @@ fn main() { ); assistant::init(cx); + extension::init(fs.clone(), languages.clone(), ThemeRegistry::global(cx), cx); + load_user_themes_in_background(fs.clone(), cx); watch_themes(fs.clone(), cx); @@ -976,20 +978,13 @@ fn watch_themes(fs: Arc, cx: &mut AppContext) { .detach() } +#[cfg(debug_assertions)] async fn watch_languages(fs: Arc, languages: Arc) { let reload_debounce = Duration::from_millis(250); - let mut events = fs.watch(PLUGINS_DIR.as_ref(), reload_debounce).await; - - #[cfg(debug_assertions)] - { - events = futures::stream::select( - events, - fs.watch("crates/zed/src/languages".as_ref(), reload_debounce) - .await, - ) - .boxed(); - } + let mut events = fs + .watch("crates/zed/src/languages".as_ref(), reload_debounce) + .await; while (events.next().await).is_some() { languages.reload(); @@ -1019,3 +1014,6 @@ fn watch_file_types(fs: Arc, cx: &mut AppContext) { #[cfg(not(debug_assertions))] fn watch_file_types(_fs: Arc, _cx: &mut AppContext) {} + +#[cfg(not(debug_assertions))] +async fn watch_languages(_fs: Arc, _languages: Arc) {} diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index b4ec471113..e9e64cff49 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -739,7 +739,7 @@ mod tests { actions, Action, AnyWindowHandle, AppContext, AssetSource, Entity, TestAppContext, VisualTestContext, WindowHandle, }; - use language::LanguageRegistry; + use language::{LanguageMatcher, LanguageRegistry}; use project::{project_settings::ProjectSettings, Project, ProjectPath}; use serde_json::json; use settings::{handle_settings_file_changes, watch_config_file, SettingsStore}; @@ -2742,7 +2742,10 @@ mod tests { Arc::new(language::Language::new( language::LanguageConfig { name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()),