diff --git a/Cargo.lock b/Cargo.lock index 6b794e3f7f4dd1..e207d6a65f7d7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9107,6 +9107,7 @@ dependencies = [ "lsp", "node_runtime", "parking_lot", + "paths", "pet", "pet-conda", "pet-core", diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 415ddc4e2ddedb..ea51f554104c0b 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -1306,6 +1306,7 @@ impl ExtensionStore { Path::new(language.extension.as_ref()), language.path.as_path(), ]); + let language_name = language_name.clone(); self.proxy.register_language( language_name.clone(), language.grammar.clone(), @@ -1314,7 +1315,7 @@ impl ExtensionStore { Arc::new(move || { let config = std::fs::read_to_string(language_path.join("config.toml"))?; let config: LanguageConfig = ::toml::from_str(&config)?; - let queries = load_plugin_queries(&language_path); + let queries = load_plugin_queries(&language_name, &language_path); let context_provider = std::fs::read_to_string(language_path.join("tasks.json")) .ok() @@ -1879,14 +1880,24 @@ impl ExtensionStore { } } -fn load_plugin_queries(root_path: &Path) -> LanguageQueries { +fn load_plugin_queries(language_name: &LanguageName, root_path: &Path) -> LanguageQueries { let mut result = LanguageQueries::default(); + load_query_dir(&mut result, root_path); + load_query_dir( + &mut result, + &paths::languages_config_dir().join(language_name.as_ref()), + ); + result +} + +fn load_query_dir(result: &mut LanguageQueries, root_path: &Path) { 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(); + log::debug!("Found language dir at {path:?}"); if let Some(remainder) = path.strip_prefix(root_path).ok().and_then(|p| p.to_str()) { if !remainder.ends_with(".scm") { continue; @@ -1894,8 +1905,9 @@ fn load_plugin_queries(root_path: &Path) -> LanguageQueries { 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()), + log::debug!("Found query at {path:?}"); + match query(result) { + None => *query(result) = Some(contents.into()), Some(r) => r.to_mut().push_str(contents.as_ref()), } } @@ -1905,5 +1917,4 @@ fn load_plugin_queries(root_path: &Path) -> LanguageQueries { } } } - result } diff --git a/crates/languages/Cargo.toml b/crates/languages/Cargo.toml index 8529bdb82ace33..41e4790ece1e5a 100644 --- a/crates/languages/Cargo.toml +++ b/crates/languages/Cargo.toml @@ -52,6 +52,7 @@ log.workspace = true lsp.workspace = true node_runtime.workspace = true parking_lot.workspace = true +paths.workspace = true pet-conda.workspace = true pet-core.workspace = true pet-fs.workspace = true diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index 7e7b83b3cb5ce8..6a5c3d25e139c8 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -435,5 +435,40 @@ fn load_queries(name: &str) -> LanguageQueries { } } } + + // TODO: dedupe this logic with extension_host? Also, should there be some kind of + // reload logic to pick up changes in the external queries for builtin langs? + let root_path = paths::languages_config_dir().join(name); + let config_dir = std::fs::read_dir(&root_path) + .context(format!("reading directory: {root_path:?}")) + .log_with_level(log::Level::Debug); + + if let Some(entries) = config_dir { + log::debug!("adding queries from {root_path:?}"); + 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; + } + log::debug!("Found query at {path:?}"); + 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), + } + } + break; + } + } + } + } + } + result } diff --git a/crates/paths/src/paths.rs b/crates/paths/src/paths.rs index a6aa8354b4661f..67a0d3e3ba1cb5 100644 --- a/crates/paths/src/paths.rs +++ b/crates/paths/src/paths.rs @@ -291,6 +291,12 @@ pub fn snippets_dir() -> &'static PathBuf { SNIPPETS_DIR.get_or_init(|| config_dir().join("snippets")) } +/// Returns the path to the user languages directory. +pub fn languages_config_dir() -> &'static PathBuf { + static LANGUAGES_CONFIG_DIR: OnceLock = OnceLock::new(); + LANGUAGES_CONFIG_DIR.get_or_init(|| config_dir().join("languages")) +} + /// Returns the path to the contexts directory. /// /// This is where the saved contexts from the Assistant are stored. diff --git a/docs/src/extensions/languages.md b/docs/src/extensions/languages.md index 8bbf3d881084d6..c951d714cb3111 100644 --- a/docs/src/extensions/languages.md +++ b/docs/src/extensions/languages.md @@ -75,6 +75,11 @@ several features: The following sections elaborate on how [Tree-sitter queries](https://tree-sitter.github.io/tree-sitter/using-parsers/queries/index.html) enable these features in Zed, using [JSON syntax](https://www.json.org/json-en.html) as a guiding example. +Note that user-defined queries may also be defined in `~/.config/zed/languages` (macOS and Linux) or `%USERPROFILE%\AppData\Roaming\Zed\languages\` (Windows). +If found, the queries will be appended to builtin queries or queries from extensions. + +For example, to add a new language injection to Rust, create a query file at `~/.config/zed/languages/rust/injections.scm`. Note that Zed may need to be restarted for these changes to take effect. + ### Syntax highlighting In Tree-sitter, the `highlights.scm` file defines syntax highlighting rules for a particular syntax. diff --git a/docs/src/themes.md b/docs/src/themes.md index 615cd2c7b38a73..04f04af382b998 100644 --- a/docs/src/themes.md +++ b/docs/src/themes.md @@ -65,7 +65,7 @@ For example, add the following to your `settings.json` if you wish to override t } ``` -To see a comprehensive list of list of captures (like `comment` and `comment.doc`) see [Language Extensions: Syntax highlighting](./extensions/languages.md#syntax-highlighting). +To see a comprehensive list of list of captures (like `comment` and `comment.doc`) see [Language Extensions: Syntax highlighting](./extensions/languages.md#syntax-highlighting). To add new captures, you can use a [custom Tree-sitter query](./extensions/languages.md#tree-sitter-queries). To see a list of available theme attributes look at the JSON file for your theme. For example, [assets/themes/one/one.json](https://github.com/zed-industries/zed/blob/main/assets/themes/one/one.json) for the default One Dark and One Light themes.