task_inventory.rs

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