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