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