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