Skip to repository content1263 lines · 49.1 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:47:02.285Z 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
task_inventory.rs
1//! Project-wide storage of the tasks available, capable of updating itself from the sources set.
2
3use std::{
4 borrow::Cow,
5 cmp::{self, Reverse},
6 collections::hash_map,
7 path::PathBuf,
8 sync::Arc,
9};
10
11use anyhow::Result;
12use collections::{HashMap, HashSet, VecDeque};
13use dap::DapRegistry;
14use gpui::{App, AppContext as _, Context, Entity, SharedString, Task, WeakEntity};
15use itertools::Itertools;
16use language::{
17 Buffer, ContextLocation, ContextProvider, File, Language, LanguageToolchainStore, Location,
18 language_settings::LanguageSettings,
19};
20use lsp::{LanguageServerId, LanguageServerName};
21use paths::{debug_task_file_name, task_file_name};
22use settings::{InvalidSettingsError, parse_json_with_comments};
23use task::{
24 DebugScenario, ResolvedTask, SharedTaskContext, TaskContext, TaskHook, TaskId, TaskTemplate,
25 TaskTemplates, TaskVariables, VariableName,
26};
27use text::{BufferId, Point, ToPoint};
28use util::{NumericPrefixWithSuffix, ResultExt as _, post_inc, rel_path::RelPath};
29use worktree::WorktreeId;
30
31use crate::{git_store::GitStore, task_store::TaskSettingsLocation, worktree_store::WorktreeStore};
32
33pub const GIT_COMMAND_TASK_TAG: &str = "git-command";
34
35#[derive(Clone, Debug, Default)]
36pub struct DebugScenarioContext {
37 pub task_context: SharedTaskContext,
38 pub worktree_id: Option<WorktreeId>,
39 pub active_buffer: Option<WeakEntity<Buffer>>,
40}
41
42/// Inventory tracks available tasks for a given project.
43pub struct Inventory {
44 last_scheduled_tasks: VecDeque<(TaskSourceKind, ResolvedTask)>,
45 last_scheduled_scenarios: VecDeque<(DebugScenario, DebugScenarioContext)>,
46 templates_from_settings: InventoryFor<TaskTemplate>,
47 scenarios_from_settings: InventoryFor<DebugScenario>,
48}
49
50impl std::fmt::Debug for Inventory {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 f.debug_struct("Inventory")
53 .field("last_scheduled_tasks", &self.last_scheduled_tasks)
54 .field("last_scheduled_scenarios", &self.last_scheduled_scenarios)
55 .field("templates_from_settings", &self.templates_from_settings)
56 .field("scenarios_from_settings", &self.scenarios_from_settings)
57 .finish()
58 }
59}
60
61// Helper trait for better error messages in [InventoryFor]
62trait InventoryContents: Clone {
63 const GLOBAL_SOURCE_FILE: &'static str;
64 const LABEL: &'static str;
65}
66
67impl InventoryContents for TaskTemplate {
68 const GLOBAL_SOURCE_FILE: &'static str = "tasks.json";
69 const LABEL: &'static str = "tasks";
70}
71
72impl InventoryContents for DebugScenario {
73 const GLOBAL_SOURCE_FILE: &'static str = "debug.json";
74
75 const LABEL: &'static str = "debug scenarios";
76}
77
78#[derive(Debug)]
79struct InventoryFor<T> {
80 global: HashMap<PathBuf, Vec<T>>,
81 worktree: HashMap<WorktreeId, HashMap<Arc<RelPath>, Vec<T>>>,
82}
83
84impl<T: InventoryContents> InventoryFor<T> {
85 fn worktree_scenarios(
86 &self,
87 worktree: WorktreeId,
88 ) -> impl '_ + Iterator<Item = (TaskSourceKind, T)> {
89 let worktree_dirs = self.worktree.get(&worktree);
90 let has_zed_dir = worktree_dirs
91 .map(|dirs| {
92 dirs.keys()
93 .any(|dir| dir.file_name().is_some_and(|name| name == ".zed"))
94 })
95 .unwrap_or(false);
96
97 worktree_dirs
98 .into_iter()
99 .flatten()
100 .filter(move |(directory, _)| {
101 !(has_zed_dir && directory.file_name().is_some_and(|name| name == ".vscode"))
102 })
103 .flat_map(|(directory, templates)| {
104 templates.iter().map(move |template| (directory, template))
105 })
106 .map(move |(directory, template)| {
107 (
108 TaskSourceKind::Worktree {
109 id: worktree,
110 directory_in_worktree: directory.clone(),
111 id_base: Cow::Owned(format!(
112 "local worktree {} from directory {directory:?}",
113 T::LABEL
114 )),
115 },
116 template.clone(),
117 )
118 })
119 }
120
121 fn global_scenarios(&self) -> impl '_ + Iterator<Item = (TaskSourceKind, T)> {
122 self.global.iter().flat_map(|(file_path, templates)| {
123 templates.iter().map(|template| {
124 (
125 TaskSourceKind::AbsPath {
126 id_base: Cow::Owned(format!("global {}", T::GLOBAL_SOURCE_FILE)),
127 abs_path: file_path.clone(),
128 },
129 template.clone(),
130 )
131 })
132 })
133 }
134}
135
136impl<T> Default for InventoryFor<T> {
137 fn default() -> Self {
138 Self {
139 global: HashMap::default(),
140 worktree: HashMap::default(),
141 }
142 }
143}
144
145/// Kind of a source the tasks are fetched from, used to display more source information in the UI.
146#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
147pub enum TaskSourceKind {
148 /// bash-like commands spawned by users, not associated with any path
149 UserInput,
150 /// Tasks from the worktree's .zed/task.json
151 Worktree {
152 id: WorktreeId,
153 directory_in_worktree: Arc<RelPath>,
154 id_base: Cow<'static, str>,
155 },
156 /// ~/.config/zed/task.json - like global files with task definitions, applicable to any path
157 AbsPath {
158 id_base: Cow<'static, str>,
159 abs_path: PathBuf,
160 },
161 /// Languages-specific tasks coming from extensions.
162 Language { name: SharedString },
163 /// Language-specific tasks coming from LSP servers.
164 Lsp {
165 language_name: SharedString,
166 server: LanguageServerId,
167 },
168}
169
170/// A collection of task contexts, derived from the current state of the workspace.
171/// Only contains worktrees that are visible and with their root being a directory.
172#[derive(Debug, Default)]
173pub struct TaskContexts {
174 /// A context, related to the currently opened item.
175 /// Item can be opened from an invisible worktree, or any other, not necessarily active worktree.
176 pub active_item_context: Option<(Option<WorktreeId>, Option<Location>, TaskContext)>,
177 /// A worktree that corresponds to the active item, or the only worktree in the workspace.
178 pub active_worktree_context: Option<(WorktreeId, TaskContext)>,
179 /// If there are multiple worktrees in the workspace, all non-active ones are included here.
180 pub other_worktree_contexts: Vec<(WorktreeId, TaskContext)>,
181 pub lsp_task_sources: HashMap<LanguageServerName, Vec<BufferId>>,
182 pub latest_selection: Option<text::Anchor>,
183}
184
185impl TaskContexts {
186 pub fn active_context(&self) -> Option<&TaskContext> {
187 self.active_item_context
188 .as_ref()
189 .map(|(_, _, context)| context)
190 .or_else(|| {
191 self.active_worktree_context
192 .as_ref()
193 .map(|(_, context)| context)
194 })
195 }
196
197 pub fn location(&self) -> Option<&Location> {
198 self.active_item_context
199 .as_ref()
200 .and_then(|(_, location, _)| location.as_ref())
201 }
202
203 pub fn file(&self, cx: &App) -> Option<Arc<dyn File>> {
204 self.active_item_context
205 .as_ref()
206 .and_then(|(_, location, _)| location.as_ref())
207 .and_then(|location| location.buffer.read(cx).file().cloned())
208 }
209
210 pub fn worktree(&self) -> Option<WorktreeId> {
211 self.active_item_context
212 .as_ref()
213 .and_then(|(worktree_id, _, _)| worktree_id.as_ref())
214 .or_else(|| {
215 self.active_worktree_context
216 .as_ref()
217 .map(|(worktree_id, _)| worktree_id)
218 })
219 .copied()
220 }
221
222 pub fn task_context_for_worktree_id(&self, worktree_id: WorktreeId) -> Option<&TaskContext> {
223 self.active_worktree_context
224 .iter()
225 .chain(self.other_worktree_contexts.iter())
226 .find(|(id, _)| *id == worktree_id)
227 .map(|(_, context)| context)
228 }
229
230 /// Re-resolves a previously resolved task against the current contexts, so that
231 /// any context-dependent variables (e.g. the active file or selection) reflect the
232 /// editor's current state rather than the state from when the task was first run.
233 ///
234 /// For worktree tasks the search is scoped to the task's own worktree; other tasks
235 /// fall back through the active item, active worktree, and finally an empty context.
236 /// If none of the contexts resolve, the original `resolved_task` is returned unchanged.
237 fn reresolve_task(&self, kind: &TaskSourceKind, resolved_task: &ResolvedTask) -> ResolvedTask {
238 let id_base = kind.to_id_base();
239 let task = resolved_task.original_task();
240
241 let candidate_contexts: Vec<Option<&TaskContext>> = match kind {
242 TaskSourceKind::Worktree { id, .. } => vec![
243 self.active_item_context
244 .as_ref()
245 .filter(|(worktree_id, _, _)| worktree_id.as_ref() == Some(id))
246 .map(|(_, _, context)| context),
247 self.active_worktree_context
248 .as_ref()
249 .filter(|(worktree_id, _)| worktree_id == id)
250 .map(|(_, context)| context),
251 self.other_worktree_contexts
252 .iter()
253 .find(|(worktree_id, _)| worktree_id == id)
254 .map(|(_, context)| context),
255 ],
256 _ => vec![
257 self.active_item_context
258 .as_ref()
259 .map(|(_, _, context)| context),
260 self.active_worktree_context
261 .as_ref()
262 .map(|(_, context)| context),
263 ],
264 };
265
266 candidate_contexts
267 .into_iter()
268 .flatten()
269 .find_map(|context| task.resolve_task(&id_base, context))
270 .or_else(|| task.resolve_task(&id_base, &TaskContext::default()))
271 .unwrap_or_else(|| resolved_task.clone())
272 }
273}
274
275impl TaskSourceKind {
276 pub fn to_id_base(&self) -> String {
277 match self {
278 Self::UserInput => "oneshot".to_string(),
279 Self::AbsPath { id_base, abs_path } => {
280 format!("{id_base}_{}", abs_path.display())
281 }
282 Self::Worktree {
283 id,
284 id_base,
285 directory_in_worktree,
286 } => {
287 format!("{id_base}_{id}_{}", directory_in_worktree.as_unix_str())
288 }
289 Self::Language { name } => format!("language_{name}"),
290 Self::Lsp {
291 server,
292 language_name,
293 } => format!("lsp_{language_name}_{server}"),
294 }
295 }
296}
297
298impl Inventory {
299 pub fn new(cx: &mut App) -> Entity<Self> {
300 cx.new(|_| Self {
301 last_scheduled_tasks: VecDeque::default(),
302 last_scheduled_scenarios: VecDeque::default(),
303 templates_from_settings: InventoryFor::default(),
304 scenarios_from_settings: InventoryFor::default(),
305 })
306 }
307
308 pub fn scenario_scheduled(
309 &mut self,
310 scenario: DebugScenario,
311 task_context: SharedTaskContext,
312 worktree_id: Option<WorktreeId>,
313 active_buffer: Option<WeakEntity<Buffer>>,
314 ) {
315 self.last_scheduled_scenarios
316 .retain(|(s, _)| s.label != scenario.label);
317 self.last_scheduled_scenarios.push_front((
318 scenario,
319 DebugScenarioContext {
320 task_context,
321 worktree_id,
322 active_buffer,
323 },
324 ));
325 if self.last_scheduled_scenarios.len() > 5_000 {
326 self.last_scheduled_scenarios.pop_front();
327 }
328 }
329
330 pub fn last_scheduled_scenario(&self) -> Option<&(DebugScenario, DebugScenarioContext)> {
331 self.last_scheduled_scenarios.back()
332 }
333
334 pub fn list_debug_scenarios(
335 &self,
336 task_contexts: &TaskContexts,
337 lsp_tasks: Vec<(TaskSourceKind, task::ResolvedTask)>,
338 current_resolved_tasks: Vec<(TaskSourceKind, task::ResolvedTask)>,
339 add_current_language_tasks: bool,
340 cx: &mut App,
341 ) -> Task<(
342 Vec<(DebugScenario, DebugScenarioContext)>,
343 Vec<(TaskSourceKind, DebugScenario)>,
344 )> {
345 let mut scenarios = Vec::new();
346
347 if let Some(worktree_id) = task_contexts
348 .active_worktree_context
349 .iter()
350 .chain(task_contexts.other_worktree_contexts.iter())
351 .map(|context| context.0)
352 .next()
353 {
354 scenarios.extend(self.worktree_scenarios_from_settings(worktree_id));
355 }
356 scenarios.extend(self.global_debug_scenarios_from_settings());
357
358 let last_scheduled_scenarios = self.last_scheduled_scenarios.iter().cloned().collect();
359
360 let adapter = task_contexts.location().and_then(|location| {
361 let buffer = location.buffer.read(cx);
362 let adapter = LanguageSettings::for_buffer(&buffer, cx)
363 .debuggers
364 .first()
365 .map(SharedString::from)
366 .or_else(|| {
367 buffer
368 .language()
369 .and_then(|l| l.config().debuggers.first().map(SharedString::from))
370 });
371 adapter.map(|adapter| (adapter, DapRegistry::global(cx).locators()))
372 });
373 cx.background_spawn(async move {
374 if let Some((adapter, locators)) = adapter {
375 for (kind, task) in
376 lsp_tasks
377 .into_iter()
378 .chain(current_resolved_tasks.into_iter().filter(|(kind, _)| {
379 add_current_language_tasks
380 || !matches!(kind, TaskSourceKind::Language { .. })
381 }))
382 {
383 let adapter = adapter.clone().into();
384
385 for locator in locators.values() {
386 if let Some(scenario) = locator
387 .create_scenario(task.original_task(), task.display_label(), &adapter)
388 .await
389 {
390 scenarios.push((kind, scenario));
391 break;
392 }
393 }
394 }
395 }
396 (last_scheduled_scenarios, scenarios)
397 })
398 }
399
400 pub fn task_template_by_label(
401 &self,
402 buffer: Option<Entity<Buffer>>,
403 worktree_id: Option<WorktreeId>,
404 label: &str,
405 cx: &App,
406 ) -> Task<Option<TaskTemplate>> {
407 let (buffer_worktree_id, language) = buffer
408 .as_ref()
409 .map(|buffer| {
410 let buffer = buffer.read(cx);
411 (
412 buffer.file().as_ref().map(|file| file.worktree_id(cx)),
413 buffer.language().cloned(),
414 )
415 })
416 .unwrap_or((None, None));
417
418 let tasks = self.list_tasks(buffer, language, worktree_id.or(buffer_worktree_id), cx);
419 let label = label.to_owned();
420 cx.background_spawn(async move {
421 tasks
422 .await
423 .into_iter()
424 .find(|(_, template)| template.label == label)
425 .map(|val| val.1)
426 })
427 }
428
429 /// Pulls its task sources relevant to the worktree and the language given,
430 /// returns all task templates with their source kinds, worktree tasks first, language tasks second
431 /// and global tasks last. No specific order inside source kinds groups.
432 pub fn list_tasks(
433 &self,
434 buffer: Option<Entity<Buffer>>,
435 language: Option<Arc<Language>>,
436 worktree: Option<WorktreeId>,
437 cx: &App,
438 ) -> Task<Vec<(TaskSourceKind, TaskTemplate)>> {
439 let global_tasks = self.global_templates_from_settings().collect::<Vec<_>>();
440 let mut worktree_tasks = worktree
441 .into_iter()
442 .flat_map(|worktree| self.worktree_templates_from_settings(worktree))
443 .collect::<Vec<_>>();
444
445 let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
446 name: language.name().into(),
447 });
448 let language_tasks = language
449 .filter(|language| {
450 LanguageSettings::resolve(
451 buffer.as_ref().map(|b| b.read(cx)),
452 Some(&language.name()),
453 cx,
454 )
455 .tasks
456 .enabled
457 })
458 .and_then(|language| {
459 language
460 .context_provider()
461 .map(|provider| provider.associated_tasks(buffer, cx))
462 });
463 cx.background_spawn(async move {
464 if let Some(t) = language_tasks {
465 worktree_tasks.extend(t.await.into_iter().flat_map(|tasks| {
466 tasks
467 .0
468 .into_iter()
469 .filter_map(|task| Some((task_source_kind.clone()?, task)))
470 }));
471 }
472 worktree_tasks.extend(global_tasks);
473 worktree_tasks
474 })
475 }
476
477 /// Pulls its task sources relevant to the worktree and the language given and resolves them with the [`TaskContexts`] given.
478 /// Joins the new resolutions with the resolved tasks that were used (spawned) before,
479 /// orders them so that the most recently used come first, all equally used ones are ordered so that the most specific tasks come first.
480 /// Deduplicates the tasks by their labels and context and splits the ordered list into two: used tasks and the rest, newly resolved tasks.
481 pub fn used_and_current_resolved_tasks(
482 &self,
483 task_contexts: Arc<TaskContexts>,
484 cx: &mut Context<Self>,
485 ) -> Task<(
486 Vec<(TaskSourceKind, ResolvedTask)>,
487 Vec<(TaskSourceKind, ResolvedTask)>,
488 )> {
489 let worktree = task_contexts.worktree();
490 let location = task_contexts.location();
491 let language = location.and_then(|location| location.buffer.read(cx).language());
492 let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
493 name: language.name().into(),
494 });
495 let buffer = location.map(|location| location.buffer.clone());
496
497 let worktrees_with_zed_tasks: HashSet<WorktreeId> = self
498 .templates_from_settings
499 .worktree
500 .iter()
501 .filter(|(_, dirs)| {
502 dirs.keys()
503 .any(|dir| dir.file_name().is_some_and(|name| name == ".zed"))
504 })
505 .map(|(id, _)| *id)
506 .collect();
507
508 let mut task_labels_to_ids = HashMap::<String, HashSet<TaskId>>::default();
509 let mut lru_score = 0_u32;
510 let previously_spawned_tasks = self
511 .last_scheduled_tasks
512 .iter()
513 .rev()
514 .filter(|(task_kind, _)| {
515 if matches!(task_kind, TaskSourceKind::Language { .. }) {
516 Some(task_kind) == task_source_kind.as_ref()
517 } else if let TaskSourceKind::Worktree {
518 id,
519 directory_in_worktree: dir,
520 ..
521 } = task_kind
522 {
523 !(worktrees_with_zed_tasks.contains(id)
524 && dir.file_name().is_some_and(|name| name == ".vscode"))
525 } else {
526 true
527 }
528 })
529 .map(|(task_source_kind, resolved_task)| {
530 let reresolved = task_contexts.reresolve_task(task_source_kind, resolved_task);
531 let task = if reresolved.resolved_label == resolved_task.resolved_label {
532 reresolved
533 } else {
534 resolved_task.clone()
535 };
536 (task_source_kind.clone(), task)
537 })
538 .filter(|(_, resolved_task)| {
539 match task_labels_to_ids.entry(resolved_task.resolved_label.clone()) {
540 hash_map::Entry::Occupied(mut o) => {
541 // Allow distinct re-resolved tasks that share a label but differ by context.
542 o.get_mut().insert(resolved_task.id.clone())
543 }
544 hash_map::Entry::Vacant(v) => {
545 v.insert(HashSet::from_iter(Some(resolved_task.id.clone())));
546 true
547 }
548 }
549 })
550 .map(|(kind, task)| (kind, task, post_inc(&mut lru_score)))
551 .sorted_unstable_by(task_lru_comparator)
552 .map(|(kind, task, _)| (kind, task))
553 .collect::<Vec<_>>();
554
555 let not_used_score = post_inc(&mut lru_score);
556 let global_tasks = self.global_templates_from_settings().collect::<Vec<_>>();
557 let associated_tasks = language
558 .filter(|language| {
559 LanguageSettings::resolve(
560 buffer.as_ref().map(|b| b.read(cx)),
561 Some(&language.name()),
562 cx,
563 )
564 .tasks
565 .enabled
566 })
567 .and_then(|language| {
568 language
569 .context_provider()
570 .map(|provider| provider.associated_tasks(buffer, cx))
571 });
572 let worktree_tasks = worktree
573 .into_iter()
574 .flat_map(|worktree| self.worktree_templates_from_settings(worktree))
575 .collect::<Vec<_>>();
576 let task_contexts = task_contexts.clone();
577 cx.background_spawn(async move {
578 let language_tasks = if let Some(task) = associated_tasks {
579 task.await.map(|templates| {
580 templates
581 .0
582 .into_iter()
583 .flat_map(|task| Some((task_source_kind.clone()?, task)))
584 })
585 } else {
586 None
587 };
588
589 let worktree_tasks = worktree_tasks
590 .into_iter()
591 .chain(language_tasks.into_iter().flatten())
592 .chain(global_tasks);
593
594 let new_resolved_tasks = worktree_tasks
595 .flat_map(|(kind, task)| {
596 let id_base = kind.to_id_base();
597
598 if let TaskSourceKind::Worktree { id, .. } = &kind {
599 None.or_else(|| {
600 let (_, _, item_context) =
601 task_contexts.active_item_context.as_ref().filter(
602 |(worktree_id, _, _)| Some(id) == worktree_id.as_ref(),
603 )?;
604 task.resolve_task(&id_base, item_context)
605 })
606 .or_else(|| {
607 let (_, worktree_context) = task_contexts
608 .active_worktree_context
609 .as_ref()
610 .filter(|(worktree_id, _)| id == worktree_id)?;
611 task.resolve_task(&id_base, worktree_context)
612 })
613 .or_else(|| {
614 if let TaskSourceKind::Worktree { id, .. } = &kind {
615 let worktree_context = task_contexts
616 .other_worktree_contexts
617 .iter()
618 .find(|(worktree_id, _)| worktree_id == id)
619 .map(|(_, context)| context)?;
620 task.resolve_task(&id_base, worktree_context)
621 } else {
622 None
623 }
624 })
625 } else {
626 None.or_else(|| {
627 let (_, _, item_context) =
628 task_contexts.active_item_context.as_ref()?;
629 task.resolve_task(&id_base, item_context)
630 })
631 .or_else(|| {
632 let (_, worktree_context) =
633 task_contexts.active_worktree_context.as_ref()?;
634 task.resolve_task(&id_base, worktree_context)
635 })
636 }
637 .or_else(|| task.resolve_task(&id_base, &TaskContext::default()))
638 .map(move |resolved_task| (kind.clone(), resolved_task, not_used_score))
639 })
640 .filter(|(_, resolved_task, _)| {
641 match task_labels_to_ids.entry(resolved_task.resolved_label.clone()) {
642 hash_map::Entry::Occupied(mut o) => {
643 // Allow new tasks with the same label, if their context is different
644 o.get_mut().insert(resolved_task.id.clone())
645 }
646 hash_map::Entry::Vacant(v) => {
647 v.insert(HashSet::from_iter(Some(resolved_task.id.clone())));
648 true
649 }
650 }
651 })
652 .sorted_unstable_by(task_lru_comparator)
653 .map(|(kind, task, _)| (kind, task))
654 .collect::<Vec<_>>();
655
656 (previously_spawned_tasks, new_resolved_tasks)
657 })
658 }
659
660 /// Returns the last scheduled task by task_id if provided.
661 /// Otherwise, returns the last scheduled task.
662 pub fn last_scheduled_task(
663 &self,
664 task_id: Option<&TaskId>,
665 ) -> Option<(TaskSourceKind, ResolvedTask)> {
666 if let Some(task_id) = task_id {
667 self.last_scheduled_tasks
668 .iter()
669 .find(|(_, task)| &task.id == task_id)
670 .cloned()
671 } else {
672 self.last_scheduled_tasks.back().cloned()
673 }
674 }
675
676 /// Registers task "usage" as being scheduled – to be used for LRU sorting when listing all tasks.
677 pub fn task_scheduled(
678 &mut self,
679 task_source_kind: TaskSourceKind,
680 resolved_task: ResolvedTask,
681 ) {
682 self.last_scheduled_tasks
683 .push_back((task_source_kind, resolved_task));
684 if self.last_scheduled_tasks.len() > 5_000 {
685 self.last_scheduled_tasks.pop_front();
686 }
687 }
688
689 /// Deletes a resolved task from history, using its id.
690 /// A similar may still resurface in `used_and_current_resolved_tasks` when its [`TaskTemplate`] is resolved again.
691 pub fn delete_previously_used(&mut self, id: &TaskId) {
692 self.last_scheduled_tasks.retain(|(_, task)| &task.id != id);
693 }
694
695 /// Returns global task templates with the provided tag.
696 pub fn global_templates_with_tag(&self, tag: &str) -> Vec<(TaskSourceKind, TaskTemplate)> {
697 self.global_templates_from_settings()
698 .filter(|(_, template)| template.tags.iter().any(|template_tag| template_tag == tag))
699 .collect()
700 }
701
702 /// Resolves global task templates with the provided tag against the provided task context.
703 pub fn resolve_global_tasks_with_tag(
704 &self,
705 tag: &str,
706 task_context: &TaskContext,
707 ) -> Vec<(TaskSourceKind, ResolvedTask)> {
708 self.global_templates_with_tag(tag)
709 .into_iter()
710 .filter_map(|(task_source_kind, task_template)| {
711 let id_base = task_source_kind.to_id_base();
712 task_template
713 .resolve_task(&id_base, task_context)
714 .map(|resolved_task| (task_source_kind, resolved_task))
715 })
716 .collect()
717 }
718
719 /// Returns all task templates (worktree and global) that have at least one
720 /// hook in the provided set.
721 pub fn templates_with_hooks(
722 &self,
723 hooks: &HashSet<TaskHook>,
724 worktree: WorktreeId,
725 ) -> Vec<(TaskSourceKind, TaskTemplate)> {
726 self.worktree_templates_from_settings(worktree)
727 .chain(self.global_templates_from_settings())
728 .filter(|(_, template)| !template.hooks.is_disjoint(hooks))
729 .collect()
730 }
731
732 fn global_templates_from_settings(
733 &self,
734 ) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
735 self.templates_from_settings.global_scenarios()
736 }
737
738 fn global_debug_scenarios_from_settings(
739 &self,
740 ) -> impl '_ + Iterator<Item = (TaskSourceKind, DebugScenario)> {
741 self.scenarios_from_settings.global_scenarios()
742 }
743
744 fn worktree_scenarios_from_settings(
745 &self,
746 worktree: WorktreeId,
747 ) -> impl '_ + Iterator<Item = (TaskSourceKind, DebugScenario)> {
748 self.scenarios_from_settings.worktree_scenarios(worktree)
749 }
750
751 fn worktree_templates_from_settings(
752 &self,
753 worktree: WorktreeId,
754 ) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
755 self.templates_from_settings.worktree_scenarios(worktree)
756 }
757
758 /// Updates in-memory task metadata from the JSON string given.
759 /// Will fail if the JSON is not a valid array of objects, but will continue if any object will not parse into a [`TaskTemplate`].
760 ///
761 /// Global tasks are updated for no worktree provided, otherwise the worktree metadata for a given path will be updated.
762 pub fn update_file_based_tasks(
763 &mut self,
764 location: TaskSettingsLocation<'_>,
765 raw_tasks_json: Option<&str>,
766 ) -> Result<(), InvalidSettingsError> {
767 let raw_tasks = match parse_json_with_comments::<Vec<serde_json::Value>>(
768 raw_tasks_json.unwrap_or("[]"),
769 ) {
770 Ok(tasks) => tasks,
771 Err(e) => {
772 return Err(InvalidSettingsError::Tasks {
773 path: match location {
774 TaskSettingsLocation::Global(path) => path.to_owned(),
775 TaskSettingsLocation::Worktree(settings_location) => {
776 settings_location.path.as_std_path().join(task_file_name())
777 }
778 },
779 message: format!("Failed to parse tasks file content as a JSON array: {e}"),
780 });
781 }
782 };
783
784 let mut validation_errors = Vec::new();
785 let new_templates = raw_tasks.into_iter().filter_map(|raw_template| {
786 let template = serde_json::from_value::<TaskTemplate>(raw_template).log_err()?;
787
788 // Validate the variable names used in the `TaskTemplate`.
789 let unknown_variables = template.unknown_variables();
790 if !unknown_variables.is_empty() {
791 let variables_list = unknown_variables
792 .iter()
793 .map(|variable| format!("${variable}"))
794 .collect::<Vec<_>>()
795 .join(", ");
796
797 validation_errors.push(format!(
798 "Task '{}' uses unknown variables: {}",
799 template.label, variables_list
800 ));
801
802 // Skip this template, since it uses unknown variable names, but
803 // continue processing others.
804 return None;
805 }
806
807 Some(template)
808 });
809
810 let parsed_templates = &mut self.templates_from_settings;
811 match location {
812 TaskSettingsLocation::Global(path) => {
813 parsed_templates
814 .global
815 .entry(path.to_owned())
816 .insert_entry(new_templates.collect());
817 self.last_scheduled_tasks.retain(|(kind, _)| {
818 if let TaskSourceKind::AbsPath { abs_path, .. } = kind {
819 abs_path != path
820 } else {
821 true
822 }
823 });
824 }
825 TaskSettingsLocation::Worktree(location) => {
826 let new_templates = new_templates.collect::<Vec<_>>();
827 if new_templates.is_empty() {
828 if let Some(worktree_tasks) =
829 parsed_templates.worktree.get_mut(&location.worktree_id)
830 {
831 worktree_tasks.remove(location.path);
832 }
833 } else {
834 parsed_templates
835 .worktree
836 .entry(location.worktree_id)
837 .or_default()
838 .insert(Arc::from(location.path), new_templates);
839 }
840 self.last_scheduled_tasks.retain(|(kind, _)| {
841 if let TaskSourceKind::Worktree {
842 directory_in_worktree,
843 id,
844 ..
845 } = kind
846 {
847 *id != location.worktree_id
848 || directory_in_worktree.as_ref() != location.path
849 } else {
850 true
851 }
852 });
853 }
854 }
855
856 if !validation_errors.is_empty() {
857 return Err(InvalidSettingsError::Tasks {
858 path: match &location {
859 TaskSettingsLocation::Global(path) => path.to_path_buf(),
860 TaskSettingsLocation::Worktree(location) => {
861 location.path.as_std_path().join(task_file_name())
862 }
863 },
864 message: validation_errors.join("\n"),
865 });
866 }
867
868 Ok(())
869 }
870
871 /// Updates in-memory task metadata from the JSON string given.
872 /// Will fail if the JSON is not a valid array of objects, but will continue if any object will not parse into a [`TaskTemplate`].
873 ///
874 /// Global tasks are updated for no worktree provided, otherwise the worktree metadata for a given path will be updated.
875 pub fn update_file_based_scenarios(
876 &mut self,
877 location: TaskSettingsLocation<'_>,
878 raw_tasks_json: Option<&str>,
879 ) -> Result<(), InvalidSettingsError> {
880 let raw_tasks = match parse_json_with_comments::<Vec<serde_json::Value>>(
881 raw_tasks_json.unwrap_or("[]"),
882 ) {
883 Ok(tasks) => tasks,
884 Err(e) => {
885 return Err(InvalidSettingsError::Debug {
886 path: match location {
887 TaskSettingsLocation::Global(path) => path.to_owned(),
888 TaskSettingsLocation::Worktree(settings_location) => settings_location
889 .path
890 .as_std_path()
891 .join(debug_task_file_name()),
892 },
893 message: format!("Failed to parse tasks file content as a JSON array: {e}"),
894 });
895 }
896 };
897
898 let new_templates = raw_tasks
899 .into_iter()
900 .filter_map(|raw_template| {
901 serde_json::from_value::<DebugScenario>(raw_template).log_err()
902 })
903 .collect::<Vec<_>>();
904
905 let parsed_scenarios = &mut self.scenarios_from_settings;
906 let mut new_definitions: HashMap<_, _> = new_templates
907 .iter()
908 .map(|template| (template.label.clone(), template.clone()))
909 .collect();
910 let previously_existing_scenarios;
911
912 match location {
913 TaskSettingsLocation::Global(path) => {
914 previously_existing_scenarios = parsed_scenarios
915 .global_scenarios()
916 .map(|(_, scenario)| scenario.label)
917 .collect::<HashSet<_>>();
918 parsed_scenarios
919 .global
920 .entry(path.to_owned())
921 .insert_entry(new_templates);
922 }
923 TaskSettingsLocation::Worktree(location) => {
924 previously_existing_scenarios = parsed_scenarios
925 .worktree_scenarios(location.worktree_id)
926 .map(|(_, scenario)| scenario.label)
927 .collect::<HashSet<_>>();
928
929 if new_templates.is_empty() {
930 if let Some(worktree_tasks) =
931 parsed_scenarios.worktree.get_mut(&location.worktree_id)
932 {
933 worktree_tasks.remove(location.path);
934 }
935 } else {
936 parsed_scenarios
937 .worktree
938 .entry(location.worktree_id)
939 .or_default()
940 .insert(Arc::from(location.path), new_templates);
941 }
942 }
943 }
944 self.last_scheduled_scenarios.retain_mut(|(scenario, _)| {
945 if !previously_existing_scenarios.contains(&scenario.label) {
946 return true;
947 }
948 if let Some(new_definition) = new_definitions.remove(&scenario.label) {
949 *scenario = new_definition;
950 true
951 } else {
952 false
953 }
954 });
955
956 Ok(())
957 }
958}
959
960fn task_lru_comparator(
961 (kind_a, task_a, lru_score_a): &(TaskSourceKind, ResolvedTask, u32),
962 (kind_b, task_b, lru_score_b): &(TaskSourceKind, ResolvedTask, u32),
963) -> cmp::Ordering {
964 lru_score_a
965 // First, display recently used templates above all.
966 .cmp(lru_score_b)
967 // Then, ensure more specific sources are displayed first.
968 .then(task_source_kind_preference(kind_a).cmp(&task_source_kind_preference(kind_b)))
969 // After that, display first more specific tasks, using more template variables.
970 // Bonus points for tasks with symbol variables.
971 .then(task_variables_preference(task_a).cmp(&task_variables_preference(task_b)))
972 // Finally, sort by the resolved label, but a bit more specifically, to avoid mixing letters and digits.
973 .then({
974 NumericPrefixWithSuffix::from_numeric_prefixed_str(&task_a.resolved_label)
975 .cmp(&NumericPrefixWithSuffix::from_numeric_prefixed_str(
976 &task_b.resolved_label,
977 ))
978 .then(task_a.resolved_label.cmp(&task_b.resolved_label))
979 .then(kind_a.cmp(kind_b))
980 })
981}
982
983pub fn task_source_kind_preference(kind: &TaskSourceKind) -> u32 {
984 match kind {
985 TaskSourceKind::Lsp { .. } => 0,
986 TaskSourceKind::Language { .. } => 1,
987 TaskSourceKind::UserInput => 2,
988 TaskSourceKind::Worktree { .. } => 3,
989 TaskSourceKind::AbsPath { .. } => 4,
990 }
991}
992
993fn task_variables_preference(task: &ResolvedTask) -> Reverse<usize> {
994 let task_variables = task.substituted_variables();
995 Reverse(if task_variables.contains(&VariableName::Symbol) {
996 task_variables.len() + 1
997 } else {
998 task_variables.len()
999 })
1000}
1001
1002/// A context provided that tries to provide values for all non-custom [`VariableName`] variants for a currently opened file.
1003/// Applied as a base for every custom [`ContextProvider`] unless explicitly oped out.
1004pub struct BasicContextProvider {
1005 worktree_store: Entity<WorktreeStore>,
1006 git_store: Entity<GitStore>,
1007}
1008
1009impl BasicContextProvider {
1010 pub fn new(worktree_store: Entity<WorktreeStore>, git_store: Entity<GitStore>) -> Self {
1011 Self {
1012 worktree_store,
1013 git_store,
1014 }
1015 }
1016}
1017
1018impl ContextProvider for BasicContextProvider {
1019 fn build_context(
1020 &self,
1021 _: &TaskVariables,
1022 location: ContextLocation<'_>,
1023 _: Option<HashMap<String, String>>,
1024 _: Arc<dyn LanguageToolchainStore>,
1025 cx: &mut App,
1026 ) -> Task<Result<TaskVariables>> {
1027 let location = location.file_location;
1028 let buffer = location.buffer.read(cx);
1029 let buffer_snapshot = buffer.snapshot();
1030 let symbols = buffer_snapshot.symbols_containing(location.range.start, None);
1031 let symbol = symbols.last().map(|symbol| {
1032 let range = symbol
1033 .name_ranges
1034 .last()
1035 .cloned()
1036 .unwrap_or(0..symbol.text.len());
1037 symbol.text[range].to_string()
1038 });
1039
1040 let current_file = buffer.file().and_then(|file| file.as_local());
1041 let Point { row, column } = location.range.start.to_point(&buffer_snapshot);
1042 let row = row + 1;
1043 let column = column + 1;
1044 let selected_text = buffer
1045 .chars_for_range(location.range.clone())
1046 .collect::<String>();
1047
1048 let mut task_variables = TaskVariables::from_iter([
1049 (VariableName::Row, row.to_string()),
1050 (VariableName::Column, column.to_string()),
1051 ]);
1052
1053 if let Some(symbol) = symbol {
1054 task_variables.insert(VariableName::Symbol, symbol);
1055 }
1056 if !selected_text.trim().is_empty() {
1057 task_variables.insert(VariableName::SelectedText, selected_text);
1058 }
1059 let worktree = buffer
1060 .file()
1061 .map(|file| file.worktree_id(cx))
1062 .and_then(|worktree_id| {
1063 self.worktree_store
1064 .read(cx)
1065 .worktree_for_id(worktree_id, cx)
1066 });
1067
1068 if let Some(worktree) = worktree {
1069 let worktree = worktree.read(cx);
1070 let path_style = worktree.path_style();
1071 task_variables.insert(
1072 VariableName::WorktreeRoot,
1073 worktree.abs_path().to_string_lossy().into_owned(),
1074 );
1075 if let Some(current_file) = current_file.as_ref() {
1076 let relative_path = current_file.path();
1077 task_variables.insert(
1078 VariableName::RelativeFile,
1079 relative_path.display(path_style).to_string(),
1080 );
1081 if let Some(relative_dir) = relative_path.parent() {
1082 task_variables.insert(
1083 VariableName::RelativeDir,
1084 if relative_dir.is_empty() {
1085 String::from(".")
1086 } else {
1087 relative_dir.display(path_style).to_string()
1088 },
1089 );
1090 }
1091 }
1092 }
1093
1094 if let Some(worktree_id) = location.buffer.read(cx).file().map(|f| f.worktree_id(cx)) {
1095 if let Some(path) = self
1096 .git_store
1097 .read(cx)
1098 .original_repo_path_for_worktree(worktree_id, cx)
1099 {
1100 task_variables.insert(
1101 VariableName::MainGitWorktree,
1102 path.to_string_lossy().into_owned(),
1103 );
1104 }
1105 }
1106
1107 if let Some(current_file) = current_file {
1108 let path = current_file.abs_path(cx);
1109 if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
1110 task_variables.insert(VariableName::Filename, String::from(filename));
1111 }
1112
1113 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
1114 task_variables.insert(VariableName::Stem, stem.into());
1115 }
1116
1117 if let Some(dirname) = path.parent().and_then(|s| s.to_str()) {
1118 task_variables.insert(VariableName::Dirname, dirname.into());
1119 }
1120
1121 task_variables.insert(VariableName::File, path.to_string_lossy().into_owned());
1122 }
1123
1124 if let Some(language) = buffer.language() {
1125 task_variables.insert(VariableName::Language, language.name().to_string());
1126 }
1127
1128 Task::ready(Ok(task_variables))
1129 }
1130}
1131
1132/// A ContextProvider that doesn't provide any task variables on it's own, though it has some associated tasks.
1133pub struct ContextProviderWithTasks {
1134 templates: TaskTemplates,
1135}
1136
1137impl ContextProviderWithTasks {
1138 pub fn new(definitions: TaskTemplates) -> Self {
1139 Self {
1140 templates: definitions,
1141 }
1142 }
1143}
1144
1145impl ContextProvider for ContextProviderWithTasks {
1146 fn associated_tasks(&self, _: Option<Entity<Buffer>>, _: &App) -> Task<Option<TaskTemplates>> {
1147 Task::ready(Some(self.templates.clone()))
1148 }
1149}
1150
1151#[cfg(test)]
1152mod tests {
1153 use super::*;
1154
1155 fn context_with_greeting(value: &str) -> TaskContext {
1156 TaskContext {
1157 task_variables: TaskVariables::from_iter([(
1158 VariableName::Custom(Cow::Owned("GREETING".to_string())),
1159 value.to_string(),
1160 )]),
1161 ..TaskContext::default()
1162 }
1163 }
1164
1165 fn greeting_template() -> TaskTemplate {
1166 TaskTemplate {
1167 label: "echo $ZED_CUSTOM_GREETING".to_string(),
1168 command: "echo".to_string(),
1169 args: vec!["$ZED_CUSTOM_GREETING".to_string()],
1170 ..TaskTemplate::default()
1171 }
1172 }
1173
1174 #[test]
1175 fn reresolve_task_uses_current_active_item_context() {
1176 let template = greeting_template();
1177 let stale = template
1178 .resolve_task("oneshot", &context_with_greeting("stale"))
1179 .expect("template should resolve against the stale context");
1180 assert_eq!(stale.resolved_label, "echo stale");
1181
1182 let task_contexts = TaskContexts {
1183 active_item_context: Some((None, None, context_with_greeting("fresh"))),
1184 ..TaskContexts::default()
1185 };
1186
1187 let reresolved = task_contexts.reresolve_task(&TaskSourceKind::UserInput, &stale);
1188 assert_eq!(
1189 reresolved.resolved_label, "echo fresh",
1190 "re-resolution should pick up the current active item context"
1191 );
1192 }
1193
1194 #[test]
1195 fn reresolve_task_scopes_worktree_tasks_to_their_worktree() {
1196 let template = greeting_template();
1197 let stale = template
1198 .resolve_task("worktree", &context_with_greeting("stale"))
1199 .expect("template should resolve against the stale context");
1200
1201 let task_worktree = WorktreeId::from_usize(1);
1202 let other_worktree = WorktreeId::from_usize(2);
1203 let kind = TaskSourceKind::Worktree {
1204 id: task_worktree,
1205 directory_in_worktree: RelPath::empty_arc(),
1206 id_base: Cow::Borrowed("worktree"),
1207 };
1208
1209 // The active item belongs to a different worktree, so it must not be used; the
1210 // task's own worktree context should win instead.
1211 let task_contexts = TaskContexts {
1212 active_item_context: Some((Some(other_worktree), None, context_with_greeting("other"))),
1213 active_worktree_context: Some((task_worktree, context_with_greeting("fresh"))),
1214 ..TaskContexts::default()
1215 };
1216
1217 let reresolved = task_contexts.reresolve_task(&kind, &stale);
1218 assert_eq!(
1219 reresolved.resolved_label, "echo fresh",
1220 "worktree tasks should only re-resolve against their own worktree's context"
1221 );
1222 }
1223
1224 #[test]
1225 fn reresolve_task_prefers_active_item_over_worktree_context() {
1226 let template = greeting_template();
1227 let stale = template
1228 .resolve_task("oneshot", &context_with_greeting("stale"))
1229 .expect("template should resolve against the stale context");
1230
1231 // Both contexts can resolve the task; the active item context is more specific
1232 // and must take precedence over the active worktree context.
1233 let task_contexts = TaskContexts {
1234 active_item_context: Some((None, None, context_with_greeting("item"))),
1235 active_worktree_context: Some((
1236 WorktreeId::from_usize(1),
1237 context_with_greeting("worktree"),
1238 )),
1239 ..TaskContexts::default()
1240 };
1241
1242 let reresolved = task_contexts.reresolve_task(&TaskSourceKind::UserInput, &stale);
1243 assert_eq!(
1244 reresolved.resolved_label, "echo item",
1245 "the active item context should take precedence over the worktree context"
1246 );
1247 }
1248
1249 #[test]
1250 fn reresolve_task_falls_back_to_original_when_no_context_resolves() {
1251 let template = greeting_template();
1252 let stale = template
1253 .resolve_task("oneshot", &context_with_greeting("stale"))
1254 .expect("template should resolve against the stale context");
1255
1256 // No contexts provide the variable, and the empty default context cannot resolve
1257 // a template that requires it, so the original resolved task is returned unchanged.
1258 let task_contexts = TaskContexts::default();
1259 let reresolved = task_contexts.reresolve_task(&TaskSourceKind::UserInput, &stale);
1260 assert_eq!(reresolved, stale);
1261 }
1262}
1263