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