tasks_ui.rs

  1use ::settings::Settings;
  2use editor::{tasks::task_context, Editor};
  3use gpui::{App, Context, Task as AsyncTask, Window};
  4use modal::{TaskOverrides, TasksModal};
  5use project::{Location, WorktreeId};
  6use task::{RevealTarget, TaskId};
  7use workspace::tasks::schedule_task;
  8use workspace::{tasks::schedule_resolved_task, Workspace};
  9
 10mod modal;
 11mod settings;
 12
 13pub use modal::{Rerun, Spawn};
 14
 15pub fn init(cx: &mut App) {
 16    settings::TaskSettings::register(cx);
 17    cx.observe_new(
 18        |workspace: &mut Workspace, _window: Option<&mut Window>, _: &mut Context<Workspace>| {
 19            workspace
 20                .register_action(spawn_task_or_modal)
 21                .register_action(move |workspace, action: &modal::Rerun, window, cx| {
 22                    if let Some((task_source_kind, mut last_scheduled_task)) = workspace
 23                        .project()
 24                        .read(cx)
 25                        .task_store()
 26                        .read(cx)
 27                        .task_inventory()
 28                        .and_then(|inventory| {
 29                            inventory.read(cx).last_scheduled_task(
 30                                action
 31                                    .task_id
 32                                    .as_ref()
 33                                    .map(|id| TaskId(id.clone()))
 34                                    .as_ref(),
 35                            )
 36                        })
 37                    {
 38                        if action.reevaluate_context {
 39                            let mut original_task = last_scheduled_task.original_task().clone();
 40                            if let Some(allow_concurrent_runs) = action.allow_concurrent_runs {
 41                                original_task.allow_concurrent_runs = allow_concurrent_runs;
 42                            }
 43                            if let Some(use_new_terminal) = action.use_new_terminal {
 44                                original_task.use_new_terminal = use_new_terminal;
 45                            }
 46                            let context_task = task_context(workspace, window, cx);
 47                            cx.spawn_in(window, |workspace, mut cx| async move {
 48                                let task_context = context_task.await;
 49                                workspace
 50                                    .update(&mut cx, |workspace, cx| {
 51                                        schedule_task(
 52                                            workspace,
 53                                            task_source_kind,
 54                                            &original_task,
 55                                            &task_context,
 56                                            false,
 57                                            cx,
 58                                        )
 59                                    })
 60                                    .ok()
 61                            })
 62                            .detach()
 63                        } else {
 64                            if let Some(resolved) = last_scheduled_task.resolved.as_mut() {
 65                                if let Some(allow_concurrent_runs) = action.allow_concurrent_runs {
 66                                    resolved.allow_concurrent_runs = allow_concurrent_runs;
 67                                }
 68                                if let Some(use_new_terminal) = action.use_new_terminal {
 69                                    resolved.use_new_terminal = use_new_terminal;
 70                                }
 71                            }
 72
 73                            schedule_resolved_task(
 74                                workspace,
 75                                task_source_kind,
 76                                last_scheduled_task,
 77                                false,
 78                                cx,
 79                            );
 80                        }
 81                    } else {
 82                        toggle_modal(workspace, None, window, cx).detach();
 83                    };
 84                });
 85        },
 86    )
 87    .detach();
 88}
 89
 90fn spawn_task_or_modal(
 91    workspace: &mut Workspace,
 92    action: &Spawn,
 93    window: &mut Window,
 94    cx: &mut Context<Workspace>,
 95) {
 96    match action {
 97        Spawn::ByName {
 98            task_name,
 99            reveal_target,
100        } => {
101            let overrides = reveal_target.map(|reveal_target| TaskOverrides {
102                reveal_target: Some(reveal_target),
103            });
104            spawn_task_with_name(task_name.clone(), overrides, window, cx).detach_and_log_err(cx)
105        }
106        Spawn::ViaModal { reveal_target } => {
107            toggle_modal(workspace, *reveal_target, window, cx).detach()
108        }
109    }
110}
111
112fn toggle_modal(
113    workspace: &mut Workspace,
114    reveal_target: Option<RevealTarget>,
115    window: &mut Window,
116    cx: &mut Context<Workspace>,
117) -> AsyncTask<()> {
118    let task_store = workspace.project().read(cx).task_store().clone();
119    let workspace_handle = workspace.weak_handle();
120    let can_open_modal = workspace.project().update(cx, |project, cx| {
121        project.is_local() || project.ssh_connection_string(cx).is_some() || project.is_via_ssh()
122    });
123    if can_open_modal {
124        let context_task = task_context(workspace, window, cx);
125        cx.spawn_in(window, |workspace, mut cx| async move {
126            let task_context = context_task.await;
127            workspace
128                .update_in(&mut cx, |workspace, window, cx| {
129                    workspace.toggle_modal(window, cx, |window, cx| {
130                        TasksModal::new(
131                            task_store.clone(),
132                            task_context,
133                            reveal_target.map(|target| TaskOverrides {
134                                reveal_target: Some(target),
135                            }),
136                            workspace_handle,
137                            window,
138                            cx,
139                        )
140                    })
141                })
142                .ok();
143        })
144    } else {
145        AsyncTask::ready(())
146    }
147}
148
149fn spawn_task_with_name(
150    name: String,
151    overrides: Option<TaskOverrides>,
152    window: &mut Window,
153    cx: &mut Context<Workspace>,
154) -> AsyncTask<anyhow::Result<()>> {
155    cx.spawn_in(window, |workspace, mut cx| async move {
156        let context_task = workspace.update_in(&mut cx, |workspace, window, cx| {
157            task_context(workspace, window, cx)
158        })?;
159        let task_context = context_task.await;
160        let tasks = workspace.update(&mut cx, |workspace, cx| {
161            let Some(task_inventory) = workspace
162                .project()
163                .read(cx)
164                .task_store()
165                .read(cx)
166                .task_inventory()
167                .cloned()
168            else {
169                return Vec::new();
170            };
171            let (worktree, location) = active_item_selection_properties(workspace, cx);
172            let (file, language) = location
173                .map(|location| {
174                    let buffer = location.buffer.read(cx);
175                    (
176                        buffer.file().cloned(),
177                        buffer.language_at(location.range.start),
178                    )
179                })
180                .unwrap_or_default();
181            task_inventory
182                .read(cx)
183                .list_tasks(file, language, worktree, cx)
184        })?;
185
186        let did_spawn = workspace
187            .update(&mut cx, |workspace, cx| {
188                let (task_source_kind, mut target_task) =
189                    tasks.into_iter().find(|(_, task)| task.label == name)?;
190                if let Some(overrides) = &overrides {
191                    if let Some(target_override) = overrides.reveal_target {
192                        target_task.reveal_target = target_override;
193                    }
194                }
195                schedule_task(
196                    workspace,
197                    task_source_kind,
198                    &target_task,
199                    &task_context,
200                    false,
201                    cx,
202                );
203                Some(())
204            })?
205            .is_some();
206        if !did_spawn {
207            workspace
208                .update_in(&mut cx, |workspace, window, cx| {
209                    spawn_task_or_modal(
210                        workspace,
211                        &Spawn::ViaModal {
212                            reveal_target: overrides.and_then(|overrides| overrides.reveal_target),
213                        },
214                        window,
215                        cx,
216                    );
217                })
218                .ok();
219        }
220
221        Ok(())
222    })
223}
224
225fn active_item_selection_properties(
226    workspace: &Workspace,
227    cx: &mut App,
228) -> (Option<WorktreeId>, Option<Location>) {
229    let active_item = workspace.active_item(cx);
230    let worktree_id = active_item
231        .as_ref()
232        .and_then(|item| item.project_path(cx))
233        .map(|path| path.worktree_id);
234    let location = active_item
235        .and_then(|active_item| active_item.act_as::<Editor>(cx))
236        .and_then(|editor| {
237            editor.update(cx, |editor, cx| {
238                let selection = editor.selections.newest_anchor();
239                let multi_buffer = editor.buffer().clone();
240                let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
241                let (buffer_snapshot, buffer_offset) =
242                    multi_buffer_snapshot.point_to_buffer_offset(selection.head())?;
243                let buffer_anchor = buffer_snapshot.anchor_before(buffer_offset);
244                let buffer = multi_buffer.read(cx).buffer(buffer_snapshot.remote_id())?;
245                Some(Location {
246                    buffer,
247                    range: buffer_anchor..buffer_anchor,
248                })
249            })
250        });
251    (worktree_id, location)
252}
253
254#[cfg(test)]
255mod tests {
256    use std::{collections::HashMap, sync::Arc};
257
258    use editor::Editor;
259    use gpui::TestAppContext;
260    use language::{Language, LanguageConfig};
261    use project::{task_store::TaskStore, BasicContextProvider, FakeFs, Project};
262    use serde_json::json;
263    use task::{TaskContext, TaskVariables, VariableName};
264    use ui::VisualContext;
265    use util::{path, separator};
266    use workspace::{AppState, Workspace};
267
268    use crate::task_context;
269
270    #[gpui::test]
271    async fn test_default_language_context(cx: &mut TestAppContext) {
272        init_test(cx);
273        let fs = FakeFs::new(cx.executor());
274        fs.insert_tree(
275            path!("/dir"),
276            json!({
277                ".zed": {
278                    "tasks.json": r#"[
279                            {
280                                "label": "example task",
281                                "command": "echo",
282                                "args": ["4"]
283                            },
284                            {
285                                "label": "another one",
286                                "command": "echo",
287                                "args": ["55"]
288                            },
289                        ]"#,
290                },
291                "a.ts": "function this_is_a_test() { }",
292                "rust": {
293                                    "b.rs": "use std; fn this_is_a_rust_file() { }",
294                }
295
296            }),
297        )
298        .await;
299        let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
300        let worktree_store = project.update(cx, |project, _| project.worktree_store().clone());
301        let rust_language = Arc::new(
302            Language::new(
303                LanguageConfig::default(),
304                Some(tree_sitter_rust::LANGUAGE.into()),
305            )
306            .with_outline_query(
307                r#"(function_item
308            "fn" @context
309            name: (_) @name) @item"#,
310            )
311            .unwrap()
312            .with_context_provider(Some(Arc::new(BasicContextProvider::new(
313                worktree_store.clone(),
314            )))),
315        );
316
317        let typescript_language = Arc::new(
318            Language::new(
319                LanguageConfig::default(),
320                Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
321            )
322            .with_outline_query(
323                r#"(function_declaration
324                    "async"? @context
325                    "function" @context
326                    name: (_) @name
327                    parameters: (formal_parameters
328                      "(" @context
329                      ")" @context)) @item"#,
330            )
331            .unwrap()
332            .with_context_provider(Some(Arc::new(BasicContextProvider::new(
333                worktree_store.clone(),
334            )))),
335        );
336
337        let worktree_id = project.update(cx, |project, cx| {
338            project.worktrees(cx).next().unwrap().read(cx).id()
339        });
340        let (workspace, cx) =
341            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
342
343        let buffer1 = workspace
344            .update(cx, |this, cx| {
345                this.project()
346                    .update(cx, |this, cx| this.open_buffer((worktree_id, "a.ts"), cx))
347            })
348            .await
349            .unwrap();
350        buffer1.update(cx, |this, cx| {
351            this.set_language(Some(typescript_language), cx)
352        });
353        let editor1 = cx.new_window_entity(|window, cx| {
354            Editor::for_buffer(buffer1, Some(project.clone()), window, cx)
355        });
356        let buffer2 = workspace
357            .update(cx, |this, cx| {
358                this.project().update(cx, |this, cx| {
359                    this.open_buffer((worktree_id, "rust/b.rs"), cx)
360                })
361            })
362            .await
363            .unwrap();
364        buffer2.update(cx, |this, cx| this.set_language(Some(rust_language), cx));
365        let editor2 = cx
366            .new_window_entity(|window, cx| Editor::for_buffer(buffer2, Some(project), window, cx));
367
368        let first_context = workspace
369            .update_in(cx, |workspace, window, cx| {
370                workspace.add_item_to_center(Box::new(editor1.clone()), window, cx);
371                workspace.add_item_to_center(Box::new(editor2.clone()), window, cx);
372                assert_eq!(
373                    workspace.active_item(cx).unwrap().item_id(),
374                    editor2.entity_id()
375                );
376                task_context(workspace, window, cx)
377            })
378            .await;
379
380        assert_eq!(
381            first_context,
382            TaskContext {
383                cwd: Some(path!("/dir").into()),
384                task_variables: TaskVariables::from_iter([
385                    (VariableName::File, path!("/dir/rust/b.rs").into()),
386                    (VariableName::Filename, "b.rs".into()),
387                    (VariableName::RelativeFile, separator!("rust/b.rs").into()),
388                    (VariableName::Dirname, path!("/dir/rust").into()),
389                    (VariableName::Stem, "b".into()),
390                    (VariableName::WorktreeRoot, path!("/dir").into()),
391                    (VariableName::Row, "1".into()),
392                    (VariableName::Column, "1".into()),
393                ]),
394                project_env: HashMap::default(),
395            }
396        );
397
398        // And now, let's select an identifier.
399        editor2.update_in(cx, |editor, window, cx| {
400            editor.change_selections(None, window, cx, |selections| {
401                selections.select_ranges([14..18])
402            })
403        });
404
405        assert_eq!(
406            workspace
407                .update_in(cx, |workspace, window, cx| {
408                    task_context(workspace, window, cx)
409                })
410                .await,
411            TaskContext {
412                cwd: Some(path!("/dir").into()),
413                task_variables: TaskVariables::from_iter([
414                    (VariableName::File, path!("/dir/rust/b.rs").into()),
415                    (VariableName::Filename, "b.rs".into()),
416                    (VariableName::RelativeFile, separator!("rust/b.rs").into()),
417                    (VariableName::Dirname, path!("/dir/rust").into()),
418                    (VariableName::Stem, "b".into()),
419                    (VariableName::WorktreeRoot, path!("/dir").into()),
420                    (VariableName::Row, "1".into()),
421                    (VariableName::Column, "15".into()),
422                    (VariableName::SelectedText, "is_i".into()),
423                    (VariableName::Symbol, "this_is_a_rust_file".into()),
424                ]),
425                project_env: HashMap::default(),
426            }
427        );
428
429        assert_eq!(
430            workspace
431                .update_in(cx, |workspace, window, cx| {
432                    // Now, let's switch the active item to .ts file.
433                    workspace.activate_item(&editor1, true, true, window, cx);
434                    task_context(workspace, window, cx)
435                })
436                .await,
437            TaskContext {
438                cwd: Some(path!("/dir").into()),
439                task_variables: TaskVariables::from_iter([
440                    (VariableName::File, path!("/dir/a.ts").into()),
441                    (VariableName::Filename, "a.ts".into()),
442                    (VariableName::RelativeFile, "a.ts".into()),
443                    (VariableName::Dirname, path!("/dir").into()),
444                    (VariableName::Stem, "a".into()),
445                    (VariableName::WorktreeRoot, path!("/dir").into()),
446                    (VariableName::Row, "1".into()),
447                    (VariableName::Column, "1".into()),
448                    (VariableName::Symbol, "this_is_a_test".into()),
449                ]),
450                project_env: HashMap::default(),
451            }
452        );
453    }
454
455    pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
456        cx.update(|cx| {
457            let state = AppState::test(cx);
458            language::init(cx);
459            crate::init(cx);
460            editor::init(cx);
461            workspace::init_settings(cx);
462            Project::init_settings(cx);
463            TaskStore::init(None);
464            state
465        })
466    }
467}