tasks_ui.rs

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