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