Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

63 changes: 51 additions & 12 deletions crates/editor/src/runnables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -710,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};
Expand Down Expand Up @@ -798,6 +795,14 @@ mod tests {
)
}

fn markdown_lang_with_lsp_task_context() -> Arc<language::Language> {
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<String>)> {
Expand All @@ -821,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, |_| {});
Expand Down
13 changes: 13 additions & 0 deletions crates/grammars/src/grammars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Expand Down Expand Up @@ -106,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);
}
}
1 change: 1 addition & 0 deletions crates/grammars/src/markdown/config.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
name = "Markdown"
grammar = "markdown"
runnables = false
path_suffixes = ["md", "mdx", "mdwn", "mdc", "markdown", "MD"]
modeline_aliases = ["md"]
completion_query_characters = ["-"]
Expand Down
1 change: 1 addition & 0 deletions crates/language/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
127 changes: 127 additions & 0 deletions crates/language/src/buffer_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, |_| {});
Expand Down Expand Up @@ -4359,6 +4454,38 @@ pub fn markdown_inline_lang() -> Language {
.unwrap()
}

fn test_go_lang() -> Arc<Language> {
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<Language> {
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<Buffer>, cx: &mut gpui::TestAppContext) -> String {
buffer.update(cx, |buffer, _| {
let snapshot = buffer.snapshot();
Expand Down
1 change: 1 addition & 0 deletions crates/language/src/language.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1610,6 +1610,7 @@ pub fn markdown_lang() -> Arc<Language> {
let language = Language::new(
LanguageConfig {
name: "Markdown".into(),
runnables: false,
matcher: LanguageMatcher {
path_suffixes: vec!["md".into()],
..Default::default()
Expand Down
25 changes: 18 additions & 7 deletions crates/language/src/runnable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,28 @@ pub(crate) fn runnable_ranges(
buffer: &BufferSnapshot,
offset_range: Range<usize>,
) -> impl Iterator<Item = RunnableRange> + '_ {
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::<Vec<_>>();
.as_ref()
.map(|syntax_matches| {
syntax_matches
.grammars()
.iter()
.map(|grammar| grammar.runnable_config.as_ref())
.collect::<Vec<_>>()
})
.unwrap_or_default();

iter::from_fn(move || -> Option<SmallVec<[RunnableRange; 1]>> {
let syntax_matches = syntax_matches.as_mut()?;
let mat = syntax_matches.peek()?;

let ranges = match runnable_configs[mat.grammar_index] {
Expand Down
7 changes: 7 additions & 0 deletions crates/language_core/src/language_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<JsxTagAutoCloseConfig>,
Expand Down Expand Up @@ -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(),
Expand Down
Loading