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 workspace::{AppState, Workspace};
266
267    use crate::task_context;
268
269    #[gpui::test]
270    async fn test_default_language_context(cx: &mut TestAppContext) {
271        init_test(cx);
272        let fs = FakeFs::new(cx.executor());
273        fs.insert_tree(
274            "/dir",
275            json!({
276                ".zed": {
277                    "tasks.json": r#"[
278                            {
279                                "label": "example task",
280                                "command": "echo",
281                                "args": ["4"]
282                            },
283                            {
284                                "label": "another one",
285                                "command": "echo",
286                                "args": ["55"]
287                            },
288                        ]"#,
289                },
290                "a.ts": "function this_is_a_test() { }",
291                "rust": {
292                                    "b.rs": "use std; fn this_is_a_rust_file() { }",
293                }
294
295            }),
296        )
297        .await;
298        let project = Project::test(fs, ["/dir".as_ref()], cx).await;
299        let worktree_store = project.update(cx, |project, _| project.worktree_store().clone());
300        let rust_language = Arc::new(
301            Language::new(
302                LanguageConfig::default(),
303                Some(tree_sitter_rust::LANGUAGE.into()),
304            )
305            .with_outline_query(
306                r#"(function_item
307            "fn" @context
308            name: (_) @name) @item"#,
309            )
310            .unwrap()
311            .with_context_provider(Some(Arc::new(BasicContextProvider::new(
312                worktree_store.clone(),
313            )))),
314        );
315
316        let typescript_language = Arc::new(
317            Language::new(
318                LanguageConfig::default(),
319                Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
320            )
321            .with_outline_query(
322                r#"(function_declaration
323                    "async"? @context
324                    "function" @context
325                    name: (_) @name
326                    parameters: (formal_parameters
327                      "(" @context
328                      ")" @context)) @item"#,
329            )
330            .unwrap()
331            .with_context_provider(Some(Arc::new(BasicContextProvider::new(
332                worktree_store.clone(),
333            )))),
334        );
335
336        let worktree_id = project.update(cx, |project, cx| {
337            project.worktrees(cx).next().unwrap().read(cx).id()
338        });
339        let (workspace, cx) =
340            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
341
342        let buffer1 = workspace
343            .update(cx, |this, cx| {
344                this.project()
345                    .update(cx, |this, cx| this.open_buffer((worktree_id, "a.ts"), cx))
346            })
347            .await
348            .unwrap();
349        buffer1.update(cx, |this, cx| {
350            this.set_language(Some(typescript_language), cx)
351        });
352        let editor1 = cx.new_window_entity(|window, cx| {
353            Editor::for_buffer(buffer1, Some(project.clone()), window, cx)
354        });
355        let buffer2 = workspace
356            .update(cx, |this, cx| {
357                this.project().update(cx, |this, cx| {
358                    this.open_buffer((worktree_id, "rust/b.rs"), cx)
359                })
360            })
361            .await
362            .unwrap();
363        buffer2.update(cx, |this, cx| this.set_language(Some(rust_language), cx));
364        let editor2 = cx
365            .new_window_entity(|window, cx| Editor::for_buffer(buffer2, Some(project), window, cx));
366
367        let first_context = workspace
368            .update_in(cx, |workspace, window, cx| {
369                workspace.add_item_to_center(Box::new(editor1.clone()), window, cx);
370                workspace.add_item_to_center(Box::new(editor2.clone()), window, cx);
371                assert_eq!(
372                    workspace.active_item(cx).unwrap().item_id(),
373                    editor2.entity_id()
374                );
375                task_context(workspace, window, cx)
376            })
377            .await;
378        assert_eq!(
379            first_context,
380            TaskContext {
381                cwd: Some("/dir".into()),
382                task_variables: TaskVariables::from_iter([
383                    (VariableName::File, "/dir/rust/b.rs".into()),
384                    (VariableName::Filename, "b.rs".into()),
385                    (VariableName::RelativeFile, "rust/b.rs".into()),
386                    (VariableName::Dirname, "/dir/rust".into()),
387                    (VariableName::Stem, "b".into()),
388                    (VariableName::WorktreeRoot, "/dir".into()),
389                    (VariableName::Row, "1".into()),
390                    (VariableName::Column, "1".into()),
391                ]),
392                project_env: HashMap::default(),
393            }
394        );
395
396        // And now, let's select an identifier.
397        editor2.update_in(cx, |editor, window, cx| {
398            editor.change_selections(None, window, cx, |selections| {
399                selections.select_ranges([14..18])
400            })
401        });
402
403        assert_eq!(
404            workspace
405                .update_in(cx, |workspace, window, cx| {
406                    task_context(workspace, window, cx)
407                })
408                .await,
409            TaskContext {
410                cwd: Some("/dir".into()),
411                task_variables: TaskVariables::from_iter([
412                    (VariableName::File, "/dir/rust/b.rs".into()),
413                    (VariableName::Filename, "b.rs".into()),
414                    (VariableName::RelativeFile, "rust/b.rs".into()),
415                    (VariableName::Dirname, "/dir/rust".into()),
416                    (VariableName::Stem, "b".into()),
417                    (VariableName::WorktreeRoot, "/dir".into()),
418                    (VariableName::Row, "1".into()),
419                    (VariableName::Column, "15".into()),
420                    (VariableName::SelectedText, "is_i".into()),
421                    (VariableName::Symbol, "this_is_a_rust_file".into()),
422                ]),
423                project_env: HashMap::default(),
424            }
425        );
426
427        assert_eq!(
428            workspace
429                .update_in(cx, |workspace, window, cx| {
430                    // Now, let's switch the active item to .ts file.
431                    workspace.activate_item(&editor1, true, true, window, cx);
432                    task_context(workspace, window, cx)
433                })
434                .await,
435            TaskContext {
436                cwd: Some("/dir".into()),
437                task_variables: TaskVariables::from_iter([
438                    (VariableName::File, "/dir/a.ts".into()),
439                    (VariableName::Filename, "a.ts".into()),
440                    (VariableName::RelativeFile, "a.ts".into()),
441                    (VariableName::Dirname, "/dir".into()),
442                    (VariableName::Stem, "a".into()),
443                    (VariableName::WorktreeRoot, "/dir".into()),
444                    (VariableName::Row, "1".into()),
445                    (VariableName::Column, "1".into()),
446                    (VariableName::Symbol, "this_is_a_test".into()),
447                ]),
448                project_env: HashMap::default(),
449            }
450        );
451    }
452
453    pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
454        cx.update(|cx| {
455            let state = AppState::test(cx);
456            file_icons::init((), cx);
457            language::init(cx);
458            crate::init(cx);
459            editor::init(cx);
460            workspace::init_settings(cx);
461            Project::init_settings(cx);
462            TaskStore::init(None);
463            state
464        })
465    }
466}