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.project().update(cx, |project, cx| {
152 project.is_local() || project.ssh_connection_string(cx).is_some() || project.is_via_ssh()
153 });
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 target_task.reveal_target = target_override;
233 }
234 workspace.schedule_task(
235 task_source_kind.clone(),
236 target_task,
237 active_context,
238 false,
239 window,
240 cx,
241 );
242 true
243 } else {
244 false
245 }
246 });
247
248 if tasks.is_empty() { None } else { Some(()) }
249 })?
250 .is_some();
251 if !did_spawn {
252 workspace
253 .update_in(cx, |workspace, window, cx| {
254 spawn_task_or_modal(
255 workspace,
256 &Spawn::ViaModal {
257 reveal_target: overrides.and_then(|overrides| overrides.reveal_target),
258 },
259 window,
260 cx,
261 );
262 })
263 .ok();
264 }
265
266 Ok(())
267 })
268}
269
270pub fn task_contexts(
271 workspace: &Workspace,
272 window: &mut Window,
273 cx: &mut App,
274) -> Task<TaskContexts> {
275 let active_item = workspace.active_item(cx);
276 let active_worktree = active_item
277 .as_ref()
278 .and_then(|item| item.project_path(cx))
279 .map(|project_path| project_path.worktree_id)
280 .filter(|worktree_id| {
281 workspace
282 .project()
283 .read(cx)
284 .worktree_for_id(*worktree_id, cx)
285 .map_or(false, |worktree| is_visible_directory(&worktree, cx))
286 })
287 .or_else(|| {
288 workspace
289 .visible_worktrees(cx)
290 .next()
291 .map(|tree| tree.read(cx).id())
292 });
293
294 let active_editor = active_item.and_then(|item| item.act_as::<Editor>(cx));
295
296 let editor_context_task = active_editor.as_ref().map(|active_editor| {
297 active_editor.update(cx, |editor, cx| editor.task_context(window, cx))
298 });
299
300 let location = active_editor.as_ref().and_then(|editor| {
301 editor.update(cx, |editor, cx| {
302 let selection = editor.selections.newest_anchor();
303 let multi_buffer = editor.buffer().clone();
304 let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
305 let (buffer_snapshot, buffer_offset) =
306 multi_buffer_snapshot.point_to_buffer_offset(selection.head())?;
307 let buffer_anchor = buffer_snapshot.anchor_before(buffer_offset);
308 let buffer = multi_buffer.read(cx).buffer(buffer_snapshot.remote_id())?;
309 Some(Location {
310 buffer,
311 range: buffer_anchor..buffer_anchor,
312 })
313 })
314 });
315
316 let lsp_task_sources = active_editor
317 .as_ref()
318 .map(|active_editor| active_editor.update(cx, |editor, cx| editor.lsp_task_sources(cx)))
319 .unwrap_or_default();
320
321 let latest_selection = active_editor.as_ref().map(|active_editor| {
322 active_editor
323 .read(cx)
324 .selections
325 .newest_anchor()
326 .head()
327 .text_anchor
328 });
329
330 let mut worktree_abs_paths = workspace
331 .worktrees(cx)
332 .filter(|worktree| is_visible_directory(worktree, cx))
333 .map(|worktree| {
334 let worktree = worktree.read(cx);
335 (worktree.id(), worktree.abs_path())
336 })
337 .collect::<HashMap<_, _>>();
338
339 cx.background_spawn(async move {
340 let mut task_contexts = TaskContexts::default();
341
342 task_contexts.lsp_task_sources = lsp_task_sources;
343 task_contexts.latest_selection = latest_selection;
344
345 if let Some(editor_context_task) = editor_context_task
346 && let Some(editor_context) = editor_context_task.await {
347 task_contexts.active_item_context =
348 Some((active_worktree, location, editor_context));
349 }
350
351 if let Some(active_worktree) = active_worktree {
352 if let Some(active_worktree_abs_path) = worktree_abs_paths.remove(&active_worktree) {
353 task_contexts.active_worktree_context =
354 Some((active_worktree, worktree_context(&active_worktree_abs_path)));
355 }
356 } else if worktree_abs_paths.len() == 1 {
357 task_contexts.active_worktree_context = worktree_abs_paths
358 .drain()
359 .next()
360 .map(|(id, abs_path)| (id, worktree_context(&abs_path)));
361 }
362
363 task_contexts.other_worktree_contexts.extend(
364 worktree_abs_paths
365 .into_iter()
366 .map(|(id, abs_path)| (id, worktree_context(&abs_path))),
367 );
368 task_contexts
369 })
370}
371
372fn is_visible_directory(worktree: &Entity<Worktree>, cx: &App) -> bool {
373 let worktree = worktree.read(cx);
374 worktree.is_visible() && worktree.root_entry().map_or(false, |entry| entry.is_dir())
375}
376
377fn worktree_context(worktree_abs_path: &Path) -> TaskContext {
378 let mut task_variables = TaskVariables::default();
379 task_variables.insert(
380 VariableName::WorktreeRoot,
381 worktree_abs_path.to_string_lossy().to_string(),
382 );
383 TaskContext {
384 cwd: Some(worktree_abs_path.to_path_buf()),
385 task_variables,
386 project_env: HashMap::default(),
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use std::{collections::HashMap, sync::Arc};
393
394 use editor::{Editor, SelectionEffects};
395 use gpui::TestAppContext;
396 use language::{Language, LanguageConfig};
397 use project::{BasicContextProvider, FakeFs, Project, task_store::TaskStore};
398 use serde_json::json;
399 use task::{TaskContext, TaskVariables, VariableName};
400 use ui::VisualContext;
401 use util::path;
402 use workspace::{AppState, Workspace};
403
404 use crate::task_contexts;
405
406 #[gpui::test]
407 async fn test_default_language_context(cx: &mut TestAppContext) {
408 init_test(cx);
409 let fs = FakeFs::new(cx.executor());
410 fs.insert_tree(
411 path!("/dir"),
412 json!({
413 ".zed": {
414 "tasks.json": r#"[
415 {
416 "label": "example task",
417 "command": "echo",
418 "args": ["4"]
419 },
420 {
421 "label": "another one",
422 "command": "echo",
423 "args": ["55"]
424 },
425 ]"#,
426 },
427 "a.ts": "function this_is_a_test() { }",
428 "rust": {
429 "b.rs": "use std; fn this_is_a_rust_file() { }",
430 }
431
432 }),
433 )
434 .await;
435 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
436 let worktree_store = project.read_with(cx, |project, _| project.worktree_store().clone());
437 let rust_language = Arc::new(
438 Language::new(
439 LanguageConfig::default(),
440 Some(tree_sitter_rust::LANGUAGE.into()),
441 )
442 .with_outline_query(
443 r#"(function_item
444 "fn" @context
445 name: (_) @name) @item"#,
446 )
447 .unwrap()
448 .with_context_provider(Some(Arc::new(BasicContextProvider::new(
449 worktree_store.clone(),
450 )))),
451 );
452
453 let typescript_language = Arc::new(
454 Language::new(
455 LanguageConfig::default(),
456 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
457 )
458 .with_outline_query(
459 r#"(function_declaration
460 "async"? @context
461 "function" @context
462 name: (_) @name
463 parameters: (formal_parameters
464 "(" @context
465 ")" @context)) @item"#,
466 )
467 .unwrap()
468 .with_context_provider(Some(Arc::new(BasicContextProvider::new(
469 worktree_store.clone(),
470 )))),
471 );
472
473 let worktree_id = project.update(cx, |project, cx| {
474 project.worktrees(cx).next().unwrap().read(cx).id()
475 });
476 let (workspace, cx) =
477 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
478
479 let buffer1 = workspace
480 .update(cx, |this, cx| {
481 this.project()
482 .update(cx, |this, cx| this.open_buffer((worktree_id, "a.ts"), cx))
483 })
484 .await
485 .unwrap();
486 buffer1.update(cx, |this, cx| {
487 this.set_language(Some(typescript_language), cx)
488 });
489 let editor1 = cx.new_window_entity(|window, cx| {
490 Editor::for_buffer(buffer1, Some(project.clone()), window, cx)
491 });
492 let buffer2 = workspace
493 .update(cx, |this, cx| {
494 this.project().update(cx, |this, cx| {
495 this.open_buffer((worktree_id, "rust/b.rs"), cx)
496 })
497 })
498 .await
499 .unwrap();
500 buffer2.update(cx, |this, cx| this.set_language(Some(rust_language), cx));
501 let editor2 = cx
502 .new_window_entity(|window, cx| Editor::for_buffer(buffer2, Some(project), window, cx));
503
504 let first_context = workspace
505 .update_in(cx, |workspace, window, cx| {
506 workspace.add_item_to_center(Box::new(editor1.clone()), window, cx);
507 workspace.add_item_to_center(Box::new(editor2.clone()), window, cx);
508 assert_eq!(
509 workspace.active_item(cx).unwrap().item_id(),
510 editor2.entity_id()
511 );
512 task_contexts(workspace, window, cx)
513 })
514 .await;
515
516 assert_eq!(
517 first_context
518 .active_context()
519 .expect("Should have an active context"),
520 &TaskContext {
521 cwd: Some(path!("/dir").into()),
522 task_variables: TaskVariables::from_iter([
523 (VariableName::File, path!("/dir/rust/b.rs").into()),
524 (VariableName::Filename, "b.rs".into()),
525 (VariableName::RelativeFile, path!("rust/b.rs").into()),
526 (VariableName::RelativeDir, "rust".into()),
527 (VariableName::Dirname, path!("/dir/rust").into()),
528 (VariableName::Stem, "b".into()),
529 (VariableName::WorktreeRoot, path!("/dir").into()),
530 (VariableName::Row, "1".into()),
531 (VariableName::Column, "1".into()),
532 ]),
533 project_env: HashMap::default(),
534 }
535 );
536
537 // And now, let's select an identifier.
538 editor2.update_in(cx, |editor, window, cx| {
539 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
540 selections.select_ranges([14..18])
541 })
542 });
543
544 assert_eq!(
545 workspace
546 .update_in(cx, |workspace, window, cx| {
547 task_contexts(workspace, window, cx)
548 })
549 .await
550 .active_context()
551 .expect("Should have an active context"),
552 &TaskContext {
553 cwd: Some(path!("/dir").into()),
554 task_variables: TaskVariables::from_iter([
555 (VariableName::File, path!("/dir/rust/b.rs").into()),
556 (VariableName::Filename, "b.rs".into()),
557 (VariableName::RelativeFile, path!("rust/b.rs").into()),
558 (VariableName::RelativeDir, "rust".into()),
559 (VariableName::Dirname, path!("/dir/rust").into()),
560 (VariableName::Stem, "b".into()),
561 (VariableName::WorktreeRoot, path!("/dir").into()),
562 (VariableName::Row, "1".into()),
563 (VariableName::Column, "15".into()),
564 (VariableName::SelectedText, "is_i".into()),
565 (VariableName::Symbol, "this_is_a_rust_file".into()),
566 ]),
567 project_env: HashMap::default(),
568 }
569 );
570
571 assert_eq!(
572 workspace
573 .update_in(cx, |workspace, window, cx| {
574 // Now, let's switch the active item to .ts file.
575 workspace.activate_item(&editor1, true, true, window, cx);
576 task_contexts(workspace, window, cx)
577 })
578 .await
579 .active_context()
580 .expect("Should have an active context"),
581 &TaskContext {
582 cwd: Some(path!("/dir").into()),
583 task_variables: TaskVariables::from_iter([
584 (VariableName::File, path!("/dir/a.ts").into()),
585 (VariableName::Filename, "a.ts".into()),
586 (VariableName::RelativeFile, "a.ts".into()),
587 (VariableName::RelativeDir, ".".into()),
588 (VariableName::Dirname, path!("/dir").into()),
589 (VariableName::Stem, "a".into()),
590 (VariableName::WorktreeRoot, path!("/dir").into()),
591 (VariableName::Row, "1".into()),
592 (VariableName::Column, "1".into()),
593 (VariableName::Symbol, "this_is_a_test".into()),
594 ]),
595 project_env: HashMap::default(),
596 }
597 );
598 }
599
600 pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
601 cx.update(|cx| {
602 let state = AppState::test(cx);
603 language::init(cx);
604 crate::init(cx);
605 editor::init(cx);
606 workspace::init_settings(cx);
607 Project::init_settings(cx);
608 TaskStore::init(None);
609 state
610 })
611 }
612}