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