Fix task modal fallback when LSP tasks are empty (#58090)

References
[FR-28](https://linear.app/zed-industries/issue/FR-28/task-modal-does-not-show-runnable-rust-test).

The bug this PR aims to fix is

> I can click the play button beside a rust test function, but it does
not show up in the task modal.

I wasn't able to reproduce it, but I suspect this was caused by the task
system preferring LSP code actions by default. It checked that an LSP
was queried for a task instead of checking if the queried LSP actually
returned any tasks. So the fix was just adding the below if statement as
a check.

```rust
if !new_lsp_tasks.is_empty() {
    lsp_tasks
        .entry(source_kind)
        .or_insert_with(Vec::new)
        .append(&mut new_lsp_tasks);
}
```

I also added a regression test for this

Self-Review Checklist:

- [x] I have reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the UI/UX checklist
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable


Release Notes:

- Fixed task modal failing to show language tasks in some cases
This commit is contained in:
Anthony Eid 2026-05-29 13:49:11 -04:00 committed by GitHub
parent 122619624d
commit 7f4a99aa95
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 112 additions and 7 deletions

View file

@ -164,10 +164,12 @@ pub fn lsp_tasks(
}, },
)); ));
} }
lsp_tasks if !new_lsp_tasks.is_empty() {
.entry(source_kind) lsp_tasks
.or_insert_with(Vec::new) .entry(source_kind)
.append(&mut new_lsp_tasks); .or_insert_with(Vec::new)
.append(&mut new_lsp_tasks);
}
} }
} }
lsp_tasks.into_iter().collect() lsp_tasks.into_iter().collect()

View file

@ -734,11 +734,14 @@ mod tests {
use std::{path::PathBuf, sync::Arc}; use std::{path::PathBuf, sync::Arc};
use editor::{Editor, SelectionEffects}; use editor::{Editor, SelectionEffects};
use gpui::{TestAppContext, VisualTestContext}; use gpui::{App, Entity, Task, TestAppContext, VisualTestContext};
use language::{Language, LanguageConfig, LanguageMatcher, Point}; use language::{
Buffer, ContextProvider, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher,
LanguageServerName, Point,
};
use project::{ContextProviderWithTasks, FakeFs, Project}; use project::{ContextProviderWithTasks, FakeFs, Project};
use serde_json::json; use serde_json::json;
use task::TaskTemplates; use task::{TaskTemplate, TaskTemplates};
use util::path; use util::path;
use workspace::{CloseInactiveTabsAndPanes, MultiWorkspace, OpenOptions, OpenVisible}; use workspace::{CloseInactiveTabsAndPanes, MultiWorkspace, OpenOptions, OpenVisible};
@ -1033,6 +1036,80 @@ mod tests {
cx.executor().run_until_parked(); cx.executor().run_until_parked();
} }
#[gpui::test]
async fn test_empty_lsp_task_response_keeps_language_tasks_in_modal(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/dir"), json!({ "main.test": "test" }))
.await;
let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(Arc::new(
Language::new(
LanguageConfig {
name: "Test".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["test".to_string()],
..LanguageMatcher::default()
},
..LanguageConfig::default()
},
None,
)
.with_context_provider(Some(Arc::new(
ContextProviderWithLspTaskSource::new(ContextProviderWithTasks::new(
TaskTemplates(vec![TaskTemplate {
label: "Run language task".to_string(),
command: "echo".to_string(),
args: vec!["language task".to_string()],
..TaskTemplate::default()
}]),
)),
))),
));
let mut fake_servers = language_registry.register_fake_lsp(
"Test",
FakeLspAdapter {
name: TEST_LSP_NAME,
..FakeLspAdapter::default()
},
);
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace =
multi_workspace.read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone());
let _item = workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_abs_path(
PathBuf::from(path!("/dir/main.test")),
OpenOptions {
visible: Some(OpenVisible::All),
..Default::default()
},
window,
cx,
)
})
.await
.unwrap();
cx.executor().run_until_parked();
let fake_server = fake_servers
.try_recv()
.expect("fake LSP server should have started");
use project::lsp_store::lsp_ext_command::Runnables;
fake_server
.set_request_handler::<Runnables, _, _>(move |_, _| async move { Ok(Vec::new()) });
let tasks_picker = open_spawn_tasks(&workspace, cx);
assert_eq!(
task_names(&tasks_picker, cx),
vec!["Run language task"],
"An empty LSP task response should not suppress language tasks in the modal"
);
}
#[gpui::test] #[gpui::test]
async fn test_language_task_filtering(cx: &mut TestAppContext) { async fn test_language_task_filtering(cx: &mut TestAppContext) {
init_test(cx); init_test(cx);
@ -1238,6 +1315,32 @@ mod tests {
); );
} }
const TEST_LSP_NAME: &str = "test-lsp";
struct ContextProviderWithLspTaskSource {
tasks: ContextProviderWithTasks,
}
impl ContextProviderWithLspTaskSource {
fn new(tasks: ContextProviderWithTasks) -> Self {
Self { tasks }
}
}
impl ContextProvider for ContextProviderWithLspTaskSource {
fn associated_tasks(
&self,
buffer: Option<Entity<Buffer>>,
cx: &App,
) -> Task<Option<TaskTemplates>> {
self.tasks.associated_tasks(buffer, cx)
}
fn lsp_task_source(&self) -> Option<LanguageServerName> {
Some(LanguageServerName::new_static(TEST_LSP_NAME))
}
}
fn emulate_task_schedule( fn emulate_task_schedule(
tasks_picker: Entity<Picker<TasksModalDelegate>>, tasks_picker: Entity<Picker<TasksModalDelegate>>,
project: &Entity<Project>, project: &Entity<Project>,