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