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.clone());
554
555            let path = Path::new(&path);
556
557            if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
558                task_variables.insert(VariableName::Filename, String::from(filename));
559            }
560
561            if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
562                task_variables.insert(VariableName::Stem, String::from(stem));
563            }
564        }
565
566        let worktree_abs_path = buffer
567            .file()
568            .map(|file| WorktreeId::from_usize(file.worktree_id()))
569            .and_then(|worktree_id| {
570                self.project
571                    .read(cx)
572                    .worktree_for_id(worktree_id, cx)
573                    .map(|worktree| worktree.read(cx).abs_path())
574            });
575        if let Some(worktree_path) = worktree_abs_path {
576            task_variables.insert(
577                VariableName::WorktreeRoot,
578                worktree_path.to_string_lossy().to_string(),
579            );
580        }
581
582        Ok(task_variables)
583    }
584}
585
586/// A ContextProvider that doesn't provide any task variables on it's own, though it has some associated tasks.
587pub struct ContextProviderWithTasks {
588    templates: TaskTemplates,
589}
590
591impl ContextProviderWithTasks {
592    pub fn new(definitions: TaskTemplates) -> Self {
593        Self {
594            templates: definitions,
595        }
596    }
597}
598
599impl ContextProvider for ContextProviderWithTasks {
600    fn associated_tasks(&self) -> Option<TaskTemplates> {
601        Some(self.templates.clone())
602    }
603}
604
605#[cfg(test)]
606mod tests {
607    use gpui::TestAppContext;
608
609    use super::test_inventory::*;
610    use super::*;
611
612    #[gpui::test]
613    fn test_task_list_sorting(cx: &mut TestAppContext) {
614        let inventory = cx.update(Inventory::new);
615        let initial_tasks = resolved_task_names(&inventory, None, cx);
616        assert!(
617            initial_tasks.is_empty(),
618            "No tasks expected for empty inventory, but got {initial_tasks:?}"
619        );
620        let initial_tasks = task_template_names(&inventory, None, cx);
621        assert!(
622            initial_tasks.is_empty(),
623            "No tasks expected for empty inventory, but got {initial_tasks:?}"
624        );
625
626        inventory.update(cx, |inventory, cx| {
627            inventory.add_source(
628                TaskSourceKind::UserInput,
629                |tx, cx| static_test_source(vec!["3_task".to_string()], tx, cx),
630                cx,
631            );
632        });
633        inventory.update(cx, |inventory, cx| {
634            inventory.add_source(
635                TaskSourceKind::UserInput,
636                |tx, cx| {
637                    static_test_source(
638                        vec![
639                            "1_task".to_string(),
640                            "2_task".to_string(),
641                            "1_a_task".to_string(),
642                        ],
643                        tx,
644                        cx,
645                    )
646                },
647                cx,
648            );
649        });
650        cx.run_until_parked();
651        let expected_initial_state = [
652            "1_a_task".to_string(),
653            "1_task".to_string(),
654            "2_task".to_string(),
655            "3_task".to_string(),
656        ];
657        assert_eq!(
658            task_template_names(&inventory, None, cx),
659            &expected_initial_state,
660        );
661        assert_eq!(
662            resolved_task_names(&inventory, None, cx),
663            &expected_initial_state,
664            "Tasks with equal amount of usages should be sorted alphanumerically"
665        );
666
667        register_task_used(&inventory, "2_task", cx);
668        assert_eq!(
669            task_template_names(&inventory, None, cx),
670            &expected_initial_state,
671        );
672        assert_eq!(
673            resolved_task_names(&inventory, None, cx),
674            vec![
675                "2_task".to_string(),
676                "2_task".to_string(),
677                "1_a_task".to_string(),
678                "1_task".to_string(),
679                "3_task".to_string()
680            ],
681        );
682
683        register_task_used(&inventory, "1_task", cx);
684        register_task_used(&inventory, "1_task", cx);
685        register_task_used(&inventory, "1_task", cx);
686        register_task_used(&inventory, "3_task", cx);
687        assert_eq!(
688            task_template_names(&inventory, None, cx),
689            &expected_initial_state,
690        );
691        assert_eq!(
692            resolved_task_names(&inventory, None, cx),
693            vec![
694                "3_task".to_string(),
695                "1_task".to_string(),
696                "2_task".to_string(),
697                "3_task".to_string(),
698                "1_task".to_string(),
699                "2_task".to_string(),
700                "1_a_task".to_string(),
701            ],
702        );
703
704        inventory.update(cx, |inventory, cx| {
705            inventory.add_source(
706                TaskSourceKind::UserInput,
707                |tx, cx| {
708                    static_test_source(vec!["10_hello".to_string(), "11_hello".to_string()], tx, cx)
709                },
710                cx,
711            );
712        });
713        cx.run_until_parked();
714        let expected_updated_state = [
715            "10_hello".to_string(),
716            "11_hello".to_string(),
717            "1_a_task".to_string(),
718            "1_task".to_string(),
719            "2_task".to_string(),
720            "3_task".to_string(),
721        ];
722        assert_eq!(
723            task_template_names(&inventory, None, cx),
724            &expected_updated_state,
725        );
726        assert_eq!(
727            resolved_task_names(&inventory, None, cx),
728            vec![
729                "3_task".to_string(),
730                "1_task".to_string(),
731                "2_task".to_string(),
732                "3_task".to_string(),
733                "1_task".to_string(),
734                "2_task".to_string(),
735                "1_a_task".to_string(),
736                "10_hello".to_string(),
737                "11_hello".to_string(),
738            ],
739        );
740
741        register_task_used(&inventory, "11_hello", cx);
742        assert_eq!(
743            task_template_names(&inventory, None, cx),
744            &expected_updated_state,
745        );
746        assert_eq!(
747            resolved_task_names(&inventory, None, cx),
748            vec![
749                "11_hello".to_string(),
750                "3_task".to_string(),
751                "1_task".to_string(),
752                "2_task".to_string(),
753                "11_hello".to_string(),
754                "3_task".to_string(),
755                "1_task".to_string(),
756                "2_task".to_string(),
757                "1_a_task".to_string(),
758                "10_hello".to_string(),
759            ],
760        );
761    }
762
763    #[gpui::test]
764    fn test_inventory_static_task_filters(cx: &mut TestAppContext) {
765        let inventory_with_statics = cx.update(Inventory::new);
766        let common_name = "common_task_name";
767        let path_1 = Path::new("path_1");
768        let path_2 = Path::new("path_2");
769        let worktree_1 = WorktreeId::from_usize(1);
770        let worktree_path_1 = Path::new("worktree_path_1");
771        let worktree_2 = WorktreeId::from_usize(2);
772        let worktree_path_2 = Path::new("worktree_path_2");
773
774        inventory_with_statics.update(cx, |inventory, cx| {
775            inventory.add_source(
776                TaskSourceKind::UserInput,
777                |tx, cx| {
778                    static_test_source(
779                        vec!["user_input".to_string(), common_name.to_string()],
780                        tx,
781                        cx,
782                    )
783                },
784                cx,
785            );
786            inventory.add_source(
787                TaskSourceKind::AbsPath {
788                    id_base: "test source",
789                    abs_path: path_1.to_path_buf(),
790                },
791                |tx, cx| {
792                    static_test_source(
793                        vec!["static_source_1".to_string(), common_name.to_string()],
794                        tx,
795                        cx,
796                    )
797                },
798                cx,
799            );
800            inventory.add_source(
801                TaskSourceKind::AbsPath {
802                    id_base: "test source",
803                    abs_path: path_2.to_path_buf(),
804                },
805                |tx, cx| {
806                    static_test_source(
807                        vec!["static_source_2".to_string(), common_name.to_string()],
808                        tx,
809                        cx,
810                    )
811                },
812                cx,
813            );
814            inventory.add_source(
815                TaskSourceKind::Worktree {
816                    id: worktree_1,
817                    abs_path: worktree_path_1.to_path_buf(),
818                    id_base: "test_source",
819                },
820                |tx, cx| {
821                    static_test_source(
822                        vec!["worktree_1".to_string(), common_name.to_string()],
823                        tx,
824                        cx,
825                    )
826                },
827                cx,
828            );
829            inventory.add_source(
830                TaskSourceKind::Worktree {
831                    id: worktree_2,
832                    abs_path: worktree_path_2.to_path_buf(),
833                    id_base: "test_source",
834                },
835                |tx, cx| {
836                    static_test_source(
837                        vec!["worktree_2".to_string(), common_name.to_string()],
838                        tx,
839                        cx,
840                    )
841                },
842                cx,
843            );
844        });
845        cx.run_until_parked();
846        let worktree_independent_tasks = vec![
847            (
848                TaskSourceKind::AbsPath {
849                    id_base: "test source",
850                    abs_path: path_1.to_path_buf(),
851                },
852                "static_source_1".to_string(),
853            ),
854            (
855                TaskSourceKind::AbsPath {
856                    id_base: "test source",
857                    abs_path: path_1.to_path_buf(),
858                },
859                common_name.to_string(),
860            ),
861            (
862                TaskSourceKind::AbsPath {
863                    id_base: "test source",
864                    abs_path: path_2.to_path_buf(),
865                },
866                common_name.to_string(),
867            ),
868            (
869                TaskSourceKind::AbsPath {
870                    id_base: "test source",
871                    abs_path: path_2.to_path_buf(),
872                },
873                "static_source_2".to_string(),
874            ),
875            (TaskSourceKind::UserInput, common_name.to_string()),
876            (TaskSourceKind::UserInput, "user_input".to_string()),
877        ];
878        let worktree_1_tasks = [
879            (
880                TaskSourceKind::Worktree {
881                    id: worktree_1,
882                    abs_path: worktree_path_1.to_path_buf(),
883                    id_base: "test_source",
884                },
885                common_name.to_string(),
886            ),
887            (
888                TaskSourceKind::Worktree {
889                    id: worktree_1,
890                    abs_path: worktree_path_1.to_path_buf(),
891                    id_base: "test_source",
892                },
893                "worktree_1".to_string(),
894            ),
895        ];
896        let worktree_2_tasks = [
897            (
898                TaskSourceKind::Worktree {
899                    id: worktree_2,
900                    abs_path: worktree_path_2.to_path_buf(),
901                    id_base: "test_source",
902                },
903                common_name.to_string(),
904            ),
905            (
906                TaskSourceKind::Worktree {
907                    id: worktree_2,
908                    abs_path: worktree_path_2.to_path_buf(),
909                    id_base: "test_source",
910                },
911                "worktree_2".to_string(),
912            ),
913        ];
914
915        let all_tasks = worktree_1_tasks
916            .iter()
917            .chain(worktree_2_tasks.iter())
918            // worktree-less tasks come later in the list
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!(list_tasks(&inventory_with_statics, None, cx), all_tasks);
925        assert_eq!(
926            list_tasks(&inventory_with_statics, Some(worktree_1), cx),
927            worktree_1_tasks
928                .iter()
929                .chain(worktree_independent_tasks.iter())
930                .cloned()
931                .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
932                .collect::<Vec<_>>(),
933        );
934        assert_eq!(
935            list_tasks(&inventory_with_statics, Some(worktree_2), cx),
936            worktree_2_tasks
937                .iter()
938                .chain(worktree_independent_tasks.iter())
939                .cloned()
940                .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
941                .collect::<Vec<_>>(),
942        );
943    }
944}