task_inventory.rs

  1//! Project-wide storage of the tasks available, capable of updating itself from the sources set.
  2
  3use std::{
  4    cmp::{self, Reverse},
  5    path::{Path, PathBuf},
  6    sync::Arc,
  7};
  8
  9use anyhow::Result;
 10use collections::{btree_map, BTreeMap, VecDeque};
 11use futures::{
 12    channel::mpsc::{unbounded, UnboundedSender},
 13    StreamExt,
 14};
 15use gpui::{AppContext, Context, Model, ModelContext, Task};
 16use itertools::Itertools;
 17use language::{ContextProvider, Language, Location};
 18use task::{
 19    static_source::StaticSource, ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskTemplates,
 20    TaskVariables, VariableName,
 21};
 22use text::{Point, ToPoint};
 23use util::{post_inc, NumericPrefixWithSuffix};
 24use worktree::WorktreeId;
 25
 26use crate::Project;
 27
 28/// Inventory tracks available tasks for a given project.
 29pub struct Inventory {
 30    sources: Vec<SourceInInventory>,
 31    last_scheduled_tasks: VecDeque<(TaskSourceKind, ResolvedTask)>,
 32    update_sender: UnboundedSender<()>,
 33    _update_pooler: Task<anyhow::Result<()>>,
 34}
 35
 36struct SourceInInventory {
 37    source: StaticSource,
 38    kind: TaskSourceKind,
 39}
 40
 41/// Kind of a source the tasks are fetched from, used to display more source information in the UI.
 42#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
 43pub enum TaskSourceKind {
 44    /// bash-like commands spawned by users, not associated with any path
 45    UserInput,
 46    /// Tasks from the worktree's .zed/task.json
 47    Worktree {
 48        id: WorktreeId,
 49        abs_path: PathBuf,
 50        id_base: &'static str,
 51    },
 52    /// ~/.config/zed/task.json - like global files with task definitions, applicable to any path
 53    AbsPath {
 54        id_base: &'static str,
 55        abs_path: PathBuf,
 56    },
 57    /// Languages-specific tasks coming from extensions.
 58    Language { name: Arc<str> },
 59}
 60
 61impl TaskSourceKind {
 62    pub fn abs_path(&self) -> Option<&Path> {
 63        match self {
 64            Self::AbsPath { abs_path, .. } | Self::Worktree { abs_path, .. } => Some(abs_path),
 65            Self::UserInput | Self::Language { .. } => None,
 66        }
 67    }
 68
 69    pub fn worktree(&self) -> Option<WorktreeId> {
 70        match self {
 71            Self::Worktree { id, .. } => Some(*id),
 72            _ => None,
 73        }
 74    }
 75
 76    pub fn to_id_base(&self) -> String {
 77        match self {
 78            TaskSourceKind::UserInput => "oneshot".to_string(),
 79            TaskSourceKind::AbsPath { id_base, abs_path } => {
 80                format!("{id_base}_{}", abs_path.display())
 81            }
 82            TaskSourceKind::Worktree {
 83                id,
 84                id_base,
 85                abs_path,
 86            } => {
 87                format!("{id_base}_{id}_{}", abs_path.display())
 88            }
 89            TaskSourceKind::Language { name } => format!("language_{name}"),
 90        }
 91    }
 92}
 93
 94impl Inventory {
 95    pub fn new(cx: &mut AppContext) -> Model<Self> {
 96        cx.new_model(|cx| {
 97            let (update_sender, mut rx) = unbounded();
 98            let _update_pooler = cx.spawn(|this, mut cx| async move {
 99                while let Some(()) = rx.next().await {
100                    this.update(&mut cx, |_, cx| {
101                        cx.notify();
102                    })?;
103                }
104                Ok(())
105            });
106            Self {
107                sources: Vec::new(),
108                last_scheduled_tasks: VecDeque::new(),
109                update_sender,
110                _update_pooler,
111            }
112        })
113    }
114
115    /// If the task with the same path was not added yet,
116    /// registers a new tasks source to fetch for available tasks later.
117    /// Unless a source is removed, ignores future additions for the same path.
118    pub fn add_source(
119        &mut self,
120        kind: TaskSourceKind,
121        create_source: impl FnOnce(UnboundedSender<()>, &mut AppContext) -> StaticSource,
122        cx: &mut ModelContext<Self>,
123    ) {
124        let abs_path = kind.abs_path();
125        if abs_path.is_some() {
126            if let Some(a) = self.sources.iter().find(|s| s.kind.abs_path() == abs_path) {
127                log::debug!("Source for path {abs_path:?} already exists, not adding. Old kind: {OLD_KIND:?}, new kind: {kind:?}", OLD_KIND = a.kind);
128                return;
129            }
130        }
131        let source = create_source(self.update_sender.clone(), cx);
132        let source = SourceInInventory { source, kind };
133        self.sources.push(source);
134        cx.notify();
135    }
136
137    /// If present, removes the local static source entry that has the given path,
138    /// making corresponding task definitions unavailable in the fetch results.
139    ///
140    /// Now, entry for this path can be re-added again.
141    pub fn remove_local_static_source(&mut self, abs_path: &Path) {
142        self.sources.retain(|s| s.kind.abs_path() != Some(abs_path));
143    }
144
145    /// If present, removes the worktree source entry that has the given worktree id,
146    /// making corresponding task definitions unavailable in the fetch results.
147    ///
148    /// Now, entry for this path can be re-added again.
149    pub fn remove_worktree_sources(&mut self, worktree: WorktreeId) {
150        self.sources.retain(|s| s.kind.worktree() != Some(worktree));
151    }
152
153    /// Pulls its task sources relevant to the worktree and the language given,
154    /// returns all task templates with their source kinds, in no specific order.
155    pub fn list_tasks(
156        &self,
157        language: Option<Arc<Language>>,
158        worktree: Option<WorktreeId>,
159    ) -> Vec<(TaskSourceKind, TaskTemplate)> {
160        let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
161            name: language.name(),
162        });
163        let language_tasks = language
164            .and_then(|language| language.context_provider()?.associated_tasks())
165            .into_iter()
166            .flat_map(|tasks| tasks.0.into_iter())
167            .flat_map(|task| Some((task_source_kind.as_ref()?, task)));
168
169        self.sources
170            .iter()
171            .filter(|source| {
172                let source_worktree = source.kind.worktree();
173                worktree.is_none() || source_worktree.is_none() || source_worktree == worktree
174            })
175            .flat_map(|source| {
176                source
177                    .source
178                    .tasks_to_schedule()
179                    .0
180                    .into_iter()
181                    .map(|task| (&source.kind, task))
182            })
183            .chain(language_tasks)
184            .map(|(task_source_kind, task)| (task_source_kind.clone(), task))
185            .collect()
186    }
187
188    /// Pulls its task sources relevant to the worktree and the language given and resolves them with the [`TaskContext`] given.
189    /// Joins the new resolutions with the resolved tasks that were used (spawned) before,
190    /// orders them so that the most recently used come first, all equally used ones are ordered so that the most specific tasks come first.
191    /// Deduplicates the tasks by their labels and splits the ordered list into two: used tasks and the rest, newly resolved tasks.
192    pub fn used_and_current_resolved_tasks(
193        &self,
194        language: Option<Arc<Language>>,
195        worktree: Option<WorktreeId>,
196        task_context: &TaskContext,
197    ) -> (
198        Vec<(TaskSourceKind, ResolvedTask)>,
199        Vec<(TaskSourceKind, ResolvedTask)>,
200    ) {
201        let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
202            name: language.name(),
203        });
204        let language_tasks = language
205            .and_then(|language| language.context_provider()?.associated_tasks())
206            .into_iter()
207            .flat_map(|tasks| tasks.0.into_iter())
208            .flat_map(|task| Some((task_source_kind.as_ref()?, task)));
209
210        let mut lru_score = 0_u32;
211        let mut task_usage = self
212            .last_scheduled_tasks
213            .iter()
214            .rev()
215            .filter(|(task_kind, _)| {
216                if matches!(task_kind, TaskSourceKind::Language { .. }) {
217                    Some(task_kind) == task_source_kind.as_ref()
218                } else {
219                    true
220                }
221            })
222            .fold(
223                BTreeMap::default(),
224                |mut tasks, (task_source_kind, resolved_task)| {
225                    tasks.entry(&resolved_task.id).or_insert_with(|| {
226                        (task_source_kind, resolved_task, post_inc(&mut lru_score))
227                    });
228                    tasks
229                },
230            );
231        let not_used_score = post_inc(&mut lru_score);
232        let currently_resolved_tasks = self
233            .sources
234            .iter()
235            .filter(|source| {
236                let source_worktree = source.kind.worktree();
237                worktree.is_none() || source_worktree.is_none() || source_worktree == worktree
238            })
239            .flat_map(|source| {
240                source
241                    .source
242                    .tasks_to_schedule()
243                    .0
244                    .into_iter()
245                    .map(|task| (&source.kind, task))
246            })
247            .chain(language_tasks)
248            .filter_map(|(kind, task)| {
249                let id_base = kind.to_id_base();
250                Some((kind, task.resolve_task(&id_base, task_context)?))
251            })
252            .map(|(kind, task)| {
253                let lru_score = task_usage
254                    .remove(&task.id)
255                    .map(|(_, _, lru_score)| lru_score)
256                    .unwrap_or(not_used_score);
257                (kind.clone(), task, lru_score)
258            })
259            .collect::<Vec<_>>();
260        let previously_spawned_tasks = task_usage
261            .into_iter()
262            .map(|(_, (kind, task, lru_score))| (kind.clone(), task.clone(), lru_score));
263
264        let mut tasks_by_label = BTreeMap::default();
265        tasks_by_label = previously_spawned_tasks.into_iter().fold(
266            tasks_by_label,
267            |mut tasks_by_label, (source, task, lru_score)| {
268                match tasks_by_label.entry((source, task.resolved_label.clone())) {
269                    btree_map::Entry::Occupied(mut o) => {
270                        let (_, previous_lru_score) = o.get();
271                        if previous_lru_score >= &lru_score {
272                            o.insert((task, lru_score));
273                        }
274                    }
275                    btree_map::Entry::Vacant(v) => {
276                        v.insert((task, lru_score));
277                    }
278                }
279                tasks_by_label
280            },
281        );
282        tasks_by_label = currently_resolved_tasks.iter().fold(
283            tasks_by_label,
284            |mut tasks_by_label, (source, task, lru_score)| {
285                match tasks_by_label.entry((source.clone(), task.resolved_label.clone())) {
286                    btree_map::Entry::Occupied(mut o) => {
287                        let (previous_task, _) = o.get();
288                        let new_template = task.original_task();
289                        if new_template != previous_task.original_task() {
290                            o.insert((task.clone(), *lru_score));
291                        }
292                    }
293                    btree_map::Entry::Vacant(v) => {
294                        v.insert((task.clone(), *lru_score));
295                    }
296                }
297                tasks_by_label
298            },
299        );
300
301        let resolved = tasks_by_label
302            .into_iter()
303            .map(|((kind, _), (task, lru_score))| (kind, task, lru_score))
304            .sorted_by(task_lru_comparator)
305            .filter_map(|(kind, task, lru_score)| {
306                if lru_score < not_used_score {
307                    Some((kind, task))
308                } else {
309                    None
310                }
311            })
312            .collect();
313
314        (
315            resolved,
316            currently_resolved_tasks
317                .into_iter()
318                .sorted_unstable_by(task_lru_comparator)
319                .map(|(kind, task, _)| (kind, task))
320                .collect(),
321        )
322    }
323
324    /// Returns the last scheduled task, if any of the sources contains one with the matching id.
325    pub fn last_scheduled_task(&self) -> Option<(TaskSourceKind, ResolvedTask)> {
326        self.last_scheduled_tasks.back().cloned()
327    }
328
329    /// Registers task "usage" as being scheduled – to be used for LRU sorting when listing all tasks.
330    pub fn task_scheduled(
331        &mut self,
332        task_source_kind: TaskSourceKind,
333        resolved_task: ResolvedTask,
334    ) {
335        self.last_scheduled_tasks
336            .push_back((task_source_kind, resolved_task));
337        if self.last_scheduled_tasks.len() > 5_000 {
338            self.last_scheduled_tasks.pop_front();
339        }
340    }
341
342    /// Deletes a resolved task from history, using its id.
343    /// A similar may still resurface in `used_and_current_resolved_tasks` when its [`TaskTemplate`] is resolved again.
344    pub fn delete_previously_used(&mut self, id: &TaskId) {
345        self.last_scheduled_tasks.retain(|(_, task)| &task.id != id);
346    }
347}
348
349fn task_lru_comparator(
350    (kind_a, task_a, lru_score_a): &(TaskSourceKind, ResolvedTask, u32),
351    (kind_b, task_b, lru_score_b): &(TaskSourceKind, ResolvedTask, u32),
352) -> cmp::Ordering {
353    lru_score_a
354        // First, display recently used templates above all.
355        .cmp(&lru_score_b)
356        // Then, ensure more specific sources are displayed first.
357        .then(task_source_kind_preference(kind_a).cmp(&task_source_kind_preference(kind_b)))
358        // After that, display first more specific tasks, using more template variables.
359        // Bonus points for tasks with symbol variables.
360        .then(task_variables_preference(task_a).cmp(&task_variables_preference(task_b)))
361        // Finally, sort by the resolved label, but a bit more specifically, to avoid mixing letters and digits.
362        .then({
363            NumericPrefixWithSuffix::from_numeric_prefixed_str(&task_a.resolved_label)
364                .cmp(&NumericPrefixWithSuffix::from_numeric_prefixed_str(
365                    &task_b.resolved_label,
366                ))
367                .then(task_a.resolved_label.cmp(&task_b.resolved_label))
368                .then(kind_a.cmp(kind_b))
369        })
370}
371
372fn task_source_kind_preference(kind: &TaskSourceKind) -> u32 {
373    match kind {
374        TaskSourceKind::Language { .. } => 1,
375        TaskSourceKind::UserInput => 2,
376        TaskSourceKind::Worktree { .. } => 3,
377        TaskSourceKind::AbsPath { .. } => 4,
378    }
379}
380
381fn task_variables_preference(task: &ResolvedTask) -> Reverse<usize> {
382    let task_variables = task.substituted_variables();
383    Reverse(if task_variables.contains(&VariableName::Symbol) {
384        task_variables.len() + 1
385    } else {
386        task_variables.len()
387    })
388}
389
390#[cfg(test)]
391mod test_inventory {
392    use gpui::{AppContext, Model, TestAppContext};
393    use itertools::Itertools;
394    use task::{
395        static_source::{StaticSource, TrackedFile},
396        TaskContext, TaskTemplate, TaskTemplates,
397    };
398    use worktree::WorktreeId;
399
400    use crate::Inventory;
401
402    use super::{task_source_kind_preference, TaskSourceKind, UnboundedSender};
403
404    #[derive(Debug, Clone, PartialEq, Eq)]
405    pub struct TestTask {
406        name: String,
407    }
408
409    pub(super) fn static_test_source(
410        task_names: impl IntoIterator<Item = String>,
411        updates: UnboundedSender<()>,
412        cx: &mut AppContext,
413    ) -> StaticSource {
414        let tasks = TaskTemplates(
415            task_names
416                .into_iter()
417                .map(|name| TaskTemplate {
418                    label: name,
419                    command: "test command".to_owned(),
420                    ..TaskTemplate::default()
421                })
422                .collect(),
423        );
424        let (tx, rx) = futures::channel::mpsc::unbounded();
425        let file = TrackedFile::new(rx, updates, cx);
426        tx.unbounded_send(serde_json::to_string(&tasks).unwrap())
427            .unwrap();
428        StaticSource::new(file)
429    }
430
431    pub(super) fn task_template_names(
432        inventory: &Model<Inventory>,
433        worktree: Option<WorktreeId>,
434        cx: &mut TestAppContext,
435    ) -> Vec<String> {
436        inventory.update(cx, |inventory, _| {
437            inventory
438                .list_tasks(None, worktree)
439                .into_iter()
440                .map(|(_, task)| task.label)
441                .sorted()
442                .collect()
443        })
444    }
445
446    pub(super) fn resolved_task_names(
447        inventory: &Model<Inventory>,
448        worktree: Option<WorktreeId>,
449        cx: &mut TestAppContext,
450    ) -> Vec<String> {
451        inventory.update(cx, |inventory, _| {
452            let (used, current) =
453                inventory.used_and_current_resolved_tasks(None, worktree, &TaskContext::default());
454            used.into_iter()
455                .chain(current)
456                .map(|(_, task)| task.original_task().label.clone())
457                .collect()
458        })
459    }
460
461    pub(super) fn register_task_used(
462        inventory: &Model<Inventory>,
463        task_name: &str,
464        cx: &mut TestAppContext,
465    ) {
466        inventory.update(cx, |inventory, _| {
467            let (task_source_kind, task) = inventory
468                .list_tasks(None, None)
469                .into_iter()
470                .find(|(_, task)| task.label == task_name)
471                .unwrap_or_else(|| panic!("Failed to find task with name {task_name}"));
472            let id_base = task_source_kind.to_id_base();
473            inventory.task_scheduled(
474                task_source_kind.clone(),
475                task.resolve_task(&id_base, &TaskContext::default())
476                    .unwrap_or_else(|| panic!("Failed to resolve task with name {task_name}")),
477            );
478        });
479    }
480
481    pub(super) fn list_tasks(
482        inventory: &Model<Inventory>,
483        worktree: Option<WorktreeId>,
484        cx: &mut TestAppContext,
485    ) -> Vec<(TaskSourceKind, String)> {
486        inventory.update(cx, |inventory, _| {
487            let (used, current) =
488                inventory.used_and_current_resolved_tasks(None, worktree, &TaskContext::default());
489            let mut all = used;
490            all.extend(current);
491            all.into_iter()
492                .map(|(source_kind, task)| (source_kind, task.resolved_label))
493                .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
494                .collect()
495        })
496    }
497}
498
499/// A context provided that tries to provide values for all non-custom [`VariableName`] variants for a currently opened file.
500/// Applied as a base for every custom [`ContextProvider`] unless explicitly oped out.
501pub struct BasicContextProvider {
502    project: Model<Project>,
503}
504
505impl BasicContextProvider {
506    pub fn new(project: Model<Project>) -> Self {
507        Self { project }
508    }
509}
510
511impl ContextProvider for BasicContextProvider {
512    fn build_context(
513        &self,
514        _: &TaskVariables,
515        location: &Location,
516        cx: &mut AppContext,
517    ) -> Result<TaskVariables> {
518        let buffer = location.buffer.read(cx);
519        let buffer_snapshot = buffer.snapshot();
520        let symbols = buffer_snapshot.symbols_containing(location.range.start, None);
521        let symbol = symbols.unwrap_or_default().last().map(|symbol| {
522            let range = symbol
523                .name_ranges
524                .last()
525                .cloned()
526                .unwrap_or(0..symbol.text.len());
527            symbol.text[range].to_string()
528        });
529
530        let current_file = buffer
531            .file()
532            .and_then(|file| file.as_local())
533            .map(|file| file.abs_path(cx).to_string_lossy().to_string());
534        let Point { row, column } = location.range.start.to_point(&buffer_snapshot);
535        let row = row + 1;
536        let column = column + 1;
537        let selected_text = buffer
538            .chars_for_range(location.range.clone())
539            .collect::<String>();
540
541        let mut task_variables = TaskVariables::from_iter([
542            (VariableName::Row, row.to_string()),
543            (VariableName::Column, column.to_string()),
544        ]);
545
546        if let Some(symbol) = symbol {
547            task_variables.insert(VariableName::Symbol, symbol);
548        }
549        if !selected_text.trim().is_empty() {
550            task_variables.insert(VariableName::SelectedText, selected_text);
551        }
552        if let Some(path) = current_file {
553            task_variables.insert(VariableName::File, path);
554        }
555
556        let worktree_abs_path = buffer
557            .file()
558            .map(|file| WorktreeId::from_usize(file.worktree_id()))
559            .and_then(|worktree_id| {
560                self.project
561                    .read(cx)
562                    .worktree_for_id(worktree_id, cx)
563                    .map(|worktree| worktree.read(cx).abs_path())
564            });
565        if let Some(worktree_path) = worktree_abs_path {
566            task_variables.insert(
567                VariableName::WorktreeRoot,
568                worktree_path.to_string_lossy().to_string(),
569            );
570        }
571
572        Ok(task_variables)
573    }
574}
575
576/// A ContextProvider that doesn't provide any task variables on it's own, though it has some associated tasks.
577pub struct ContextProviderWithTasks {
578    templates: TaskTemplates,
579}
580
581impl ContextProviderWithTasks {
582    pub fn new(definitions: TaskTemplates) -> Self {
583        Self {
584            templates: definitions,
585        }
586    }
587}
588
589impl ContextProvider for ContextProviderWithTasks {
590    fn associated_tasks(&self) -> Option<TaskTemplates> {
591        Some(self.templates.clone())
592    }
593}
594
595#[cfg(test)]
596mod tests {
597    use gpui::TestAppContext;
598
599    use super::test_inventory::*;
600    use super::*;
601
602    #[gpui::test]
603    fn test_task_list_sorting(cx: &mut TestAppContext) {
604        let inventory = cx.update(Inventory::new);
605        let initial_tasks = resolved_task_names(&inventory, None, cx);
606        assert!(
607            initial_tasks.is_empty(),
608            "No tasks expected for empty inventory, but got {initial_tasks:?}"
609        );
610        let initial_tasks = task_template_names(&inventory, None, cx);
611        assert!(
612            initial_tasks.is_empty(),
613            "No tasks expected for empty inventory, but got {initial_tasks:?}"
614        );
615
616        inventory.update(cx, |inventory, cx| {
617            inventory.add_source(
618                TaskSourceKind::UserInput,
619                |tx, cx| static_test_source(vec!["3_task".to_string()], tx, cx),
620                cx,
621            );
622        });
623        inventory.update(cx, |inventory, cx| {
624            inventory.add_source(
625                TaskSourceKind::UserInput,
626                |tx, cx| {
627                    static_test_source(
628                        vec![
629                            "1_task".to_string(),
630                            "2_task".to_string(),
631                            "1_a_task".to_string(),
632                        ],
633                        tx,
634                        cx,
635                    )
636                },
637                cx,
638            );
639        });
640        cx.run_until_parked();
641        let expected_initial_state = [
642            "1_a_task".to_string(),
643            "1_task".to_string(),
644            "2_task".to_string(),
645            "3_task".to_string(),
646        ];
647        assert_eq!(
648            task_template_names(&inventory, None, cx),
649            &expected_initial_state,
650        );
651        assert_eq!(
652            resolved_task_names(&inventory, None, cx),
653            &expected_initial_state,
654            "Tasks with equal amount of usages should be sorted alphanumerically"
655        );
656
657        register_task_used(&inventory, "2_task", cx);
658        assert_eq!(
659            task_template_names(&inventory, None, cx),
660            &expected_initial_state,
661        );
662        assert_eq!(
663            resolved_task_names(&inventory, None, cx),
664            vec![
665                "2_task".to_string(),
666                "2_task".to_string(),
667                "1_a_task".to_string(),
668                "1_task".to_string(),
669                "3_task".to_string()
670            ],
671        );
672
673        register_task_used(&inventory, "1_task", cx);
674        register_task_used(&inventory, "1_task", cx);
675        register_task_used(&inventory, "1_task", cx);
676        register_task_used(&inventory, "3_task", cx);
677        assert_eq!(
678            task_template_names(&inventory, None, cx),
679            &expected_initial_state,
680        );
681        assert_eq!(
682            resolved_task_names(&inventory, None, cx),
683            vec![
684                "3_task".to_string(),
685                "1_task".to_string(),
686                "2_task".to_string(),
687                "3_task".to_string(),
688                "1_task".to_string(),
689                "2_task".to_string(),
690                "1_a_task".to_string(),
691            ],
692        );
693
694        inventory.update(cx, |inventory, cx| {
695            inventory.add_source(
696                TaskSourceKind::UserInput,
697                |tx, cx| {
698                    static_test_source(vec!["10_hello".to_string(), "11_hello".to_string()], tx, cx)
699                },
700                cx,
701            );
702        });
703        cx.run_until_parked();
704        let expected_updated_state = [
705            "10_hello".to_string(),
706            "11_hello".to_string(),
707            "1_a_task".to_string(),
708            "1_task".to_string(),
709            "2_task".to_string(),
710            "3_task".to_string(),
711        ];
712        assert_eq!(
713            task_template_names(&inventory, None, cx),
714            &expected_updated_state,
715        );
716        assert_eq!(
717            resolved_task_names(&inventory, None, cx),
718            vec![
719                "3_task".to_string(),
720                "1_task".to_string(),
721                "2_task".to_string(),
722                "3_task".to_string(),
723                "1_task".to_string(),
724                "2_task".to_string(),
725                "1_a_task".to_string(),
726                "10_hello".to_string(),
727                "11_hello".to_string(),
728            ],
729        );
730
731        register_task_used(&inventory, "11_hello", cx);
732        assert_eq!(
733            task_template_names(&inventory, None, cx),
734            &expected_updated_state,
735        );
736        assert_eq!(
737            resolved_task_names(&inventory, None, cx),
738            vec![
739                "11_hello".to_string(),
740                "3_task".to_string(),
741                "1_task".to_string(),
742                "2_task".to_string(),
743                "11_hello".to_string(),
744                "3_task".to_string(),
745                "1_task".to_string(),
746                "2_task".to_string(),
747                "1_a_task".to_string(),
748                "10_hello".to_string(),
749            ],
750        );
751    }
752
753    #[gpui::test]
754    fn test_inventory_static_task_filters(cx: &mut TestAppContext) {
755        let inventory_with_statics = cx.update(Inventory::new);
756        let common_name = "common_task_name";
757        let path_1 = Path::new("path_1");
758        let path_2 = Path::new("path_2");
759        let worktree_1 = WorktreeId::from_usize(1);
760        let worktree_path_1 = Path::new("worktree_path_1");
761        let worktree_2 = WorktreeId::from_usize(2);
762        let worktree_path_2 = Path::new("worktree_path_2");
763
764        inventory_with_statics.update(cx, |inventory, cx| {
765            inventory.add_source(
766                TaskSourceKind::UserInput,
767                |tx, cx| {
768                    static_test_source(
769                        vec!["user_input".to_string(), common_name.to_string()],
770                        tx,
771                        cx,
772                    )
773                },
774                cx,
775            );
776            inventory.add_source(
777                TaskSourceKind::AbsPath {
778                    id_base: "test source",
779                    abs_path: path_1.to_path_buf(),
780                },
781                |tx, cx| {
782                    static_test_source(
783                        vec!["static_source_1".to_string(), common_name.to_string()],
784                        tx,
785                        cx,
786                    )
787                },
788                cx,
789            );
790            inventory.add_source(
791                TaskSourceKind::AbsPath {
792                    id_base: "test source",
793                    abs_path: path_2.to_path_buf(),
794                },
795                |tx, cx| {
796                    static_test_source(
797                        vec!["static_source_2".to_string(), common_name.to_string()],
798                        tx,
799                        cx,
800                    )
801                },
802                cx,
803            );
804            inventory.add_source(
805                TaskSourceKind::Worktree {
806                    id: worktree_1,
807                    abs_path: worktree_path_1.to_path_buf(),
808                    id_base: "test_source",
809                },
810                |tx, cx| {
811                    static_test_source(
812                        vec!["worktree_1".to_string(), common_name.to_string()],
813                        tx,
814                        cx,
815                    )
816                },
817                cx,
818            );
819            inventory.add_source(
820                TaskSourceKind::Worktree {
821                    id: worktree_2,
822                    abs_path: worktree_path_2.to_path_buf(),
823                    id_base: "test_source",
824                },
825                |tx, cx| {
826                    static_test_source(
827                        vec!["worktree_2".to_string(), common_name.to_string()],
828                        tx,
829                        cx,
830                    )
831                },
832                cx,
833            );
834        });
835        cx.run_until_parked();
836        let worktree_independent_tasks = vec![
837            (
838                TaskSourceKind::AbsPath {
839                    id_base: "test source",
840                    abs_path: path_1.to_path_buf(),
841                },
842                "static_source_1".to_string(),
843            ),
844            (
845                TaskSourceKind::AbsPath {
846                    id_base: "test source",
847                    abs_path: path_1.to_path_buf(),
848                },
849                common_name.to_string(),
850            ),
851            (
852                TaskSourceKind::AbsPath {
853                    id_base: "test source",
854                    abs_path: path_2.to_path_buf(),
855                },
856                common_name.to_string(),
857            ),
858            (
859                TaskSourceKind::AbsPath {
860                    id_base: "test source",
861                    abs_path: path_2.to_path_buf(),
862                },
863                "static_source_2".to_string(),
864            ),
865            (TaskSourceKind::UserInput, common_name.to_string()),
866            (TaskSourceKind::UserInput, "user_input".to_string()),
867        ];
868        let worktree_1_tasks = [
869            (
870                TaskSourceKind::Worktree {
871                    id: worktree_1,
872                    abs_path: worktree_path_1.to_path_buf(),
873                    id_base: "test_source",
874                },
875                common_name.to_string(),
876            ),
877            (
878                TaskSourceKind::Worktree {
879                    id: worktree_1,
880                    abs_path: worktree_path_1.to_path_buf(),
881                    id_base: "test_source",
882                },
883                "worktree_1".to_string(),
884            ),
885        ];
886        let worktree_2_tasks = [
887            (
888                TaskSourceKind::Worktree {
889                    id: worktree_2,
890                    abs_path: worktree_path_2.to_path_buf(),
891                    id_base: "test_source",
892                },
893                common_name.to_string(),
894            ),
895            (
896                TaskSourceKind::Worktree {
897                    id: worktree_2,
898                    abs_path: worktree_path_2.to_path_buf(),
899                    id_base: "test_source",
900                },
901                "worktree_2".to_string(),
902            ),
903        ];
904
905        let all_tasks = worktree_1_tasks
906            .iter()
907            .chain(worktree_2_tasks.iter())
908            // worktree-less tasks come later in the list
909            .chain(worktree_independent_tasks.iter())
910            .cloned()
911            .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
912            .collect::<Vec<_>>();
913
914        assert_eq!(list_tasks(&inventory_with_statics, None, cx), all_tasks);
915        assert_eq!(
916            list_tasks(&inventory_with_statics, Some(worktree_1), cx),
917            worktree_1_tasks
918                .iter()
919                .chain(worktree_independent_tasks.iter())
920                .cloned()
921                .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
922                .collect::<Vec<_>>(),
923        );
924        assert_eq!(
925            list_tasks(&inventory_with_statics, Some(worktree_2), cx),
926            worktree_2_tasks
927                .iter()
928                .chain(worktree_independent_tasks.iter())
929                .cloned()
930                .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
931                .collect::<Vec<_>>(),
932        );
933    }
934}