Always provide default task context (#10764)

Kirill Bulatov created

Based on
https://github.com/zed-industries/zed/issues/8324?notification_referrer_id=NT_kwDOACkO1bI5NTk0NjM0NzkyOjI2OTA3NzM&notifications_query=repo%3Azed-industries%2Fzed+is%3Aunread#issuecomment-2065551553

Release Notes:

- Fixed certain files' task modal not showing context-based tasks

Change summary

crates/language/src/task_context.rs |   6 
crates/languages/src/lib.rs         |  16 ----
crates/tasks_ui/src/lib.rs          |   4 
crates/tasks_ui/src/modal.rs        | 100 ++++++++++++++++++++++++++++++
4 files changed, 106 insertions(+), 20 deletions(-)

Detailed changes

crates/language/src/task_context.rs πŸ”—

@@ -15,9 +15,9 @@ pub trait ContextProvider: Send + Sync {
     /// Builds a specific context to be placed on top of the basic one (replacing all conflicting entries) and to be used for task resolving later.
     fn build_context(
         &self,
-        _: Option<&Path>,
-        _: &Location,
-        _: &mut AppContext,
+        _worktree_abs_path: Option<&Path>,
+        _location: &Location,
+        _cx: &mut AppContext,
     ) -> Result<TaskVariables> {
         Ok(TaskVariables::default())
     }

crates/languages/src/lib.rs πŸ”—

@@ -85,13 +85,7 @@ pub fn init(
                 config.name.clone(),
                 config.grammar.clone(),
                 config.matcher.clone(),
-                move || {
-                    Ok((
-                        config.clone(),
-                        load_queries($name),
-                        Some(Arc::new(language::BasicContextProvider)),
-                    ))
-                },
+                move || Ok((config.clone(), load_queries($name), None)),
             );
         };
         ($name:literal, $adapters:expr) => {
@@ -105,13 +99,7 @@ pub fn init(
                 config.name.clone(),
                 config.grammar.clone(),
                 config.matcher.clone(),
-                move || {
-                    Ok((
-                        config.clone(),
-                        load_queries($name),
-                        Some(Arc::new(language::BasicContextProvider)),
-                    ))
-                },
+                move || Ok((config.clone(), load_queries($name), None)),
             );
         };
         ($name:literal, $adapters:expr, $context_provider:expr) => {

crates/tasks_ui/src/lib.rs πŸ”—

@@ -168,8 +168,8 @@ fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> TaskContex
         let language_context_provider = buffer
             .read(cx)
             .language()
-            .and_then(|language| language.context_provider())?;
-
+            .and_then(|language| language.context_provider())
+            .unwrap_or_else(|| Arc::new(BasicContextProvider));
         let selection_range = selection.range();
         let start = editor_snapshot
             .display_snapshot

crates/tasks_ui/src/modal.rs πŸ”—

@@ -405,7 +405,11 @@ impl PickerDelegate for TasksModalDelegate {
 
 #[cfg(test)]
 mod tests {
+    use std::path::PathBuf;
+
+    use editor::Editor;
     use gpui::{TestAppContext, VisualTestContext};
+    use language::Point;
     use project::{FakeFs, Project};
     use serde_json::json;
 
@@ -561,6 +565,100 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    async fn test_basic_context_for_simple_files(cx: &mut TestAppContext) {
+        crate::tests::init_test(cx);
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree(
+            "/dir",
+            json!({
+                ".zed": {
+                    "tasks.json": r#"[
+                        {
+                            "label": "hello from $ZED_FILE:$ZED_ROW:$ZED_COLUMN",
+                            "command": "echo",
+                            "args": ["hello", "from", "$ZED_FILE", ":", "$ZED_ROW", ":", "$ZED_COLUMN"]
+                        },
+                        {
+                            "label": "opened now: $ZED_WORKTREE_ROOT",
+                            "command": "echo",
+                            "args": ["opened", "now:", "$ZED_WORKTREE_ROOT"]
+                        }
+                    ]"#,
+                },
+                "file_without_extension": "aaaaaaaaaaaaaaaaaaaa\naaaaaaaaaaaaaaaaaa",
+                "file_with.odd_extension": "b",
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
+
+        let tasks_picker = open_spawn_tasks(&workspace, cx);
+        assert_eq!(
+            task_names(&tasks_picker, cx),
+            Vec::<String>::new(),
+            "Should list no file or worktree context-dependent when no file is open"
+        );
+        tasks_picker.update(cx, |_, cx| {
+            cx.emit(DismissEvent);
+        });
+        drop(tasks_picker);
+        cx.executor().run_until_parked();
+
+        let _ = workspace
+            .update(cx, |workspace, cx| {
+                workspace.open_abs_path(PathBuf::from("/dir/file_with.odd_extension"), true, cx)
+            })
+            .await
+            .unwrap();
+        cx.executor().run_until_parked();
+        let tasks_picker = open_spawn_tasks(&workspace, cx);
+        assert_eq!(
+            task_names(&tasks_picker, cx),
+            vec![
+                "hello from …th.odd_extension:1:1".to_string(),
+                "opened now: /dir".to_string()
+            ],
+            "Second opened buffer should fill the context, labels should be trimmed if long enough"
+        );
+        tasks_picker.update(cx, |_, cx| {
+            cx.emit(DismissEvent);
+        });
+        drop(tasks_picker);
+        cx.executor().run_until_parked();
+
+        let second_item = workspace
+            .update(cx, |workspace, cx| {
+                workspace.open_abs_path(PathBuf::from("/dir/file_without_extension"), true, cx)
+            })
+            .await
+            .unwrap();
+
+        let editor = cx.update(|cx| second_item.act_as::<Editor>(cx)).unwrap();
+        editor.update(cx, |editor, cx| {
+            editor.change_selections(None, cx, |s| {
+                s.select_ranges(Some(Point::new(1, 2)..Point::new(1, 5)))
+            })
+        });
+        cx.executor().run_until_parked();
+        let tasks_picker = open_spawn_tasks(&workspace, cx);
+        assert_eq!(
+            task_names(&tasks_picker, cx),
+            vec![
+                "hello from …ithout_extension:2:3".to_string(),
+                "opened now: /dir".to_string()
+            ],
+            "Opened buffer should fill the context, labels should be trimmed if long enough"
+        );
+        tasks_picker.update(cx, |_, cx| {
+            cx.emit(DismissEvent);
+        });
+        drop(tasks_picker);
+        cx.executor().run_until_parked();
+    }
+
     fn open_spawn_tasks(
         workspace: &View<Workspace>,
         cx: &mut VisualTestContext,
@@ -569,7 +667,7 @@ mod tests {
         workspace.update(cx, |workspace, cx| {
             workspace
                 .active_modal::<TasksModal>(cx)
-                .unwrap()
+                .expect("no task modal after `Spawn` action was dispatched")
                 .read(cx)
                 .picker
                 .clone()