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, 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::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: 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 .clone()
328 .into_iter()
329 .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: match template.task_type {
337 task::TaskType::Script => paths::tasks_file().clone(),
338 task::TaskType::Debug(_) => paths::debug_tasks_file().clone(),
339 },
340 },
341 template,
342 )
343 })
344 }
345
346 fn worktree_templates_from_settings(
347 &self,
348 worktree: Option<WorktreeId>,
349 ) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
350 worktree.into_iter().flat_map(|worktree| {
351 self.templates_from_settings
352 .worktree
353 .get(&worktree)
354 .into_iter()
355 .flatten()
356 .flat_map(|(directory, templates)| {
357 templates.iter().map(move |template| (directory, template))
358 })
359 .map(move |((directory, _task_kind), template)| {
360 (
361 TaskSourceKind::Worktree {
362 id: worktree,
363 directory_in_worktree: directory.to_path_buf(),
364 id_base: Cow::Owned(format!(
365 "local worktree tasks from directory {directory:?}"
366 )),
367 },
368 template.clone(),
369 )
370 })
371 })
372 }
373
374 /// Updates in-memory task metadata from the JSON string given.
375 /// Will fail if the JSON is not a valid array of objects, but will continue if any object will not parse into a [`TaskTemplate`].
376 ///
377 /// Global tasks are updated for no worktree provided, otherwise the worktree metadata for a given path will be updated.
378 pub(crate) fn update_file_based_tasks(
379 &mut self,
380 location: Option<SettingsLocation<'_>>,
381 raw_tasks_json: Option<&str>,
382 task_kind: TaskKind,
383 ) -> anyhow::Result<()> {
384 let raw_tasks =
385 parse_json_with_comments::<Vec<serde_json::Value>>(raw_tasks_json.unwrap_or("[]"))
386 .context("parsing tasks file content as a JSON array")?;
387 let new_templates = raw_tasks
388 .into_iter()
389 .filter_map(|raw_template| match &task_kind {
390 TaskKind::Script => serde_json::from_value::<TaskTemplate>(raw_template).log_err(),
391 TaskKind::Debug => serde_json::from_value::<DebugTaskDefinition>(raw_template)
392 .log_err()
393 .and_then(|content| content.to_zed_format().log_err()),
394 });
395
396 let parsed_templates = &mut self.templates_from_settings;
397 match location {
398 Some(location) => {
399 let new_templates = new_templates.collect::<Vec<_>>();
400 if new_templates.is_empty() {
401 if let Some(worktree_tasks) =
402 parsed_templates.worktree.get_mut(&location.worktree_id)
403 {
404 worktree_tasks.remove(&(Arc::from(location.path), task_kind));
405 }
406 } else {
407 parsed_templates
408 .worktree
409 .entry(location.worktree_id)
410 .or_default()
411 .insert((Arc::from(location.path), task_kind), new_templates);
412 }
413 }
414 None => parsed_templates.global = new_templates.collect(),
415 }
416 Ok(())
417 }
418}
419
420fn task_lru_comparator(
421 (kind_a, task_a, lru_score_a): &(TaskSourceKind, ResolvedTask, u32),
422 (kind_b, task_b, lru_score_b): &(TaskSourceKind, ResolvedTask, u32),
423) -> cmp::Ordering {
424 lru_score_a
425 // First, display recently used templates above all.
426 .cmp(lru_score_b)
427 // Then, ensure more specific sources are displayed first.
428 .then(task_source_kind_preference(kind_a).cmp(&task_source_kind_preference(kind_b)))
429 // After that, display first more specific tasks, using more template variables.
430 // Bonus points for tasks with symbol variables.
431 .then(task_variables_preference(task_a).cmp(&task_variables_preference(task_b)))
432 // Finally, sort by the resolved label, but a bit more specifically, to avoid mixing letters and digits.
433 .then({
434 NumericPrefixWithSuffix::from_numeric_prefixed_str(&task_a.resolved_label)
435 .cmp(&NumericPrefixWithSuffix::from_numeric_prefixed_str(
436 &task_b.resolved_label,
437 ))
438 .then(task_a.resolved_label.cmp(&task_b.resolved_label))
439 .then(kind_a.cmp(kind_b))
440 })
441}
442
443fn task_source_kind_preference(kind: &TaskSourceKind) -> u32 {
444 match kind {
445 TaskSourceKind::Language { .. } => 1,
446 TaskSourceKind::UserInput => 2,
447 TaskSourceKind::Worktree { .. } => 3,
448 TaskSourceKind::AbsPath { .. } => 4,
449 }
450}
451
452fn task_variables_preference(task: &ResolvedTask) -> Reverse<usize> {
453 let task_variables = task.substituted_variables();
454 Reverse(if task_variables.contains(&VariableName::Symbol) {
455 task_variables.len() + 1
456 } else {
457 task_variables.len()
458 })
459}
460
461#[cfg(test)]
462mod test_inventory {
463 use gpui::{Entity, TestAppContext};
464 use itertools::Itertools;
465 use task::TaskContext;
466 use worktree::WorktreeId;
467
468 use crate::Inventory;
469
470 use super::TaskSourceKind;
471
472 pub(super) fn task_template_names(
473 inventory: &Entity<Inventory>,
474 worktree: Option<WorktreeId>,
475 cx: &mut TestAppContext,
476 ) -> Vec<String> {
477 inventory.update(cx, |inventory, cx| {
478 inventory
479 .list_tasks(None, None, worktree, cx)
480 .into_iter()
481 .map(|(_, task)| task.label)
482 .sorted()
483 .collect()
484 })
485 }
486
487 pub(super) fn register_task_used(
488 inventory: &Entity<Inventory>,
489 task_name: &str,
490 cx: &mut TestAppContext,
491 ) {
492 inventory.update(cx, |inventory, cx| {
493 let (task_source_kind, task) = inventory
494 .list_tasks(None, None, None, cx)
495 .into_iter()
496 .find(|(_, task)| task.label == task_name)
497 .unwrap_or_else(|| panic!("Failed to find task with name {task_name}"));
498 let id_base = task_source_kind.to_id_base();
499 inventory.task_scheduled(
500 task_source_kind.clone(),
501 task.resolve_task(&id_base, &TaskContext::default())
502 .unwrap_or_else(|| panic!("Failed to resolve task with name {task_name}")),
503 );
504 });
505 }
506
507 pub(super) async fn list_tasks(
508 inventory: &Entity<Inventory>,
509 worktree: Option<WorktreeId>,
510 cx: &mut TestAppContext,
511 ) -> Vec<(TaskSourceKind, String)> {
512 inventory.update(cx, |inventory, cx| {
513 let task_context = &TaskContext::default();
514 inventory
515 .list_tasks(None, None, worktree, cx)
516 .into_iter()
517 .filter_map(|(source_kind, task)| {
518 let id_base = source_kind.to_id_base();
519 Some((source_kind, task.resolve_task(&id_base, task_context)?))
520 })
521 .map(|(source_kind, resolved_task)| (source_kind, resolved_task.resolved_label))
522 .collect()
523 })
524 }
525}
526
527/// A context provided that tries to provide values for all non-custom [`VariableName`] variants for a currently opened file.
528/// Applied as a base for every custom [`ContextProvider`] unless explicitly oped out.
529pub struct BasicContextProvider {
530 worktree_store: Entity<WorktreeStore>,
531}
532
533impl BasicContextProvider {
534 pub fn new(worktree_store: Entity<WorktreeStore>) -> Self {
535 Self { worktree_store }
536 }
537}
538impl ContextProvider for BasicContextProvider {
539 fn build_context(
540 &self,
541 _: &TaskVariables,
542 location: &Location,
543 _: Option<HashMap<String, String>>,
544 _: Arc<dyn LanguageToolchainStore>,
545 cx: &mut App,
546 ) -> Task<Result<TaskVariables>> {
547 let buffer = location.buffer.read(cx);
548 let buffer_snapshot = buffer.snapshot();
549 let symbols = buffer_snapshot.symbols_containing(location.range.start, None);
550 let symbol = symbols.unwrap_or_default().last().map(|symbol| {
551 let range = symbol
552 .name_ranges
553 .last()
554 .cloned()
555 .unwrap_or(0..symbol.text.len());
556 symbol.text[range].to_string()
557 });
558
559 let current_file = buffer
560 .file()
561 .and_then(|file| file.as_local())
562 .map(|file| file.abs_path(cx).to_sanitized_string());
563 let Point { row, column } = location.range.start.to_point(&buffer_snapshot);
564 let row = row + 1;
565 let column = column + 1;
566 let selected_text = buffer
567 .chars_for_range(location.range.clone())
568 .collect::<String>();
569
570 let mut task_variables = TaskVariables::from_iter([
571 (VariableName::Row, row.to_string()),
572 (VariableName::Column, column.to_string()),
573 ]);
574
575 if let Some(symbol) = symbol {
576 task_variables.insert(VariableName::Symbol, symbol);
577 }
578 if !selected_text.trim().is_empty() {
579 task_variables.insert(VariableName::SelectedText, selected_text);
580 }
581 let worktree_root_dir =
582 buffer
583 .file()
584 .map(|file| file.worktree_id(cx))
585 .and_then(|worktree_id| {
586 self.worktree_store
587 .read(cx)
588 .worktree_for_id(worktree_id, cx)
589 .and_then(|worktree| worktree.read(cx).root_dir())
590 });
591 if let Some(worktree_path) = worktree_root_dir {
592 task_variables.insert(
593 VariableName::WorktreeRoot,
594 worktree_path.to_sanitized_string(),
595 );
596 if let Some(full_path) = current_file.as_ref() {
597 let relative_path = pathdiff::diff_paths(full_path, worktree_path);
598 if let Some(relative_path) = relative_path {
599 task_variables.insert(
600 VariableName::RelativeFile,
601 relative_path.to_sanitized_string(),
602 );
603 }
604 }
605 }
606
607 if let Some(path_as_string) = current_file {
608 let path = Path::new(&path_as_string);
609 if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
610 task_variables.insert(VariableName::Filename, String::from(filename));
611 }
612
613 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
614 task_variables.insert(VariableName::Stem, stem.into());
615 }
616
617 if let Some(dirname) = path.parent().and_then(|s| s.to_str()) {
618 task_variables.insert(VariableName::Dirname, dirname.into());
619 }
620
621 task_variables.insert(VariableName::File, path_as_string);
622 }
623
624 Task::ready(Ok(task_variables))
625 }
626}
627
628/// A ContextProvider that doesn't provide any task variables on it's own, though it has some associated tasks.
629pub struct ContextProviderWithTasks {
630 templates: TaskTemplates,
631}
632
633impl ContextProviderWithTasks {
634 pub fn new(definitions: TaskTemplates) -> Self {
635 Self {
636 templates: definitions,
637 }
638 }
639}
640
641impl ContextProvider for ContextProviderWithTasks {
642 fn associated_tasks(
643 &self,
644 _: Option<Arc<dyn language::File>>,
645 _: &App,
646 ) -> Option<TaskTemplates> {
647 Some(self.templates.clone())
648 }
649}
650
651#[cfg(test)]
652mod tests {
653 use gpui::TestAppContext;
654 use pretty_assertions::assert_eq;
655 use serde_json::json;
656
657 use crate::task_store::TaskStore;
658
659 use super::test_inventory::*;
660 use super::*;
661
662 #[gpui::test]
663 async fn test_task_list_sorting(cx: &mut TestAppContext) {
664 init_test(cx);
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 cx.run_until_parked();
677 let expected_initial_state = [
678 "1_a_task".to_string(),
679 "1_task".to_string(),
680 "2_task".to_string(),
681 "3_task".to_string(),
682 ];
683
684 inventory.update(cx, |inventory, _| {
685 inventory
686 .update_file_based_tasks(
687 None,
688 Some(&mock_tasks_from_names(
689 expected_initial_state.iter().map(|name| name.as_str()),
690 )),
691 settings::TaskKind::Script,
692 )
693 .unwrap();
694 });
695 assert_eq!(
696 task_template_names(&inventory, None, cx),
697 &expected_initial_state,
698 );
699 assert_eq!(
700 resolved_task_names(&inventory, None, cx).await,
701 &expected_initial_state,
702 "Tasks with equal amount of usages should be sorted alphanumerically"
703 );
704
705 register_task_used(&inventory, "2_task", cx);
706 assert_eq!(
707 task_template_names(&inventory, None, cx),
708 &expected_initial_state,
709 );
710 assert_eq!(
711 resolved_task_names(&inventory, None, cx).await,
712 vec![
713 "2_task".to_string(),
714 "1_a_task".to_string(),
715 "1_task".to_string(),
716 "3_task".to_string()
717 ],
718 );
719
720 register_task_used(&inventory, "1_task", cx);
721 register_task_used(&inventory, "1_task", cx);
722 register_task_used(&inventory, "1_task", cx);
723 register_task_used(&inventory, "3_task", cx);
724 assert_eq!(
725 task_template_names(&inventory, None, cx),
726 &expected_initial_state,
727 );
728 assert_eq!(
729 resolved_task_names(&inventory, None, cx).await,
730 vec![
731 "3_task".to_string(),
732 "1_task".to_string(),
733 "2_task".to_string(),
734 "1_a_task".to_string(),
735 ],
736 );
737
738 inventory.update(cx, |inventory, _| {
739 inventory
740 .update_file_based_tasks(
741 None,
742 Some(&mock_tasks_from_names(
743 ["10_hello", "11_hello"]
744 .into_iter()
745 .chain(expected_initial_state.iter().map(|name| name.as_str())),
746 )),
747 settings::TaskKind::Script,
748 )
749 .unwrap();
750 });
751 cx.run_until_parked();
752 let expected_updated_state = [
753 "10_hello".to_string(),
754 "11_hello".to_string(),
755 "1_a_task".to_string(),
756 "1_task".to_string(),
757 "2_task".to_string(),
758 "3_task".to_string(),
759 ];
760 assert_eq!(
761 task_template_names(&inventory, None, cx),
762 &expected_updated_state,
763 );
764 assert_eq!(
765 resolved_task_names(&inventory, None, cx).await,
766 vec![
767 "3_task".to_string(),
768 "1_task".to_string(),
769 "2_task".to_string(),
770 "1_a_task".to_string(),
771 "10_hello".to_string(),
772 "11_hello".to_string(),
773 ],
774 );
775
776 register_task_used(&inventory, "11_hello", cx);
777 assert_eq!(
778 task_template_names(&inventory, None, cx),
779 &expected_updated_state,
780 );
781 assert_eq!(
782 resolved_task_names(&inventory, None, cx).await,
783 vec![
784 "11_hello".to_string(),
785 "3_task".to_string(),
786 "1_task".to_string(),
787 "2_task".to_string(),
788 "1_a_task".to_string(),
789 "10_hello".to_string(),
790 ],
791 );
792 }
793
794 #[gpui::test]
795 async fn test_inventory_static_task_filters(cx: &mut TestAppContext) {
796 init_test(cx);
797 let inventory = cx.update(Inventory::new);
798 let common_name = "common_task_name";
799 let worktree_1 = WorktreeId::from_usize(1);
800 let worktree_2 = WorktreeId::from_usize(2);
801
802 cx.run_until_parked();
803 let worktree_independent_tasks = vec![
804 (
805 TaskSourceKind::AbsPath {
806 id_base: "global tasks.json".into(),
807 abs_path: paths::tasks_file().clone(),
808 },
809 common_name.to_string(),
810 ),
811 (
812 TaskSourceKind::AbsPath {
813 id_base: "global tasks.json".into(),
814 abs_path: paths::tasks_file().clone(),
815 },
816 "static_source_1".to_string(),
817 ),
818 (
819 TaskSourceKind::AbsPath {
820 id_base: "global tasks.json".into(),
821 abs_path: paths::tasks_file().clone(),
822 },
823 "static_source_2".to_string(),
824 ),
825 ];
826 let worktree_1_tasks = [
827 (
828 TaskSourceKind::Worktree {
829 id: worktree_1,
830 directory_in_worktree: PathBuf::from(".zed"),
831 id_base: "local worktree tasks from directory \".zed\"".into(),
832 },
833 common_name.to_string(),
834 ),
835 (
836 TaskSourceKind::Worktree {
837 id: worktree_1,
838 directory_in_worktree: PathBuf::from(".zed"),
839 id_base: "local worktree tasks from directory \".zed\"".into(),
840 },
841 "worktree_1".to_string(),
842 ),
843 ];
844 let worktree_2_tasks = [
845 (
846 TaskSourceKind::Worktree {
847 id: worktree_2,
848 directory_in_worktree: PathBuf::from(".zed"),
849 id_base: "local worktree tasks from directory \".zed\"".into(),
850 },
851 common_name.to_string(),
852 ),
853 (
854 TaskSourceKind::Worktree {
855 id: worktree_2,
856 directory_in_worktree: PathBuf::from(".zed"),
857 id_base: "local worktree tasks from directory \".zed\"".into(),
858 },
859 "worktree_2".to_string(),
860 ),
861 ];
862
863 inventory.update(cx, |inventory, _| {
864 inventory
865 .update_file_based_tasks(
866 None,
867 Some(&mock_tasks_from_names(
868 worktree_independent_tasks
869 .iter()
870 .map(|(_, name)| name.as_str()),
871 )),
872 settings::TaskKind::Script,
873 )
874 .unwrap();
875 inventory
876 .update_file_based_tasks(
877 Some(SettingsLocation {
878 worktree_id: worktree_1,
879 path: Path::new(".zed"),
880 }),
881 Some(&mock_tasks_from_names(
882 worktree_1_tasks.iter().map(|(_, name)| name.as_str()),
883 )),
884 settings::TaskKind::Script,
885 )
886 .unwrap();
887 inventory
888 .update_file_based_tasks(
889 Some(SettingsLocation {
890 worktree_id: worktree_2,
891 path: Path::new(".zed"),
892 }),
893 Some(&mock_tasks_from_names(
894 worktree_2_tasks.iter().map(|(_, name)| name.as_str()),
895 )),
896 settings::TaskKind::Script,
897 )
898 .unwrap();
899 });
900
901 assert_eq!(
902 list_tasks_sorted_by_last_used(&inventory, None, cx).await,
903 worktree_independent_tasks,
904 "Without a worktree, only worktree-independent tasks should be listed"
905 );
906 assert_eq!(
907 list_tasks_sorted_by_last_used(&inventory, Some(worktree_1), cx).await,
908 worktree_1_tasks
909 .iter()
910 .chain(worktree_independent_tasks.iter())
911 .cloned()
912 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
913 .collect::<Vec<_>>(),
914 );
915 assert_eq!(
916 list_tasks_sorted_by_last_used(&inventory, Some(worktree_2), cx).await,
917 worktree_2_tasks
918 .iter()
919 .chain(worktree_independent_tasks.iter())
920 .cloned()
921 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
922 .collect::<Vec<_>>(),
923 );
924
925 assert_eq!(
926 list_tasks(&inventory, None, cx).await,
927 worktree_independent_tasks,
928 "Without a worktree, only worktree-independent tasks should be listed"
929 );
930 assert_eq!(
931 list_tasks(&inventory, Some(worktree_1), cx).await,
932 worktree_1_tasks
933 .iter()
934 .chain(worktree_independent_tasks.iter())
935 .cloned()
936 .collect::<Vec<_>>(),
937 );
938 assert_eq!(
939 list_tasks(&inventory, Some(worktree_2), cx).await,
940 worktree_2_tasks
941 .iter()
942 .chain(worktree_independent_tasks.iter())
943 .cloned()
944 .collect::<Vec<_>>(),
945 );
946 }
947
948 fn init_test(_cx: &mut TestAppContext) {
949 if std::env::var("RUST_LOG").is_ok() {
950 env_logger::try_init().ok();
951 }
952 TaskStore::init(None);
953 }
954
955 async fn resolved_task_names(
956 inventory: &Entity<Inventory>,
957 worktree: Option<WorktreeId>,
958 cx: &mut TestAppContext,
959 ) -> Vec<String> {
960 let (used, current) = inventory.update(cx, |inventory, cx| {
961 let mut task_contexts = TaskContexts::default();
962 task_contexts.active_worktree_context =
963 worktree.map(|worktree| (worktree, TaskContext::default()));
964 inventory.used_and_current_resolved_tasks(&task_contexts, cx)
965 });
966 used.into_iter()
967 .chain(current)
968 .map(|(_, task)| task.original_task().label.clone())
969 .collect()
970 }
971
972 fn mock_tasks_from_names<'a>(task_names: impl Iterator<Item = &'a str> + 'a) -> String {
973 serde_json::to_string(&serde_json::Value::Array(
974 task_names
975 .map(|task_name| {
976 json!({
977 "label": task_name,
978 "command": "echo",
979 "args": vec![task_name],
980 })
981 })
982 .collect::<Vec<_>>(),
983 ))
984 .unwrap()
985 }
986
987 async fn list_tasks_sorted_by_last_used(
988 inventory: &Entity<Inventory>,
989 worktree: Option<WorktreeId>,
990 cx: &mut TestAppContext,
991 ) -> Vec<(TaskSourceKind, String)> {
992 let (used, current) = inventory.update(cx, |inventory, cx| {
993 let mut task_contexts = TaskContexts::default();
994 task_contexts.active_worktree_context =
995 worktree.map(|worktree| (worktree, TaskContext::default()));
996 inventory.used_and_current_resolved_tasks(&task_contexts, cx)
997 });
998 let mut all = used;
999 all.extend(current);
1000 all.into_iter()
1001 .map(|(source_kind, task)| (source_kind, task.resolved_label))
1002 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
1003 .collect()
1004 }
1005}