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