Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:47:02.285Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

task_inventory.rs

1263 lines · 49.1 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.