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