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