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