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