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