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