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 (file, language) = task_contexts
208 .location()
209 .map(|location| {
210 let buffer = location.buffer.read(cx);
211 (
212 buffer.file().cloned(),
213 buffer.language_at(location.range.start),
214 )
215 })
216 .unwrap_or_default();
217 task_inventory
218 .read(cx)
219 .list_tasks(file, 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::default(),
443 Some(tree_sitter_rust::LANGUAGE.into()),
444 )
445 .with_outline_query(
446 r#"(function_item
447 "fn" @context
448 name: (_) @name) @item"#,
449 )
450 .unwrap()
451 .with_context_provider(Some(Arc::new(BasicContextProvider::new(
452 worktree_store.clone(),
453 )))),
454 );
455
456 let typescript_language = Arc::new(
457 Language::new(
458 LanguageConfig::default(),
459 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
460 )
461 .with_outline_query(
462 r#"(function_declaration
463 "async"? @context
464 "function" @context
465 name: (_) @name
466 parameters: (formal_parameters
467 "(" @context
468 ")" @context)) @item"#,
469 )
470 .unwrap()
471 .with_context_provider(Some(Arc::new(BasicContextProvider::new(
472 worktree_store.clone(),
473 )))),
474 );
475
476 let worktree_id = project.update(cx, |project, cx| {
477 project.worktrees(cx).next().unwrap().read(cx).id()
478 });
479 let (multi_workspace, cx) =
480 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
481 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
482
483 let buffer1 = workspace
484 .update(cx, |this, cx| {
485 this.project().update(cx, |this, cx| {
486 this.open_buffer((worktree_id, rel_path("a.ts")), cx)
487 })
488 })
489 .await
490 .unwrap();
491 buffer1.update(cx, |this, cx| {
492 this.set_language(Some(typescript_language), cx)
493 });
494 let editor1 = cx.new_window_entity(|window, cx| {
495 Editor::for_buffer(buffer1, Some(project.clone()), window, cx)
496 });
497 let buffer2 = workspace
498 .update(cx, |this, cx| {
499 this.project().update(cx, |this, cx| {
500 this.open_buffer((worktree_id, rel_path("rust/b.rs")), cx)
501 })
502 })
503 .await
504 .unwrap();
505 buffer2.update(cx, |this, cx| this.set_language(Some(rust_language), cx));
506 let editor2 = cx
507 .new_window_entity(|window, cx| Editor::for_buffer(buffer2, Some(project), window, cx));
508
509 let first_context = workspace
510 .update_in(cx, |workspace, window, cx| {
511 workspace.add_item_to_center(Box::new(editor1.clone()), window, cx);
512 workspace.add_item_to_center(Box::new(editor2.clone()), window, cx);
513 assert_eq!(
514 workspace.active_item(cx).unwrap().item_id(),
515 editor2.entity_id()
516 );
517 task_contexts(workspace, window, cx)
518 })
519 .await;
520
521 assert_eq!(
522 first_context
523 .active_context()
524 .expect("Should have an active context"),
525 &TaskContext {
526 cwd: Some(path!("/dir").into()),
527 task_variables: TaskVariables::from_iter([
528 (VariableName::File, path!("/dir/rust/b.rs").into()),
529 (VariableName::Filename, "b.rs".into()),
530 (VariableName::RelativeFile, path!("rust/b.rs").into()),
531 (VariableName::RelativeDir, "rust".into()),
532 (VariableName::Dirname, path!("/dir/rust").into()),
533 (VariableName::Stem, "b".into()),
534 (VariableName::WorktreeRoot, path!("/dir").into()),
535 (VariableName::Row, "1".into()),
536 (VariableName::Column, "1".into()),
537 ]),
538 project_env: HashMap::default(),
539 }
540 );
541
542 // And now, let's select an identifier.
543 editor2.update_in(cx, |editor, window, cx| {
544 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
545 selections.select_ranges([MultiBufferOffset(14)..MultiBufferOffset(18)])
546 })
547 });
548
549 assert_eq!(
550 workspace
551 .update_in(cx, |workspace, window, cx| {
552 task_contexts(workspace, window, cx)
553 })
554 .await
555 .active_context()
556 .expect("Should have an active context"),
557 &TaskContext {
558 cwd: Some(path!("/dir").into()),
559 task_variables: TaskVariables::from_iter([
560 (VariableName::File, path!("/dir/rust/b.rs").into()),
561 (VariableName::Filename, "b.rs".into()),
562 (VariableName::RelativeFile, path!("rust/b.rs").into()),
563 (VariableName::RelativeDir, "rust".into()),
564 (VariableName::Dirname, path!("/dir/rust").into()),
565 (VariableName::Stem, "b".into()),
566 (VariableName::WorktreeRoot, path!("/dir").into()),
567 (VariableName::Row, "1".into()),
568 (VariableName::Column, "15".into()),
569 (VariableName::SelectedText, "is_i".into()),
570 (VariableName::Symbol, "this_is_a_rust_file".into()),
571 ]),
572 project_env: HashMap::default(),
573 }
574 );
575
576 assert_eq!(
577 workspace
578 .update_in(cx, |workspace, window, cx| {
579 // Now, let's switch the active item to .ts file.
580 workspace.activate_item(&editor1, true, true, window, cx);
581 task_contexts(workspace, window, cx)
582 })
583 .await
584 .active_context()
585 .expect("Should have an active context"),
586 &TaskContext {
587 cwd: Some(path!("/dir").into()),
588 task_variables: TaskVariables::from_iter([
589 (VariableName::File, path!("/dir/a.ts").into()),
590 (VariableName::Filename, "a.ts".into()),
591 (VariableName::RelativeFile, "a.ts".into()),
592 (VariableName::RelativeDir, ".".into()),
593 (VariableName::Dirname, path!("/dir").into()),
594 (VariableName::Stem, "a".into()),
595 (VariableName::WorktreeRoot, path!("/dir").into()),
596 (VariableName::Row, "1".into()),
597 (VariableName::Column, "1".into()),
598 (VariableName::Symbol, "this_is_a_test".into()),
599 ]),
600 project_env: HashMap::default(),
601 }
602 );
603 }
604
605 pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
606 cx.update(|cx| {
607 let state = AppState::test(cx);
608 crate::init(cx);
609 editor::init(cx);
610 TaskStore::init(None);
611 state
612 })
613 }
614}