From 3ca51bdd9eacf5430d9710f9f41791c26b0442b6 Mon Sep 17 00:00:00 2001 From: Alvaro Parker Date: Fri, 10 Jul 2026 11:33:33 -0400 Subject: [PATCH 1/2] added LanguageConfig::runnables to language config --- crates/editor/src/runnables.rs | 19 +++++++--------- crates/grammars/src/grammars.rs | 1 + crates/grammars/src/markdown/config.toml | 1 + crates/language/src/language.rs | 1 + crates/language/src/runnable.rs | 25 +++++++++++++++------ crates/language_core/src/language_config.rs | 7 ++++++ 6 files changed, 36 insertions(+), 18 deletions(-) diff --git a/crates/editor/src/runnables.rs b/crates/editor/src/runnables.rs index 956e4dd2e09484..f3797bfb394c17 100644 --- a/crates/editor/src/runnables.rs +++ b/crates/editor/src/runnables.rs @@ -436,21 +436,18 @@ impl Editor { buffers .into_iter() .filter_map(|buffer| { - let lsp_tasks_source = buffer - .read(cx) - .language()? - .context_provider()? - .lsp_task_source()?; + let buffer_read = buffer.read(cx); + let language = buffer_read.language()?; + if !language.config().runnables { + return None; + } + let lsp_tasks_source = language.context_provider()?.lsp_task_source()?; if lsp_settings .get(&lsp_tasks_source) .is_none_or(|s| s.enable_lsp_tasks) { - let buffer_id = buffer.read(cx).remote_id(); - if skip_cached - && self - .runnables - .has_cached(buffer_id, &buffer.read(cx).version()) - { + let buffer_id = buffer_read.remote_id(); + if skip_cached && self.runnables.has_cached(buffer_id, &buffer_read.version()) { None } else { Some((lsp_tasks_source, buffer_id)) diff --git a/crates/grammars/src/grammars.rs b/crates/grammars/src/grammars.rs index 00d6e6281c45b1..6b593cada90f57 100644 --- a/crates/grammars/src/grammars.rs +++ b/crates/grammars/src/grammars.rs @@ -70,6 +70,7 @@ pub fn load_config_for_feature(name: &str, grammars_loaded: bool) -> LanguageCon name: config.name, matcher: config.matcher, jsx_tag_auto_close: config.jsx_tag_auto_close, + runnables: config.runnables, ..Default::default() } } diff --git a/crates/grammars/src/markdown/config.toml b/crates/grammars/src/markdown/config.toml index b366032579c6e1..7e3be0b3e36264 100644 --- a/crates/grammars/src/markdown/config.toml +++ b/crates/grammars/src/markdown/config.toml @@ -1,5 +1,6 @@ name = "Markdown" grammar = "markdown" +runnables = false path_suffixes = ["md", "mdx", "mdwn", "mdc", "markdown", "MD"] modeline_aliases = ["md"] completion_query_characters = ["-"] diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 6a799b62d32206..76f12a192e9107 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -1610,6 +1610,7 @@ pub fn markdown_lang() -> Arc { let language = Language::new( LanguageConfig { name: "Markdown".into(), + runnables: false, matcher: LanguageMatcher { path_suffixes: vec!["md".into()], ..Default::default() diff --git a/crates/language/src/runnable.rs b/crates/language/src/runnable.rs index a49bf42aae74ec..0ffcb992b3399d 100644 --- a/crates/language/src/runnable.rs +++ b/crates/language/src/runnable.rs @@ -64,17 +64,28 @@ pub(crate) fn runnable_ranges( buffer: &BufferSnapshot, offset_range: Range, ) -> impl Iterator + '_ { - let mut syntax_matches = buffer.matches(offset_range.clone(), |grammar| { - grammar.runnable_config.as_ref().map(|config| &config.query) - }); + let mut syntax_matches = buffer + .language() + .is_none_or(|language| language.config().runnables) + .then(|| { + buffer.matches(offset_range.clone(), |grammar| { + grammar.runnable_config.as_ref().map(|config| &config.query) + }) + }); let runnable_configs = syntax_matches - .grammars() - .iter() - .map(|grammar| grammar.runnable_config.as_ref()) - .collect::>(); + .as_ref() + .map(|syntax_matches| { + syntax_matches + .grammars() + .iter() + .map(|grammar| grammar.runnable_config.as_ref()) + .collect::>() + }) + .unwrap_or_default(); iter::from_fn(move || -> Option> { + let syntax_matches = syntax_matches.as_mut()?; let mat = syntax_matches.peek()?; let ranges = match runnable_configs[mat.grammar_index] { diff --git a/crates/language_core/src/language_config.rs b/crates/language_core/src/language_config.rs index 93f9423ada0642..76e8d9ce0894fd 100644 --- a/crates/language_core/src/language_config.rs +++ b/crates/language_core/src/language_config.rs @@ -135,6 +135,12 @@ pub struct LanguageConfig { /// languages, but should not appear to the user as a distinct language. #[serde(default)] pub hidden: bool, + /// Whether buffers whose root language is this language may expose runnable tasks. + /// + /// When false, runnable discovery is disabled for the whole buffer, including + /// injected syntax layers. + #[serde(default = "default_true")] + pub runnables: bool, /// If configured, this language contains JSX style tags, and should support auto-closing of those tags. #[serde(default)] pub jsx_tag_auto_close: Option, @@ -190,6 +196,7 @@ impl Default for LanguageConfig { wrap_characters: None, prettier_parser_name: None, hidden: false, + runnables: true, jsx_tag_auto_close: None, completion_query_characters: Default::default(), linked_edit_characters: Default::default(), From 14d86b106de2f2215367a5e4d7571d8842a81c16 Mon Sep 17 00:00:00 2001 From: Alvaro Parker Date: Fri, 10 Jul 2026 11:40:46 -0400 Subject: [PATCH 2/2] added tests --- Cargo.lock | 1 + crates/editor/src/runnables.rs | 44 +++++++++- crates/grammars/src/grammars.rs | 12 +++ crates/language/Cargo.toml | 1 + crates/language/src/buffer_tests.rs | 127 ++++++++++++++++++++++++++++ 5 files changed, 184 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index f65225de1bd947..12c6448d9be7d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9702,6 +9702,7 @@ dependencies = [ "tree-sitter-c", "tree-sitter-elixir", "tree-sitter-embedded-template", + "tree-sitter-go", "tree-sitter-heex", "tree-sitter-html", "tree-sitter-json", diff --git a/crates/editor/src/runnables.rs b/crates/editor/src/runnables.rs index f3797bfb394c17..0031a67fcd7d96 100644 --- a/crates/editor/src/runnables.rs +++ b/crates/editor/src/runnables.rs @@ -707,7 +707,7 @@ mod tests { use futures::StreamExt as _; use gpui::{AppContext as _, Entity, Task, TestAppContext}; use indoc::indoc; - use language::{ContextProvider, FakeLspAdapter}; + use language::{ContextProvider, FakeLspAdapter, markdown_lang}; use languages::rust_lang; use lsp::LanguageServerName; use multi_buffer::{MultiBuffer, PathKey}; @@ -795,6 +795,14 @@ mod tests { ) } + fn markdown_lang_with_lsp_task_context() -> Arc { + Arc::new( + Arc::try_unwrap(markdown_lang()) + .expect("test Markdown language should have a single owner") + .with_context_provider(Some(Arc::new(TestRustContextProviderWithLsp))), + ) + } + fn collect_runnable_labels( editor: &Editor, ) -> Vec<(text::BufferId, language::BufferRow, Vec)> { @@ -818,6 +826,40 @@ mod tests { result } + #[gpui::test] + async fn test_disabled_language_omits_lsp_task_sources(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/project"), + json!({ + "README.md": "# Project\n", + }), + ) + .await; + + let project = Project::test(fs, [path!("/project").as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(markdown_lang_with_lsp_task_context()); + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer(path!("/project/README.md"), cx) + }) + .await + .expect("open README.md"); + let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + let editor = cx + .add_window(|window, cx| build_editor_with_project(project, multi_buffer, window, cx)); + + let task_sources = editor + .update(cx, |editor, _, cx| { + editor.lsp_task_sources(false, false, cx) + }) + .expect("editor update"); + assert!(task_sources.is_empty()); + } + #[gpui::test] async fn test_multi_buffer_runnables_on_scroll(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/grammars/src/grammars.rs b/crates/grammars/src/grammars.rs index 6b593cada90f57..d19548da727893 100644 --- a/crates/grammars/src/grammars.rs +++ b/crates/grammars/src/grammars.rs @@ -107,3 +107,15 @@ pub fn load_queries(name: &str) -> LanguageQueries { } result } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_runnables_config() { + assert!(!load_config("markdown").runnables); + assert!(load_config("rust").runnables); + assert!(!load_config_for_feature("markdown", false).runnables); + } +} diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 58a7517d2cb925..3023653358eea1 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -96,6 +96,7 @@ tree-sitter-md.workspace = true tree-sitter-python.workspace = true tree-sitter-ruby.workspace = true tree-sitter-c.workspace = true +tree-sitter-go.workspace = true tree-sitter-rust.workspace = true tree-sitter-typescript.workspace = true toml.workspace = true diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 29afbe84b72c59..8b25dbea832523 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -2981,6 +2981,101 @@ fn test_language_at_for_markdown_code_block(cx: &mut App) { }); } +#[gpui::test] +fn test_root_language_controls_injected_runnables(cx: &mut App) { + init_settings(cx, |_| {}); + + cx.new(|cx| { + let go_source = "func TestAddition(t *testing.T) {}"; + let go_language = test_go_lang(); + let go_buffer = Buffer::local(go_source, cx).with_language(go_language.clone(), cx); + assert_eq!( + go_buffer + .snapshot() + .runnable_ranges(0..go_source.len()) + .count(), + 1 + ); + + let markdown_source = indoc! {r#" + ```go + func TestAddition(t *testing.T) {} + ``` + + ```rust + #[test] + fn test_addition() {} + ``` + "#}; + let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); + language_registry.add(markdown_lang()); + language_registry.add(go_language.clone()); + language_registry.add(rust_lang()); + + let mut markdown_buffer = Buffer::local(markdown_source, cx); + markdown_buffer.set_language_registry(language_registry.clone()); + markdown_buffer.set_language( + language_registry + .language_for_name("Markdown") + .now_or_never() + .and_then(Result::ok), + cx, + ); + + let markdown_snapshot = markdown_buffer.snapshot(); + let test_offset = markdown_source + .find("TestAddition") + .expect("test name should be present"); + assert_eq!( + markdown_snapshot + .language_at(test_offset) + .expect("Go injection should be parsed") + .name(), + "Go" + ); + let rust_test_offset = markdown_source + .find("test_addition") + .expect("Rust test name should be present"); + assert_eq!( + markdown_snapshot + .language_at(rust_test_offset) + .expect("Rust injection should be parsed") + .name(), + "Rust" + ); + assert_eq!( + markdown_snapshot + .runnable_ranges(0..markdown_source.len()) + .count(), + 0 + ); + + let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); + language_registry.add(test_markdown_lang_with_runnables()); + language_registry.add(go_language); + language_registry.add(rust_lang()); + + let mut runnable_markdown_buffer = Buffer::local(markdown_source, cx); + runnable_markdown_buffer.set_language_registry(language_registry.clone()); + runnable_markdown_buffer.set_language( + language_registry + .language_for_name("Runnable Markdown") + .now_or_never() + .and_then(Result::ok), + cx, + ); + assert_eq!( + runnable_markdown_buffer + .snapshot() + .runnable_ranges(0..markdown_source.len()) + .count(), + 2 + ); + + markdown_buffer + }); +} + #[gpui::test] fn test_syntax_layer_at_for_combined_injections(cx: &mut App) { init_settings(cx, |_| {}); @@ -4359,6 +4454,38 @@ pub fn markdown_inline_lang() -> Language { .unwrap() } +fn test_go_lang() -> Arc { + Arc::new( + Language::new( + LanguageConfig { + name: "Go".into(), + matcher: LanguageMatcher { + path_suffixes: vec!["go".to_string()], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_go::LANGUAGE.into()), + ) + .with_runnable_query(include_str!("../../grammars/src/go/runnables.scm")) + .expect("valid Go runnable query"), + ) +} + +fn test_markdown_lang_with_runnables() -> Arc { + Arc::new( + Language::new( + LanguageConfig { + name: "Runnable Markdown".into(), + ..Default::default() + }, + Some(tree_sitter_md::LANGUAGE.into()), + ) + .with_injection_query(include_str!("../../grammars/src/markdown/injections.scm")) + .expect("valid Markdown injection query"), + ) +} + fn get_tree_sexp(buffer: &Entity, cx: &mut gpui::TestAppContext) -> String { buffer.update(cx, |buffer, _| { let snapshot = buffer.snapshot();