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