Skip to repository content713 lines · 27.3 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T03:36:56.916Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
tasks_ui.rs
1use std::{
2 path::Path,
3 sync::{Arc, LazyLock},
4};
5
6use anyhow::Context as _;
7use collections::HashMap;
8use editor::{Editor, MultiBufferOffset, ToPoint as _};
9use gpui::{App, AppContext as _, Context, Entity, Task, TaskExt, Window};
10use project::{Location, TaskContexts, TaskSourceKind, Worktree};
11use task::{RevealTarget, TaskContext, TaskId, TaskTemplate, TaskVariables, VariableName};
12use tree_sitter::{Query, StreamingIterator as _};
13use workspace::Workspace;
14
15mod modal;
16
17pub use modal::{Rerun, ShowAttachModal, Spawn, TaskOverrides, TasksModal};
18
19/// Inserts `new_task` (pretty-printed JSON object text) at the end of the top-level JSON
20/// array in the editor's buffer, creating the array if the buffer has none, and moves the
21/// cursor to the inserted task. The edit is left unsaved so callers decide whether to persist it.
22pub fn insert_task_json_into_editor(
23 editor: &mut Editor,
24 new_task: String,
25 window: &mut Window,
26 cx: &mut Context<Editor>,
27) -> anyhow::Result<()> {
28 static LAST_ITEM_QUERY: LazyLock<Query> = LazyLock::new(|| {
29 Query::new(
30 &tree_sitter_json::LANGUAGE.into(),
31 "(document (array (object) @object))", // TODO: use "." anchor to only match last object
32 )
33 .expect("Failed to create LAST_ITEM_QUERY")
34 });
35 static EMPTY_ARRAY_QUERY: LazyLock<Query> = LazyLock::new(|| {
36 Query::new(
37 &tree_sitter_json::LANGUAGE.into(),
38 "(document (array) @array)",
39 )
40 .expect("Failed to create EMPTY_ARRAY_QUERY")
41 });
42
43 let content = editor.text(cx);
44 let mut parser = tree_sitter::Parser::new();
45 parser.set_language(&tree_sitter_json::LANGUAGE.into())?;
46 let mut cursor = tree_sitter::QueryCursor::new();
47 let syntax_tree = parser
48 .parse(&content, None)
49 .context("could not parse tasks file")?;
50 let mut matches = cursor.matches(
51 &LAST_ITEM_QUERY,
52 syntax_tree.root_node(),
53 content.as_bytes(),
54 );
55
56 let mut last_offset = None;
57 while let Some(mat) = matches.next() {
58 if let Some(pos) = mat.captures.first().map(|m| m.node.byte_range().end) {
59 last_offset = Some(MultiBufferOffset(pos))
60 }
61 }
62 let mut edits = Vec::new();
63 let mut cursor_position = MultiBufferOffset(0);
64
65 if let Some(pos) = last_offset {
66 edits.push((pos..pos, format!(",\n{new_task}")));
67 cursor_position = pos + ",\n ".len();
68 } else {
69 let mut matches = cursor.matches(
70 &EMPTY_ARRAY_QUERY,
71 syntax_tree.root_node(),
72 content.as_bytes(),
73 );
74
75 if let Some(mat) = matches.next() {
76 if let Some(pos) = mat.captures.first().map(|m| m.node.byte_range().end - 1) {
77 edits.push((
78 MultiBufferOffset(pos)..MultiBufferOffset(pos),
79 format!("\n{new_task}\n"),
80 ));
81 cursor_position = MultiBufferOffset(pos) + "\n ".len();
82 }
83 } else {
84 edits.push((
85 MultiBufferOffset(0)..MultiBufferOffset(0),
86 format!("[\n{}\n]", new_task),
87 ));
88 cursor_position = MultiBufferOffset("[\n ".len());
89 }
90 }
91 editor.transact(window, cx, |editor, window, cx| {
92 editor.edit(edits, cx);
93 let snapshot = editor.buffer().read(cx).read(cx);
94 let point = cursor_position.to_point(&snapshot);
95 drop(snapshot);
96 editor.go_to_singleton_buffer_point(point, window, cx);
97 });
98 Ok(())
99}
100
101pub fn init(cx: &mut App) {
102 cx.observe_new(
103 |workspace: &mut Workspace, _: Option<&mut Window>, _: &mut Context<Workspace>| {
104 workspace
105 .register_action(spawn_task_or_modal)
106 .register_action(move |workspace, action: &modal::Rerun, window, cx| {
107 if let Some((task_source_kind, mut last_scheduled_task)) = workspace
108 .project()
109 .read(cx)
110 .task_store()
111 .read(cx)
112 .task_inventory()
113 .and_then(|inventory| {
114 inventory.read(cx).last_scheduled_task(
115 action
116 .task_id
117 .as_ref()
118 .map(|id| TaskId(id.clone()))
119 .as_ref(),
120 )
121 })
122 {
123 if action.reevaluate_context {
124 let mut original_task = last_scheduled_task.original_task().clone();
125 if let Some(allow_concurrent_runs) = action.allow_concurrent_runs {
126 original_task.allow_concurrent_runs = allow_concurrent_runs;
127 }
128 if let Some(use_new_terminal) = action.use_new_terminal {
129 original_task.use_new_terminal = use_new_terminal;
130 }
131 let task_contexts = task_contexts(workspace, window, cx);
132 cx.spawn_in(window, async move |workspace, cx| {
133 let task_contexts = task_contexts.await;
134 let default_context = TaskContext::default();
135 workspace
136 .update_in(cx, |workspace, window, cx| {
137 workspace.schedule_task(
138 task_source_kind,
139 &original_task,
140 task_contexts
141 .active_context()
142 .unwrap_or(&default_context),
143 false,
144 window,
145 cx,
146 )
147 })
148 .ok()
149 })
150 .detach()
151 } else {
152 let resolved = &mut last_scheduled_task.resolved;
153
154 if let Some(allow_concurrent_runs) = action.allow_concurrent_runs {
155 resolved.allow_concurrent_runs = allow_concurrent_runs;
156 }
157 if let Some(use_new_terminal) = action.use_new_terminal {
158 resolved.use_new_terminal = use_new_terminal;
159 }
160
161 workspace.schedule_resolved_task(
162 task_source_kind,
163 last_scheduled_task,
164 false,
165 window,
166 cx,
167 );
168 }
169 } else {
170 spawn_task_or_modal(
171 workspace,
172 &Spawn::ViaModal {
173 reveal_target: None,
174 },
175 window,
176 cx,
177 );
178 };
179 });
180 },
181 )
182 .detach();
183}
184
185fn spawn_task_or_modal(
186 workspace: &mut Workspace,
187 action: &Spawn,
188 window: &mut Window,
189 cx: &mut Context<Workspace>,
190) {
191 if let Some(provider) = workspace.debugger_provider() {
192 provider.spawn_task_or_modal(workspace, action, window, cx);
193 return;
194 }
195
196 match action {
197 Spawn::ByName {
198 task_name,
199 reveal_target,
200 } => {
201 let overrides = reveal_target.map(|reveal_target| TaskOverrides {
202 reveal_target: Some(reveal_target),
203 });
204 let name = task_name.clone();
205 spawn_tasks_filtered(move |(_, task)| task.label.eq(&name), overrides, window, cx)
206 .detach_and_log_err(cx)
207 }
208 Spawn::ByTag {
209 task_tag,
210 reveal_target,
211 } => {
212 let overrides = reveal_target.map(|reveal_target| TaskOverrides {
213 reveal_target: Some(reveal_target),
214 });
215 let tag = task_tag.clone();
216 spawn_tasks_filtered(
217 move |(_, task)| task.tags.contains(&tag),
218 overrides,
219 window,
220 cx,
221 )
222 .detach_and_log_err(cx)
223 }
224 Spawn::ViaModal { reveal_target } => {
225 toggle_modal(workspace, *reveal_target, window, cx).detach()
226 }
227 }
228}
229
230pub fn toggle_modal(
231 workspace: &mut Workspace,
232 reveal_target: Option<RevealTarget>,
233 window: &mut Window,
234 cx: &mut Context<Workspace>,
235) -> Task<()> {
236 let task_store = workspace.project().read(cx).task_store().clone();
237 let workspace_handle = workspace.weak_handle();
238 let can_open_modal = workspace
239 .project()
240 .read_with(cx, |project, _| !project.is_via_collab());
241 if can_open_modal {
242 let task_contexts = task_contexts(workspace, window, cx);
243 cx.spawn_in(window, async move |workspace, cx| {
244 let task_contexts = Arc::new(task_contexts.await);
245 workspace
246 .update_in(cx, |workspace, window, cx| {
247 workspace.toggle_modal(window, cx, |window, cx| {
248 TasksModal::new(
249 task_store.clone(),
250 task_contexts,
251 reveal_target.map(|target| TaskOverrides {
252 reveal_target: Some(target),
253 }),
254 true,
255 workspace_handle,
256 window,
257 cx,
258 )
259 })
260 })
261 .ok();
262 })
263 } else {
264 Task::ready(())
265 }
266}
267
268pub fn spawn_tasks_filtered<F>(
269 mut predicate: F,
270 overrides: Option<TaskOverrides>,
271 window: &mut Window,
272 cx: &mut Context<Workspace>,
273) -> Task<anyhow::Result<()>>
274where
275 F: FnMut((&TaskSourceKind, &TaskTemplate)) -> bool + 'static,
276{
277 cx.spawn_in(window, async move |workspace, cx| {
278 let task_contexts = workspace.update_in(cx, |workspace, window, cx| {
279 task_contexts(workspace, window, cx)
280 })?;
281 let task_contexts = task_contexts.await;
282 let mut tasks = workspace
283 .update(cx, |workspace, cx| {
284 let Some(task_inventory) = workspace
285 .project()
286 .read(cx)
287 .task_store()
288 .read(cx)
289 .task_inventory()
290 .cloned()
291 else {
292 return Task::ready(Vec::new());
293 };
294 let (language, buffer) = task_contexts
295 .location()
296 .map(|location| {
297 let buffer = location.buffer.clone();
298 (
299 buffer.read(cx).language_at(location.range.start),
300 Some(buffer),
301 )
302 })
303 .unwrap_or_default();
304 task_inventory
305 .read(cx)
306 .list_tasks(buffer, language, task_contexts.worktree(), cx)
307 })?
308 .await;
309
310 let did_spawn = workspace
311 .update_in(cx, |workspace, window, cx| {
312 let default_context = TaskContext::default();
313 let active_context = task_contexts.active_context().unwrap_or(&default_context);
314
315 tasks.retain_mut(|(task_source_kind, target_task)| {
316 if predicate((task_source_kind, target_task)) {
317 if let Some(overrides) = &overrides
318 && let Some(target_override) = overrides.reveal_target
319 {
320 target_task.reveal_target = target_override;
321 }
322 workspace.schedule_task(
323 task_source_kind.clone(),
324 target_task,
325 active_context,
326 false,
327 window,
328 cx,
329 );
330 true
331 } else {
332 false
333 }
334 });
335
336 if tasks.is_empty() { None } else { Some(()) }
337 })?
338 .is_some();
339 if !did_spawn {
340 workspace
341 .update_in(cx, |workspace, window, cx| {
342 spawn_task_or_modal(
343 workspace,
344 &Spawn::ViaModal {
345 reveal_target: overrides.and_then(|overrides| overrides.reveal_target),
346 },
347 window,
348 cx,
349 );
350 })
351 .ok();
352 }
353
354 Ok(())
355 })
356}
357
358pub fn task_contexts(
359 workspace: &Workspace,
360 window: &mut Window,
361 cx: &mut App,
362) -> Task<TaskContexts> {
363 let active_item = workspace.active_item(cx);
364 let active_worktree = active_item
365 .as_ref()
366 .and_then(|item| item.project_path(cx))
367 .map(|project_path| project_path.worktree_id)
368 .filter(|worktree_id| {
369 workspace
370 .project()
371 .read(cx)
372 .worktree_for_id(*worktree_id, cx)
373 .is_some_and(|worktree| is_visible_directory(&worktree, cx))
374 })
375 .or_else(|| {
376 workspace
377 .visible_worktrees(cx)
378 .next()
379 .map(|tree| tree.read(cx).id())
380 });
381
382 let active_editor = active_item.and_then(|item| item.act_as::<Editor>(cx));
383
384 let editor_context_task = active_editor.as_ref().map(|active_editor| {
385 active_editor.update(cx, |editor, cx| editor.task_context(window, cx))
386 });
387
388 let location = active_editor.as_ref().and_then(|editor| {
389 editor.update(cx, |editor, cx| {
390 let selection = editor.selections.newest_anchor();
391 let multi_buffer = editor.buffer().clone();
392 let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
393 let (buffer_snapshot, buffer_offset) =
394 multi_buffer_snapshot.point_to_buffer_offset(selection.head())?;
395 let buffer_anchor = buffer_snapshot.anchor_before(buffer_offset);
396 let buffer = multi_buffer.read(cx).buffer(buffer_snapshot.remote_id())?;
397 Some(Location {
398 buffer,
399 range: buffer_anchor..buffer_anchor,
400 })
401 })
402 });
403
404 let lsp_task_sources = active_editor
405 .as_ref()
406 .map(|active_editor| {
407 active_editor.update(cx, |editor, cx| editor.lsp_task_sources(false, false, cx))
408 })
409 .unwrap_or_default();
410
411 let latest_selection = active_editor.as_ref().and_then(|active_editor| {
412 let snapshot = active_editor.read(cx).buffer().read(cx).snapshot(cx);
413 snapshot
414 .anchor_to_buffer_anchor(active_editor.read(cx).selections.newest_anchor().head())
415 .map(|(anchor, _)| anchor)
416 });
417
418 let mut worktree_abs_paths = workspace
419 .worktrees(cx)
420 .filter(|worktree| is_visible_directory(worktree, cx))
421 .map(|worktree| {
422 let worktree = worktree.read(cx);
423 (worktree.id(), worktree.abs_path())
424 })
425 .collect::<HashMap<_, _>>();
426
427 cx.background_spawn(async move {
428 let mut task_contexts = TaskContexts::default();
429
430 task_contexts.lsp_task_sources = lsp_task_sources;
431 task_contexts.latest_selection = latest_selection;
432
433 if let Some(editor_context_task) = editor_context_task
434 && let Some(editor_context) = editor_context_task.await
435 {
436 task_contexts.active_item_context = Some((active_worktree, location, editor_context));
437 }
438
439 if let Some(active_worktree) = active_worktree {
440 if let Some(active_worktree_abs_path) = worktree_abs_paths.remove(&active_worktree) {
441 task_contexts.active_worktree_context =
442 Some((active_worktree, worktree_context(&active_worktree_abs_path)));
443 }
444 } else if worktree_abs_paths.len() == 1 {
445 task_contexts.active_worktree_context = worktree_abs_paths
446 .drain()
447 .next()
448 .map(|(id, abs_path)| (id, worktree_context(&abs_path)));
449 }
450
451 task_contexts.other_worktree_contexts.extend(
452 worktree_abs_paths
453 .into_iter()
454 .map(|(id, abs_path)| (id, worktree_context(&abs_path))),
455 );
456 task_contexts
457 })
458}
459
460fn is_visible_directory(worktree: &Entity<Worktree>, cx: &App) -> bool {
461 let worktree = worktree.read(cx);
462 worktree.is_visible() && worktree.root_entry().is_some_and(|entry| entry.is_dir())
463}
464
465fn worktree_context(worktree_abs_path: &Path) -> TaskContext {
466 let mut task_variables = TaskVariables::default();
467 task_variables.insert(
468 VariableName::WorktreeRoot,
469 worktree_abs_path.to_string_lossy().into_owned(),
470 );
471 TaskContext {
472 cwd: Some(worktree_abs_path.to_path_buf()),
473 task_variables,
474 project_env: HashMap::default(),
475 }
476}
477
478#[cfg(test)]
479mod tests {
480 use std::{collections::HashMap, sync::Arc};
481
482 use editor::{Editor, MultiBufferOffset, SelectionEffects};
483 use gpui::TestAppContext;
484 use language::{Language, LanguageConfig};
485 use project::{BasicContextProvider, FakeFs, Project, task_store::TaskStore};
486 use serde_json::json;
487 use task::{TaskContext, TaskVariables, VariableName};
488 use ui::VisualContext;
489 use util::{path, rel_path::rel_path};
490 use workspace::{AppState, MultiWorkspace};
491
492 use crate::task_contexts;
493
494 #[gpui::test]
495 async fn test_default_language_context(cx: &mut TestAppContext) {
496 init_test(cx);
497 let fs = FakeFs::new(cx.executor());
498 fs.insert_tree(
499 path!("/dir"),
500 json!({
501 ".zed": {
502 "tasks.json": r#"[
503 {
504 "label": "example task",
505 "command": "echo",
506 "args": ["4"]
507 },
508 {
509 "label": "another one",
510 "command": "echo",
511 "args": ["55"]
512 },
513 ]"#,
514 },
515 "a.ts": "function this_is_a_test() { }",
516 "rust": {
517 "b.rs": "use std; fn this_is_a_rust_file() { }",
518 }
519
520 }),
521 )
522 .await;
523 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
524 let (worktree_store, git_store) = project.read_with(cx, |project, _| {
525 (project.worktree_store(), project.git_store().clone())
526 });
527 let rust_language = Arc::new(
528 Language::new(
529 LanguageConfig {
530 name: "Rust".into(),
531 ..Default::default()
532 },
533 Some(tree_sitter_rust::LANGUAGE.into()),
534 )
535 .with_outline_query(
536 r#"(function_item
537 "fn" @context
538 name: (_) @name) @item"#,
539 )
540 .unwrap()
541 .with_context_provider(Some(Arc::new(BasicContextProvider::new(
542 worktree_store.clone(),
543 git_store.clone(),
544 )))),
545 );
546
547 let typescript_language = Arc::new(
548 Language::new(
549 LanguageConfig {
550 name: "TypeScript".into(),
551 ..Default::default()
552 },
553 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
554 )
555 .with_outline_query(
556 r#"(function_declaration
557 "async"? @context
558 "function" @context
559 name: (_) @name
560 parameters: (formal_parameters
561 "(" @context
562 ")" @context)) @item"#,
563 )
564 .unwrap()
565 .with_context_provider(Some(Arc::new(BasicContextProvider::new(
566 worktree_store.clone(),
567 git_store.clone(),
568 )))),
569 );
570
571 let worktree_id = project.update(cx, |project, cx| {
572 project.worktrees(cx).next().unwrap().read(cx).id()
573 });
574 let (multi_workspace, cx) =
575 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
576 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
577
578 let buffer1 = workspace
579 .update(cx, |this, cx| {
580 this.project().update(cx, |this, cx| {
581 this.open_buffer((worktree_id, rel_path("a.ts")), cx)
582 })
583 })
584 .await
585 .unwrap();
586 buffer1.update(cx, |this, cx| {
587 this.set_language(Some(typescript_language), cx)
588 });
589 let editor1 = cx.new_window_entity(|window, cx| {
590 Editor::for_buffer(buffer1, Some(project.clone()), window, cx)
591 });
592 let buffer2 = workspace
593 .update(cx, |this, cx| {
594 this.project().update(cx, |this, cx| {
595 this.open_buffer((worktree_id, rel_path("rust/b.rs")), cx)
596 })
597 })
598 .await
599 .unwrap();
600 buffer2.update(cx, |this, cx| this.set_language(Some(rust_language), cx));
601 let editor2 = cx
602 .new_window_entity(|window, cx| Editor::for_buffer(buffer2, Some(project), window, cx));
603
604 let first_context = workspace
605 .update_in(cx, |workspace, window, cx| {
606 workspace.add_item_to_center(Box::new(editor1.clone()), window, cx);
607 workspace.add_item_to_center(Box::new(editor2.clone()), window, cx);
608 assert_eq!(
609 workspace.active_item(cx).unwrap().item_id(),
610 editor2.entity_id()
611 );
612 task_contexts(workspace, window, cx)
613 })
614 .await;
615
616 assert_eq!(
617 first_context
618 .active_context()
619 .expect("Should have an active context"),
620 &TaskContext {
621 cwd: Some(path!("/dir").into()),
622 task_variables: TaskVariables::from_iter([
623 (VariableName::File, path!("/dir/rust/b.rs").into()),
624 (VariableName::Filename, "b.rs".into()),
625 (VariableName::RelativeFile, path!("rust/b.rs").into()),
626 (VariableName::RelativeDir, "rust".into()),
627 (VariableName::Dirname, path!("/dir/rust").into()),
628 (VariableName::Stem, "b".into()),
629 (VariableName::WorktreeRoot, path!("/dir").into()),
630 (VariableName::Row, "1".into()),
631 (VariableName::Column, "1".into()),
632 (VariableName::Language, "Rust".into()),
633 ]),
634 project_env: HashMap::default(),
635 }
636 );
637
638 // And now, let's select an identifier.
639 editor2.update_in(cx, |editor, window, cx| {
640 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
641 selections.select_ranges([MultiBufferOffset(14)..MultiBufferOffset(18)])
642 })
643 });
644
645 assert_eq!(
646 workspace
647 .update_in(cx, |workspace, window, cx| {
648 task_contexts(workspace, window, cx)
649 })
650 .await
651 .active_context()
652 .expect("Should have an active context"),
653 &TaskContext {
654 cwd: Some(path!("/dir").into()),
655 task_variables: TaskVariables::from_iter([
656 (VariableName::File, path!("/dir/rust/b.rs").into()),
657 (VariableName::Filename, "b.rs".into()),
658 (VariableName::RelativeFile, path!("rust/b.rs").into()),
659 (VariableName::RelativeDir, "rust".into()),
660 (VariableName::Dirname, path!("/dir/rust").into()),
661 (VariableName::Stem, "b".into()),
662 (VariableName::WorktreeRoot, path!("/dir").into()),
663 (VariableName::Row, "1".into()),
664 (VariableName::Column, "15".into()),
665 (VariableName::SelectedText, "is_i".into()),
666 (VariableName::Symbol, "this_is_a_rust_file".into()),
667 (VariableName::Language, "Rust".into()),
668 ]),
669 project_env: HashMap::default(),
670 }
671 );
672
673 assert_eq!(
674 workspace
675 .update_in(cx, |workspace, window, cx| {
676 // Now, let's switch the active item to .ts file.
677 workspace.activate_item(&editor1, true, true, window, cx);
678 task_contexts(workspace, window, cx)
679 })
680 .await
681 .active_context()
682 .expect("Should have an active context"),
683 &TaskContext {
684 cwd: Some(path!("/dir").into()),
685 task_variables: TaskVariables::from_iter([
686 (VariableName::File, path!("/dir/a.ts").into()),
687 (VariableName::Filename, "a.ts".into()),
688 (VariableName::RelativeFile, "a.ts".into()),
689 (VariableName::RelativeDir, ".".into()),
690 (VariableName::Dirname, path!("/dir").into()),
691 (VariableName::Stem, "a".into()),
692 (VariableName::WorktreeRoot, path!("/dir").into()),
693 (VariableName::Row, "1".into()),
694 (VariableName::Column, "1".into()),
695 (VariableName::Symbol, "this_is_a_test".into()),
696 (VariableName::Language, "TypeScript".into()),
697 ]),
698 project_env: HashMap::default(),
699 }
700 );
701 }
702
703 pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
704 cx.update(|cx| {
705 let state = AppState::test(cx);
706 crate::init(cx);
707 editor::init(cx);
708 TaskStore::init(None);
709 state
710 })
711 }
712}
713