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