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