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| active_editor.update(cx, |editor, cx| editor.lsp_task_sources(cx)))
320 .unwrap_or_default();
321
322 let latest_selection = active_editor.as_ref().map(|active_editor| {
323 active_editor
324 .read(cx)
325 .selections
326 .newest_anchor()
327 .head()
328 .text_anchor
329 });
330
331 let mut worktree_abs_paths = workspace
332 .worktrees(cx)
333 .filter(|worktree| is_visible_directory(worktree, cx))
334 .map(|worktree| {
335 let worktree = worktree.read(cx);
336 (worktree.id(), worktree.abs_path())
337 })
338 .collect::<HashMap<_, _>>();
339
340 cx.background_spawn(async move {
341 let mut task_contexts = TaskContexts::default();
342
343 task_contexts.lsp_task_sources = lsp_task_sources;
344 task_contexts.latest_selection = latest_selection;
345
346 if let Some(editor_context_task) = editor_context_task
347 && let Some(editor_context) = editor_context_task.await
348 {
349 task_contexts.active_item_context = Some((active_worktree, location, editor_context));
350 }
351
352 if let Some(active_worktree) = active_worktree {
353 if let Some(active_worktree_abs_path) = worktree_abs_paths.remove(&active_worktree) {
354 task_contexts.active_worktree_context =
355 Some((active_worktree, worktree_context(&active_worktree_abs_path)));
356 }
357 } else if worktree_abs_paths.len() == 1 {
358 task_contexts.active_worktree_context = worktree_abs_paths
359 .drain()
360 .next()
361 .map(|(id, abs_path)| (id, worktree_context(&abs_path)));
362 }
363
364 task_contexts.other_worktree_contexts.extend(
365 worktree_abs_paths
366 .into_iter()
367 .map(|(id, abs_path)| (id, worktree_context(&abs_path))),
368 );
369 task_contexts
370 })
371}
372
373fn is_visible_directory(worktree: &Entity<Worktree>, cx: &App) -> bool {
374 let worktree = worktree.read(cx);
375 worktree.is_visible() && worktree.root_entry().is_some_and(|entry| entry.is_dir())
376}
377
378fn worktree_context(worktree_abs_path: &Path) -> TaskContext {
379 let mut task_variables = TaskVariables::default();
380 task_variables.insert(
381 VariableName::WorktreeRoot,
382 worktree_abs_path.to_string_lossy().into_owned(),
383 );
384 TaskContext {
385 cwd: Some(worktree_abs_path.to_path_buf()),
386 task_variables,
387 project_env: HashMap::default(),
388 }
389}
390
391#[cfg(test)]
392mod tests {
393 use std::{collections::HashMap, sync::Arc};
394
395 use editor::{Editor, SelectionEffects};
396 use gpui::TestAppContext;
397 use language::{Language, LanguageConfig};
398 use project::{BasicContextProvider, FakeFs, Project, task_store::TaskStore};
399 use serde_json::json;
400 use task::{TaskContext, TaskVariables, VariableName};
401 use ui::VisualContext;
402 use util::{path, rel_path::rel_path};
403 use workspace::{AppState, Workspace};
404
405 use crate::task_contexts;
406
407 #[gpui::test]
408 async fn test_default_language_context(cx: &mut TestAppContext) {
409 init_test(cx);
410 let fs = FakeFs::new(cx.executor());
411 fs.insert_tree(
412 path!("/dir"),
413 json!({
414 ".zed": {
415 "tasks.json": r#"[
416 {
417 "label": "example task",
418 "command": "echo",
419 "args": ["4"]
420 },
421 {
422 "label": "another one",
423 "command": "echo",
424 "args": ["55"]
425 },
426 ]"#,
427 },
428 "a.ts": "function this_is_a_test() { }",
429 "rust": {
430 "b.rs": "use std; fn this_is_a_rust_file() { }",
431 }
432
433 }),
434 )
435 .await;
436 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
437 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
438 let rust_language = Arc::new(
439 Language::new(
440 LanguageConfig::default(),
441 Some(tree_sitter_rust::LANGUAGE.into()),
442 )
443 .with_outline_query(
444 r#"(function_item
445 "fn" @context
446 name: (_) @name) @item"#,
447 )
448 .unwrap()
449 .with_context_provider(Some(Arc::new(BasicContextProvider::new(
450 worktree_store.clone(),
451 )))),
452 );
453
454 let typescript_language = Arc::new(
455 Language::new(
456 LanguageConfig::default(),
457 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
458 )
459 .with_outline_query(
460 r#"(function_declaration
461 "async"? @context
462 "function" @context
463 name: (_) @name
464 parameters: (formal_parameters
465 "(" @context
466 ")" @context)) @item"#,
467 )
468 .unwrap()
469 .with_context_provider(Some(Arc::new(BasicContextProvider::new(
470 worktree_store.clone(),
471 )))),
472 );
473
474 let worktree_id = project.update(cx, |project, cx| {
475 project.worktrees(cx).next().unwrap().read(cx).id()
476 });
477 let (workspace, cx) =
478 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
479
480 let buffer1 = workspace
481 .update(cx, |this, cx| {
482 this.project().update(cx, |this, cx| {
483 this.open_buffer((worktree_id, rel_path("a.ts")), cx)
484 })
485 })
486 .await
487 .unwrap();
488 buffer1.update(cx, |this, cx| {
489 this.set_language(Some(typescript_language), cx)
490 });
491 let editor1 = cx.new_window_entity(|window, cx| {
492 Editor::for_buffer(buffer1, Some(project.clone()), window, cx)
493 });
494 let buffer2 = workspace
495 .update(cx, |this, cx| {
496 this.project().update(cx, |this, cx| {
497 this.open_buffer((worktree_id, rel_path("rust/b.rs")), cx)
498 })
499 })
500 .await
501 .unwrap();
502 buffer2.update(cx, |this, cx| this.set_language(Some(rust_language), cx));
503 let editor2 = cx
504 .new_window_entity(|window, cx| Editor::for_buffer(buffer2, Some(project), window, cx));
505
506 let first_context = workspace
507 .update_in(cx, |workspace, window, cx| {
508 workspace.add_item_to_center(Box::new(editor1.clone()), window, cx);
509 workspace.add_item_to_center(Box::new(editor2.clone()), window, cx);
510 assert_eq!(
511 workspace.active_item(cx).unwrap().item_id(),
512 editor2.entity_id()
513 );
514 task_contexts(workspace, window, cx)
515 })
516 .await;
517
518 assert_eq!(
519 first_context
520 .active_context()
521 .expect("Should have an active context"),
522 &TaskContext {
523 cwd: Some(path!("/dir").into()),
524 task_variables: TaskVariables::from_iter([
525 (VariableName::File, path!("/dir/rust/b.rs").into()),
526 (VariableName::Filename, "b.rs".into()),
527 (VariableName::RelativeFile, path!("rust/b.rs").into()),
528 (VariableName::RelativeDir, "rust".into()),
529 (VariableName::Dirname, path!("/dir/rust").into()),
530 (VariableName::Stem, "b".into()),
531 (VariableName::WorktreeRoot, path!("/dir").into()),
532 (VariableName::Row, "1".into()),
533 (VariableName::Column, "1".into()),
534 ]),
535 project_env: HashMap::default(),
536 }
537 );
538
539 // And now, let's select an identifier.
540 editor2.update_in(cx, |editor, window, cx| {
541 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
542 selections.select_ranges([14..18])
543 })
544 });
545
546 assert_eq!(
547 workspace
548 .update_in(cx, |workspace, window, cx| {
549 task_contexts(workspace, window, cx)
550 })
551 .await
552 .active_context()
553 .expect("Should have an active context"),
554 &TaskContext {
555 cwd: Some(path!("/dir").into()),
556 task_variables: TaskVariables::from_iter([
557 (VariableName::File, path!("/dir/rust/b.rs").into()),
558 (VariableName::Filename, "b.rs".into()),
559 (VariableName::RelativeFile, path!("rust/b.rs").into()),
560 (VariableName::RelativeDir, "rust".into()),
561 (VariableName::Dirname, path!("/dir/rust").into()),
562 (VariableName::Stem, "b".into()),
563 (VariableName::WorktreeRoot, path!("/dir").into()),
564 (VariableName::Row, "1".into()),
565 (VariableName::Column, "15".into()),
566 (VariableName::SelectedText, "is_i".into()),
567 (VariableName::Symbol, "this_is_a_rust_file".into()),
568 ]),
569 project_env: HashMap::default(),
570 }
571 );
572
573 assert_eq!(
574 workspace
575 .update_in(cx, |workspace, window, cx| {
576 // Now, let's switch the active item to .ts file.
577 workspace.activate_item(&editor1, true, true, window, cx);
578 task_contexts(workspace, window, cx)
579 })
580 .await
581 .active_context()
582 .expect("Should have an active context"),
583 &TaskContext {
584 cwd: Some(path!("/dir").into()),
585 task_variables: TaskVariables::from_iter([
586 (VariableName::File, path!("/dir/a.ts").into()),
587 (VariableName::Filename, "a.ts".into()),
588 (VariableName::RelativeFile, "a.ts".into()),
589 (VariableName::RelativeDir, ".".into()),
590 (VariableName::Dirname, path!("/dir").into()),
591 (VariableName::Stem, "a".into()),
592 (VariableName::WorktreeRoot, path!("/dir").into()),
593 (VariableName::Row, "1".into()),
594 (VariableName::Column, "1".into()),
595 (VariableName::Symbol, "this_is_a_test".into()),
596 ]),
597 project_env: HashMap::default(),
598 }
599 );
600 }
601
602 pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
603 cx.update(|cx| {
604 let state = AppState::test(cx);
605 language::init(cx);
606 crate::init(cx);
607 editor::init(cx);
608 workspace::init_settings(cx);
609 Project::init_settings(cx);
610 TaskStore::init(None);
611 state
612 })
613 }
614}