task: Make UseSelectedQuery modal action expand to full command (#9947)

Piotr Osiewicz created

Previously it expanded to a label, which was correct for oneshots, but
wrong for everything else.

Release Notes:

- Improved UseSelectedQuery (shift-enter) action for tasks modal by
making it substitute a full command and not the task label.
- Fixed one-shot tasks having duplicates in tasks modal.

Change summary

Cargo.lock                        |  1 
crates/task/src/oneshot_source.rs | 11 +++++++--
crates/tasks_ui/Cargo.toml        |  1 
crates/tasks_ui/src/modal.rs      | 35 ++++++++++++++++++++++----------
4 files changed, 34 insertions(+), 14 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -9640,6 +9640,7 @@ dependencies = [
  "editor",
  "fuzzy",
  "gpui",
+ "itertools 0.11.0",
  "language",
  "menu",
  "picker",

crates/task/src/oneshot_source.rs 🔗

@@ -66,9 +66,14 @@ impl OneshotSource {
 
     /// Spawns a certain task based on the user prompt.
     pub fn spawn(&mut self, prompt: String) -> Arc<dyn Task> {
-        let ret = Arc::new(OneshotTask::new(prompt));
-        self.tasks.push(ret.clone());
-        ret
+        if let Some(task) = self.tasks.iter().find(|task| task.id().0 == prompt) {
+            // If we already have an oneshot task with that command, let's just reuse it.
+            task.clone()
+        } else {
+            let new_oneshot = Arc::new(OneshotTask::new(prompt));
+            self.tasks.push(new_oneshot.clone());
+            new_oneshot
+        }
     }
 }
 

crates/tasks_ui/Cargo.toml 🔗

@@ -22,6 +22,7 @@ ui.workspace = true
 util.workspace = true
 workspace.workspace = true
 language.workspace = true
+itertools.workspace = true
 
 [dev-dependencies]
 editor = { workspace = true, features = ["test-support"] }

crates/tasks_ui/src/modal.rs 🔗

@@ -310,7 +310,20 @@ impl PickerDelegate for TasksModalDelegate {
     }
 
     fn selected_as_query(&self) -> Option<String> {
-        Some(self.matches.get(self.selected_index())?.string.clone())
+        use itertools::intersperse;
+        let task_index = self.matches.get(self.selected_index())?.candidate_id;
+        let tasks = self.candidates.as_ref()?;
+        let (_, task) = tasks.get(task_index)?;
+        // .exec doesn't actually spawn anything; it merely prepares a spawning command,
+        // which we can use for substitution.
+        let mut spawn_prompt = task.exec(self.task_context.clone())?;
+        if !spawn_prompt.args.is_empty() {
+            spawn_prompt.command.push(' ');
+            spawn_prompt
+                .command
+                .extend(intersperse(spawn_prompt.args, " ".to_string()));
+        }
+        Some(spawn_prompt.command)
     }
 }
 
@@ -381,15 +394,15 @@ mod tests {
         cx.dispatch_action(menu::UseSelectedQuery);
         assert_eq!(
             query(&tasks_picker, cx),
-            "example task",
-            "Query should be set to the selected task's name"
+            "echo 4",
+            "Query should be set to the selected task's command"
         );
         assert_eq!(
             task_names(&tasks_picker, cx),
-            vec!["example task"],
-            "No other tasks should be listed"
+            Vec::<String>::new(),
+            "No task should be listed"
         );
-        cx.dispatch_action(menu::Confirm);
+        cx.dispatch_action(menu::SecondaryConfirm);
 
         let tasks_picker = open_spawn_tasks(&workspace, cx);
         assert_eq!(
@@ -399,8 +412,8 @@ mod tests {
         );
         assert_eq!(
             task_names(&tasks_picker, cx),
-            vec!["example task", "another one"],
-            "Last recently used task should be listed first"
+            vec!["echo 4", "another one", "example task"],
+            "New oneshot task should be listed first"
         );
 
         let query_str = "echo 4";
@@ -408,8 +421,8 @@ mod tests {
         assert_eq!(query(&tasks_picker, cx), query_str);
         assert_eq!(
             task_names(&tasks_picker, cx),
-            Vec::<String>::new(),
-            "No tasks should match custom command query"
+            vec!["echo 4"],
+            "New oneshot should match custom command query"
         );
 
         cx.dispatch_action(menu::SecondaryConfirm);
@@ -421,7 +434,7 @@ mod tests {
         );
         assert_eq!(
             task_names(&tasks_picker, cx),
-            vec![query_str, "example task", "another one"],
+            vec![query_str, "another one", "example task"],
             "Last recently used one show task should be listed first"
         );