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