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::Result;
12use collections::{HashMap, HashSet, VecDeque};
13use dap::DapRegistry;
14use fs::Fs;
15use gpui::{App, AppContext as _, Context, Entity, SharedString, Task};
16use itertools::Itertools;
17use language::{
18 Buffer, ContextLocation, ContextProvider, File, Language, LanguageToolchainStore, Location,
19 language_settings::language_settings,
20};
21use lsp::{LanguageServerId, LanguageServerName};
22use paths::{debug_task_file_name, task_file_name};
23use settings::{InvalidSettingsError, parse_json_with_comments};
24use task::{
25 DebugScenario, ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskTemplates, TaskVariables,
26 VariableName,
27};
28use text::{BufferId, Point, ToPoint};
29use util::{NumericPrefixWithSuffix, ResultExt as _, paths::PathExt as _, post_inc};
30use worktree::WorktreeId;
31
32use crate::{task_store::TaskSettingsLocation, worktree_store::WorktreeStore};
33
34/// Inventory tracks available tasks for a given project.
35pub struct Inventory {
36 fs: Arc<dyn Fs>,
37 last_scheduled_tasks: VecDeque<(TaskSourceKind, ResolvedTask)>,
38 last_scheduled_scenarios: VecDeque<DebugScenario>,
39 templates_from_settings: InventoryFor<TaskTemplate>,
40 scenarios_from_settings: InventoryFor<DebugScenario>,
41}
42
43impl std::fmt::Debug for Inventory {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 f.debug_struct("Inventory")
46 .field("last_scheduled_tasks", &self.last_scheduled_tasks)
47 .field("last_scheduled_scenarios", &self.last_scheduled_scenarios)
48 .field("templates_from_settings", &self.templates_from_settings)
49 .field("scenarios_from_settings", &self.scenarios_from_settings)
50 .finish()
51 }
52}
53
54// Helper trait for better error messages in [InventoryFor]
55trait InventoryContents: Clone {
56 const GLOBAL_SOURCE_FILE: &'static str;
57 const LABEL: &'static str;
58}
59
60impl InventoryContents for TaskTemplate {
61 const GLOBAL_SOURCE_FILE: &'static str = "tasks.json";
62 const LABEL: &'static str = "tasks";
63}
64
65impl InventoryContents for DebugScenario {
66 const GLOBAL_SOURCE_FILE: &'static str = "debug.json";
67
68 const LABEL: &'static str = "debug scenarios";
69}
70
71#[derive(Debug)]
72struct InventoryFor<T> {
73 global: HashMap<PathBuf, Vec<T>>,
74 worktree: HashMap<WorktreeId, HashMap<Arc<Path>, Vec<T>>>,
75}
76
77impl<T: InventoryContents> InventoryFor<T> {
78 fn worktree_scenarios(
79 &self,
80 worktree: WorktreeId,
81 ) -> impl '_ + Iterator<Item = (TaskSourceKind, T)> {
82 self.worktree
83 .get(&worktree)
84 .into_iter()
85 .flatten()
86 .flat_map(|(directory, templates)| {
87 templates.iter().map(move |template| (directory, template))
88 })
89 .map(move |(directory, template)| {
90 (
91 TaskSourceKind::Worktree {
92 id: worktree,
93 directory_in_worktree: directory.to_path_buf(),
94 id_base: Cow::Owned(format!(
95 "local worktree {} from directory {directory:?}",
96 T::LABEL
97 )),
98 },
99 template.clone(),
100 )
101 })
102 }
103
104 fn global_scenarios(&self) -> impl '_ + Iterator<Item = (TaskSourceKind, T)> {
105 self.global.iter().flat_map(|(file_path, templates)| {
106 templates.into_iter().map(|template| {
107 (
108 TaskSourceKind::AbsPath {
109 id_base: Cow::Owned(format!("global {}", T::GLOBAL_SOURCE_FILE)),
110 abs_path: file_path.clone(),
111 },
112 template.clone(),
113 )
114 })
115 })
116 }
117}
118
119impl<T> Default for InventoryFor<T> {
120 fn default() -> Self {
121 Self {
122 global: HashMap::default(),
123 worktree: HashMap::default(),
124 }
125 }
126}
127
128/// Kind of a source the tasks are fetched from, used to display more source information in the UI.
129#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
130pub enum TaskSourceKind {
131 /// bash-like commands spawned by users, not associated with any path
132 UserInput,
133 /// Tasks from the worktree's .zed/task.json
134 Worktree {
135 id: WorktreeId,
136 directory_in_worktree: PathBuf,
137 id_base: Cow<'static, str>,
138 },
139 /// ~/.config/zed/task.json - like global files with task definitions, applicable to any path
140 AbsPath {
141 id_base: Cow<'static, str>,
142 abs_path: PathBuf,
143 },
144 /// Languages-specific tasks coming from extensions.
145 Language { name: SharedString },
146 /// Language-specific tasks coming from LSP servers.
147 Lsp {
148 language_name: SharedString,
149 server: LanguageServerId,
150 },
151}
152
153/// A collection of task contexts, derived from the current state of the workspace.
154/// Only contains worktrees that are visible and with their root being a directory.
155#[derive(Debug, Default)]
156pub struct TaskContexts {
157 /// A context, related to the currently opened item.
158 /// Item can be opened from an invisible worktree, or any other, not necessarily active worktree.
159 pub active_item_context: Option<(Option<WorktreeId>, Option<Location>, TaskContext)>,
160 /// A worktree that corresponds to the active item, or the only worktree in the workspace.
161 pub active_worktree_context: Option<(WorktreeId, TaskContext)>,
162 /// If there are multiple worktrees in the workspace, all non-active ones are included here.
163 pub other_worktree_contexts: Vec<(WorktreeId, TaskContext)>,
164 pub lsp_task_sources: HashMap<LanguageServerName, Vec<BufferId>>,
165 pub latest_selection: Option<text::Anchor>,
166}
167
168impl TaskContexts {
169 pub fn active_context(&self) -> Option<&TaskContext> {
170 self.active_item_context
171 .as_ref()
172 .map(|(_, _, context)| context)
173 .or_else(|| {
174 self.active_worktree_context
175 .as_ref()
176 .map(|(_, context)| context)
177 })
178 }
179
180 pub fn location(&self) -> Option<&Location> {
181 self.active_item_context
182 .as_ref()
183 .and_then(|(_, location, _)| location.as_ref())
184 }
185
186 pub fn file(&self, cx: &App) -> Option<Arc<dyn File>> {
187 self.active_item_context
188 .as_ref()
189 .and_then(|(_, location, _)| location.as_ref())
190 .and_then(|location| location.buffer.read(cx).file().cloned())
191 }
192
193 pub fn worktree(&self) -> Option<WorktreeId> {
194 self.active_item_context
195 .as_ref()
196 .and_then(|(worktree_id, _, _)| worktree_id.as_ref())
197 .or_else(|| {
198 self.active_worktree_context
199 .as_ref()
200 .map(|(worktree_id, _)| worktree_id)
201 })
202 .copied()
203 }
204
205 pub fn task_context_for_worktree_id(&self, worktree_id: WorktreeId) -> Option<&TaskContext> {
206 self.active_worktree_context
207 .iter()
208 .chain(self.other_worktree_contexts.iter())
209 .find(|(id, _)| *id == worktree_id)
210 .map(|(_, context)| context)
211 }
212}
213
214impl TaskSourceKind {
215 pub fn to_id_base(&self) -> String {
216 match self {
217 Self::UserInput => "oneshot".to_string(),
218 Self::AbsPath { id_base, abs_path } => {
219 format!("{id_base}_{}", abs_path.display())
220 }
221 Self::Worktree {
222 id,
223 id_base,
224 directory_in_worktree,
225 } => {
226 format!("{id_base}_{id}_{}", directory_in_worktree.display())
227 }
228 Self::Language { name } => format!("language_{name}"),
229 Self::Lsp {
230 server,
231 language_name,
232 } => format!("lsp_{language_name}_{server}"),
233 }
234 }
235}
236
237impl Inventory {
238 pub fn new(fs: Arc<dyn Fs>, cx: &mut App) -> Entity<Self> {
239 cx.new(|_| Self {
240 fs,
241 last_scheduled_tasks: VecDeque::default(),
242 last_scheduled_scenarios: VecDeque::default(),
243 templates_from_settings: InventoryFor::default(),
244 scenarios_from_settings: InventoryFor::default(),
245 })
246 }
247
248 pub fn scenario_scheduled(&mut self, scenario: DebugScenario) {
249 self.last_scheduled_scenarios
250 .retain(|s| s.label != scenario.label);
251 self.last_scheduled_scenarios.push_back(scenario);
252 if self.last_scheduled_scenarios.len() > 5_000 {
253 self.last_scheduled_scenarios.pop_front();
254 }
255 }
256
257 pub fn last_scheduled_scenario(&self) -> Option<&DebugScenario> {
258 self.last_scheduled_scenarios.back()
259 }
260
261 pub fn list_debug_scenarios(
262 &self,
263 task_contexts: &TaskContexts,
264 lsp_tasks: Vec<(TaskSourceKind, task::ResolvedTask)>,
265 current_resolved_tasks: Vec<(TaskSourceKind, task::ResolvedTask)>,
266 add_current_language_tasks: bool,
267 cx: &mut App,
268 ) -> (Vec<DebugScenario>, Vec<(TaskSourceKind, DebugScenario)>) {
269 let mut scenarios = Vec::new();
270
271 if let Some(worktree_id) = task_contexts
272 .active_worktree_context
273 .iter()
274 .chain(task_contexts.other_worktree_contexts.iter())
275 .map(|context| context.0)
276 .next()
277 {
278 scenarios.extend(self.worktree_scenarios_from_settings(worktree_id));
279 }
280 scenarios.extend(self.global_debug_scenarios_from_settings());
281
282 if let Some(location) = task_contexts.location() {
283 let file = location.buffer.read(cx).file();
284 let language = location.buffer.read(cx).language();
285 let language_name = language.as_ref().map(|l| l.name());
286 let adapter = language_settings(language_name, file, cx)
287 .debuggers
288 .first()
289 .map(SharedString::from)
290 .or_else(|| {
291 language.and_then(|l| l.config().debuggers.first().map(SharedString::from))
292 });
293 if let Some(adapter) = adapter {
294 for (kind, task) in
295 lsp_tasks
296 .into_iter()
297 .chain(current_resolved_tasks.into_iter().filter(|(kind, _)| {
298 add_current_language_tasks
299 || !matches!(kind, TaskSourceKind::Language { .. })
300 }))
301 {
302 if let Some(scenario) =
303 DapRegistry::global(cx)
304 .locators()
305 .values()
306 .find_map(|locator| {
307 locator.create_scenario(
308 &task.original_task().clone(),
309 &task.display_label(),
310 adapter.clone().into(),
311 )
312 })
313 {
314 scenarios.push((kind, scenario));
315 }
316 }
317 }
318 }
319
320 (
321 self.last_scheduled_scenarios.iter().cloned().collect(),
322 scenarios,
323 )
324 }
325
326 pub fn task_template_by_label(
327 &self,
328 buffer: Option<Entity<Buffer>>,
329 worktree_id: Option<WorktreeId>,
330 label: &str,
331 cx: &App,
332 ) -> Task<Option<TaskTemplate>> {
333 let (buffer_worktree_id, file, language) = buffer
334 .map(|buffer| {
335 let buffer = buffer.read(cx);
336 let file = buffer.file().cloned();
337 (
338 file.as_ref().map(|file| file.worktree_id(cx)),
339 file,
340 buffer.language().cloned(),
341 )
342 })
343 .unwrap_or((None, None, None));
344
345 let tasks = self.list_tasks(file, language, worktree_id.or(buffer_worktree_id), cx);
346 let label = label.to_owned();
347 cx.background_spawn(async move {
348 tasks
349 .await
350 .into_iter()
351 .find(|(_, template)| template.label == label)
352 .map(|val| val.1)
353 })
354 }
355
356 /// Pulls its task sources relevant to the worktree and the language given,
357 /// returns all task templates with their source kinds, worktree tasks first, language tasks second
358 /// and global tasks last. No specific order inside source kinds groups.
359 pub fn list_tasks(
360 &self,
361 file: Option<Arc<dyn File>>,
362 language: Option<Arc<Language>>,
363 worktree: Option<WorktreeId>,
364 cx: &App,
365 ) -> Task<Vec<(TaskSourceKind, TaskTemplate)>> {
366 let global_tasks = self.global_templates_from_settings().collect::<Vec<_>>();
367 let fs = self.fs.clone();
368 let mut worktree_tasks = worktree
369 .into_iter()
370 .flat_map(|worktree| self.worktree_templates_from_settings(worktree))
371 .collect::<Vec<_>>();
372 let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
373 name: language.name().into(),
374 });
375 let language_tasks = language
376 .filter(|language| {
377 language_settings(Some(language.name()), file.as_ref(), cx)
378 .tasks
379 .enabled
380 })
381 .and_then(|language| {
382 language
383 .context_provider()
384 .map(|provider| provider.associated_tasks(fs, file, cx))
385 });
386 cx.background_spawn(async move {
387 if let Some(t) = language_tasks {
388 worktree_tasks.extend(t.await.into_iter().flat_map(|tasks| {
389 tasks
390 .0
391 .into_iter()
392 .filter_map(|task| Some((task_source_kind.clone()?, task)))
393 }));
394 }
395 worktree_tasks.extend(global_tasks);
396 worktree_tasks
397 })
398 }
399
400 /// Pulls its task sources relevant to the worktree and the language given and resolves them with the [`TaskContexts`] given.
401 /// Joins the new resolutions with the resolved tasks that were used (spawned) before,
402 /// orders them so that the most recently used come first, all equally used ones are ordered so that the most specific tasks come first.
403 /// Deduplicates the tasks by their labels and context and splits the ordered list into two: used tasks and the rest, newly resolved tasks.
404 pub fn used_and_current_resolved_tasks(
405 &self,
406 task_contexts: Arc<TaskContexts>,
407 cx: &mut Context<Self>,
408 ) -> Task<(
409 Vec<(TaskSourceKind, ResolvedTask)>,
410 Vec<(TaskSourceKind, ResolvedTask)>,
411 )> {
412 let fs = self.fs.clone();
413 let worktree = task_contexts.worktree();
414 let location = task_contexts.location();
415 let language = location
416 .and_then(|location| location.buffer.read(cx).language_at(location.range.start));
417 let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
418 name: language.name().into(),
419 });
420 let file = location.and_then(|location| location.buffer.read(cx).file().cloned());
421
422 let mut task_labels_to_ids = HashMap::<String, HashSet<TaskId>>::default();
423 let mut lru_score = 0_u32;
424 let previously_spawned_tasks = self
425 .last_scheduled_tasks
426 .iter()
427 .rev()
428 .filter(|(task_kind, _)| {
429 if matches!(task_kind, TaskSourceKind::Language { .. }) {
430 Some(task_kind) == task_source_kind.as_ref()
431 } else {
432 true
433 }
434 })
435 .filter(|(_, resolved_task)| {
436 match task_labels_to_ids.entry(resolved_task.resolved_label.clone()) {
437 hash_map::Entry::Occupied(mut o) => {
438 o.get_mut().insert(resolved_task.id.clone());
439 // Neber allow duplicate reused tasks with the same labels
440 false
441 }
442 hash_map::Entry::Vacant(v) => {
443 v.insert(HashSet::from_iter(Some(resolved_task.id.clone())));
444 true
445 }
446 }
447 })
448 .map(|(task_source_kind, resolved_task)| {
449 (
450 task_source_kind.clone(),
451 resolved_task.clone(),
452 post_inc(&mut lru_score),
453 )
454 })
455 .sorted_unstable_by(task_lru_comparator)
456 .map(|(kind, task, _)| (kind, task))
457 .collect::<Vec<_>>();
458
459 let not_used_score = post_inc(&mut lru_score);
460 let global_tasks = self.global_templates_from_settings().collect::<Vec<_>>();
461 let associated_tasks = language
462 .filter(|language| {
463 language_settings(Some(language.name()), file.as_ref(), cx)
464 .tasks
465 .enabled
466 })
467 .and_then(|language| {
468 language
469 .context_provider()
470 .map(|provider| provider.associated_tasks(fs, file, cx))
471 });
472 let worktree_tasks = worktree
473 .into_iter()
474 .flat_map(|worktree| self.worktree_templates_from_settings(worktree))
475 .collect::<Vec<_>>();
476 let task_contexts = task_contexts.clone();
477 cx.background_spawn(async move {
478 let language_tasks = if let Some(task) = associated_tasks {
479 task.await.map(|templates| {
480 templates
481 .0
482 .into_iter()
483 .flat_map(|task| Some((task_source_kind.clone()?, task)))
484 })
485 } else {
486 None
487 };
488
489 let worktree_tasks = worktree_tasks
490 .into_iter()
491 .chain(language_tasks.into_iter().flatten())
492 .chain(global_tasks);
493
494 let new_resolved_tasks = worktree_tasks
495 .flat_map(|(kind, task)| {
496 let id_base = kind.to_id_base();
497 if let TaskSourceKind::Worktree { id, .. } = &kind {
498 None.or_else(|| {
499 let (_, _, item_context) =
500 task_contexts.active_item_context.as_ref().filter(
501 |(worktree_id, _, _)| Some(id) == worktree_id.as_ref(),
502 )?;
503 task.resolve_task(&id_base, item_context)
504 })
505 .or_else(|| {
506 let (_, worktree_context) = task_contexts
507 .active_worktree_context
508 .as_ref()
509 .filter(|(worktree_id, _)| id == worktree_id)?;
510 task.resolve_task(&id_base, worktree_context)
511 })
512 .or_else(|| {
513 if let TaskSourceKind::Worktree { id, .. } = &kind {
514 let worktree_context = task_contexts
515 .other_worktree_contexts
516 .iter()
517 .find(|(worktree_id, _)| worktree_id == id)
518 .map(|(_, context)| context)?;
519 task.resolve_task(&id_base, worktree_context)
520 } else {
521 None
522 }
523 })
524 } else {
525 None.or_else(|| {
526 let (_, _, item_context) =
527 task_contexts.active_item_context.as_ref()?;
528 task.resolve_task(&id_base, item_context)
529 })
530 .or_else(|| {
531 let (_, worktree_context) =
532 task_contexts.active_worktree_context.as_ref()?;
533 task.resolve_task(&id_base, worktree_context)
534 })
535 }
536 .or_else(|| task.resolve_task(&id_base, &TaskContext::default()))
537 .map(move |resolved_task| (kind.clone(), resolved_task, not_used_score))
538 })
539 .filter(|(_, resolved_task, _)| {
540 match task_labels_to_ids.entry(resolved_task.resolved_label.clone()) {
541 hash_map::Entry::Occupied(mut o) => {
542 // Allow new tasks with the same label, if their context is different
543 o.get_mut().insert(resolved_task.id.clone())
544 }
545 hash_map::Entry::Vacant(v) => {
546 v.insert(HashSet::from_iter(Some(resolved_task.id.clone())));
547 true
548 }
549 }
550 })
551 .sorted_unstable_by(task_lru_comparator)
552 .map(|(kind, task, _)| (kind, task))
553 .collect::<Vec<_>>();
554
555 (previously_spawned_tasks, new_resolved_tasks)
556 })
557 }
558
559 /// Returns the last scheduled task by task_id if provided.
560 /// Otherwise, returns the last scheduled task.
561 pub fn last_scheduled_task(
562 &self,
563 task_id: Option<&TaskId>,
564 ) -> Option<(TaskSourceKind, ResolvedTask)> {
565 if let Some(task_id) = task_id {
566 self.last_scheduled_tasks
567 .iter()
568 .find(|(_, task)| &task.id == task_id)
569 .cloned()
570 } else {
571 self.last_scheduled_tasks.back().cloned()
572 }
573 }
574
575 /// Registers task "usage" as being scheduled – to be used for LRU sorting when listing all tasks.
576 pub fn task_scheduled(
577 &mut self,
578 task_source_kind: TaskSourceKind,
579 resolved_task: ResolvedTask,
580 ) {
581 self.last_scheduled_tasks
582 .push_back((task_source_kind, resolved_task));
583 if self.last_scheduled_tasks.len() > 5_000 {
584 self.last_scheduled_tasks.pop_front();
585 }
586 }
587
588 /// Deletes a resolved task from history, using its id.
589 /// A similar may still resurface in `used_and_current_resolved_tasks` when its [`TaskTemplate`] is resolved again.
590 pub fn delete_previously_used(&mut self, id: &TaskId) {
591 self.last_scheduled_tasks.retain(|(_, task)| &task.id != id);
592 }
593
594 fn global_templates_from_settings(
595 &self,
596 ) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
597 self.templates_from_settings.global_scenarios()
598 }
599
600 fn global_debug_scenarios_from_settings(
601 &self,
602 ) -> impl '_ + Iterator<Item = (TaskSourceKind, DebugScenario)> {
603 self.scenarios_from_settings.global_scenarios()
604 }
605
606 fn worktree_scenarios_from_settings(
607 &self,
608 worktree: WorktreeId,
609 ) -> impl '_ + Iterator<Item = (TaskSourceKind, DebugScenario)> {
610 self.scenarios_from_settings.worktree_scenarios(worktree)
611 }
612
613 fn worktree_templates_from_settings(
614 &self,
615 worktree: WorktreeId,
616 ) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
617 self.templates_from_settings.worktree_scenarios(worktree)
618 }
619
620 /// Updates in-memory task metadata from the JSON string given.
621 /// Will fail if the JSON is not a valid array of objects, but will continue if any object will not parse into a [`TaskTemplate`].
622 ///
623 /// Global tasks are updated for no worktree provided, otherwise the worktree metadata for a given path will be updated.
624 pub(crate) fn update_file_based_tasks(
625 &mut self,
626 location: TaskSettingsLocation<'_>,
627 raw_tasks_json: Option<&str>,
628 ) -> Result<(), InvalidSettingsError> {
629 let raw_tasks = match parse_json_with_comments::<Vec<serde_json::Value>>(
630 raw_tasks_json.unwrap_or("[]"),
631 ) {
632 Ok(tasks) => tasks,
633 Err(e) => {
634 return Err(InvalidSettingsError::Tasks {
635 path: match location {
636 TaskSettingsLocation::Global(path) => path.to_owned(),
637 TaskSettingsLocation::Worktree(settings_location) => {
638 settings_location.path.join(task_file_name())
639 }
640 },
641 message: format!("Failed to parse tasks file content as a JSON array: {e}"),
642 });
643 }
644 };
645 let new_templates = raw_tasks.into_iter().filter_map(|raw_template| {
646 serde_json::from_value::<TaskTemplate>(raw_template).log_err()
647 });
648
649 let parsed_templates = &mut self.templates_from_settings;
650 match location {
651 TaskSettingsLocation::Global(path) => {
652 parsed_templates
653 .global
654 .entry(path.to_owned())
655 .insert_entry(new_templates.collect());
656 self.last_scheduled_tasks.retain(|(kind, _)| {
657 if let TaskSourceKind::AbsPath { abs_path, .. } = kind {
658 abs_path != path
659 } else {
660 true
661 }
662 });
663 }
664 TaskSettingsLocation::Worktree(location) => {
665 let new_templates = new_templates.collect::<Vec<_>>();
666 if new_templates.is_empty() {
667 if let Some(worktree_tasks) =
668 parsed_templates.worktree.get_mut(&location.worktree_id)
669 {
670 worktree_tasks.remove(location.path);
671 }
672 } else {
673 parsed_templates
674 .worktree
675 .entry(location.worktree_id)
676 .or_default()
677 .insert(Arc::from(location.path), new_templates);
678 }
679 self.last_scheduled_tasks.retain(|(kind, _)| {
680 if let TaskSourceKind::Worktree {
681 directory_in_worktree,
682 id,
683 ..
684 } = kind
685 {
686 *id != location.worktree_id || directory_in_worktree != location.path
687 } else {
688 true
689 }
690 });
691 }
692 }
693
694 Ok(())
695 }
696
697 /// Updates in-memory task metadata from the JSON string given.
698 /// Will fail if the JSON is not a valid array of objects, but will continue if any object will not parse into a [`TaskTemplate`].
699 ///
700 /// Global tasks are updated for no worktree provided, otherwise the worktree metadata for a given path will be updated.
701 pub(crate) fn update_file_based_scenarios(
702 &mut self,
703 location: TaskSettingsLocation<'_>,
704 raw_tasks_json: Option<&str>,
705 ) -> Result<(), InvalidSettingsError> {
706 let raw_tasks = match parse_json_with_comments::<Vec<serde_json::Value>>(
707 raw_tasks_json.unwrap_or("[]"),
708 ) {
709 Ok(tasks) => tasks,
710 Err(e) => {
711 return Err(InvalidSettingsError::Debug {
712 path: match location {
713 TaskSettingsLocation::Global(path) => path.to_owned(),
714 TaskSettingsLocation::Worktree(settings_location) => {
715 settings_location.path.join(debug_task_file_name())
716 }
717 },
718 message: format!("Failed to parse tasks file content as a JSON array: {e}"),
719 });
720 }
721 };
722
723 let new_templates = raw_tasks.into_iter().filter_map(|raw_template| {
724 serde_json::from_value::<DebugScenario>(raw_template).log_err()
725 });
726
727 let parsed_scenarios = &mut self.scenarios_from_settings;
728 match location {
729 TaskSettingsLocation::Global(path) => {
730 parsed_scenarios
731 .global
732 .entry(path.to_owned())
733 .insert_entry(new_templates.collect());
734 }
735 TaskSettingsLocation::Worktree(location) => {
736 let new_templates = new_templates.collect::<Vec<_>>();
737 if new_templates.is_empty() {
738 if let Some(worktree_tasks) =
739 parsed_scenarios.worktree.get_mut(&location.worktree_id)
740 {
741 worktree_tasks.remove(location.path);
742 }
743 } else {
744 parsed_scenarios
745 .worktree
746 .entry(location.worktree_id)
747 .or_default()
748 .insert(Arc::from(location.path), new_templates);
749 }
750 }
751 }
752
753 Ok(())
754 }
755}
756
757fn task_lru_comparator(
758 (kind_a, task_a, lru_score_a): &(TaskSourceKind, ResolvedTask, u32),
759 (kind_b, task_b, lru_score_b): &(TaskSourceKind, ResolvedTask, u32),
760) -> cmp::Ordering {
761 lru_score_a
762 // First, display recently used templates above all.
763 .cmp(lru_score_b)
764 // Then, ensure more specific sources are displayed first.
765 .then(task_source_kind_preference(kind_a).cmp(&task_source_kind_preference(kind_b)))
766 // After that, display first more specific tasks, using more template variables.
767 // Bonus points for tasks with symbol variables.
768 .then(task_variables_preference(task_a).cmp(&task_variables_preference(task_b)))
769 // Finally, sort by the resolved label, but a bit more specifically, to avoid mixing letters and digits.
770 .then({
771 NumericPrefixWithSuffix::from_numeric_prefixed_str(&task_a.resolved_label)
772 .cmp(&NumericPrefixWithSuffix::from_numeric_prefixed_str(
773 &task_b.resolved_label,
774 ))
775 .then(task_a.resolved_label.cmp(&task_b.resolved_label))
776 .then(kind_a.cmp(kind_b))
777 })
778}
779
780fn task_source_kind_preference(kind: &TaskSourceKind) -> u32 {
781 match kind {
782 TaskSourceKind::Lsp { .. } => 0,
783 TaskSourceKind::Language { .. } => 1,
784 TaskSourceKind::UserInput => 2,
785 TaskSourceKind::Worktree { .. } => 3,
786 TaskSourceKind::AbsPath { .. } => 4,
787 }
788}
789
790fn task_variables_preference(task: &ResolvedTask) -> Reverse<usize> {
791 let task_variables = task.substituted_variables();
792 Reverse(if task_variables.contains(&VariableName::Symbol) {
793 task_variables.len() + 1
794 } else {
795 task_variables.len()
796 })
797}
798
799#[cfg(test)]
800mod test_inventory {
801 use gpui::{AppContext as _, Entity, Task, TestAppContext};
802 use itertools::Itertools;
803 use task::TaskContext;
804 use worktree::WorktreeId;
805
806 use crate::Inventory;
807
808 use super::TaskSourceKind;
809
810 pub(super) fn task_template_names(
811 inventory: &Entity<Inventory>,
812 worktree: Option<WorktreeId>,
813 cx: &mut TestAppContext,
814 ) -> Task<Vec<String>> {
815 let new_tasks = inventory.update(cx, |inventory, cx| {
816 inventory.list_tasks(None, None, worktree, cx)
817 });
818 cx.background_spawn(async move {
819 new_tasks
820 .await
821 .into_iter()
822 .map(|(_, task)| task.label)
823 .sorted()
824 .collect()
825 })
826 }
827
828 pub(super) fn register_task_used(
829 inventory: &Entity<Inventory>,
830 task_name: &str,
831 cx: &mut TestAppContext,
832 ) -> Task<()> {
833 let tasks = inventory.update(cx, |inventory, cx| {
834 inventory.list_tasks(None, None, None, cx)
835 });
836
837 let task_name = task_name.to_owned();
838 let inventory = inventory.clone();
839 cx.spawn(|mut cx| async move {
840 let (task_source_kind, task) = tasks
841 .await
842 .into_iter()
843 .find(|(_, task)| task.label == task_name)
844 .unwrap_or_else(|| panic!("Failed to find task with name {task_name}"));
845
846 let id_base = task_source_kind.to_id_base();
847 inventory
848 .update(&mut cx, |inventory, _| {
849 inventory.task_scheduled(
850 task_source_kind.clone(),
851 task.resolve_task(&id_base, &TaskContext::default())
852 .unwrap_or_else(|| {
853 panic!("Failed to resolve task with name {task_name}")
854 }),
855 )
856 })
857 .unwrap();
858 })
859 }
860
861 pub(super) fn register_worktree_task_used(
862 inventory: &Entity<Inventory>,
863 worktree_id: WorktreeId,
864 task_name: &str,
865 cx: &mut TestAppContext,
866 ) -> Task<()> {
867 let tasks = inventory.update(cx, |inventory, cx| {
868 inventory.list_tasks(None, None, Some(worktree_id), cx)
869 });
870
871 let inventory = inventory.clone();
872 let task_name = task_name.to_owned();
873 cx.spawn(|mut cx| async move {
874 let (task_source_kind, task) = tasks
875 .await
876 .into_iter()
877 .find(|(_, task)| task.label == task_name)
878 .unwrap_or_else(|| panic!("Failed to find task with name {task_name}"));
879 let id_base = task_source_kind.to_id_base();
880 inventory
881 .update(&mut cx, |inventory, _| {
882 inventory.task_scheduled(
883 task_source_kind.clone(),
884 task.resolve_task(&id_base, &TaskContext::default())
885 .unwrap_or_else(|| {
886 panic!("Failed to resolve task with name {task_name}")
887 }),
888 );
889 })
890 .unwrap();
891 })
892 }
893
894 pub(super) async fn list_tasks(
895 inventory: &Entity<Inventory>,
896 worktree: Option<WorktreeId>,
897 cx: &mut TestAppContext,
898 ) -> Vec<(TaskSourceKind, String)> {
899 let task_context = &TaskContext::default();
900 inventory
901 .update(cx, |inventory, cx| {
902 inventory.list_tasks(None, None, worktree, cx)
903 })
904 .await
905 .into_iter()
906 .filter_map(|(source_kind, task)| {
907 let id_base = source_kind.to_id_base();
908 Some((source_kind, task.resolve_task(&id_base, task_context)?))
909 })
910 .map(|(source_kind, resolved_task)| (source_kind, resolved_task.resolved_label))
911 .collect()
912 }
913}
914
915/// A context provided that tries to provide values for all non-custom [`VariableName`] variants for a currently opened file.
916/// Applied as a base for every custom [`ContextProvider`] unless explicitly oped out.
917pub struct BasicContextProvider {
918 worktree_store: Entity<WorktreeStore>,
919}
920
921impl BasicContextProvider {
922 pub fn new(worktree_store: Entity<WorktreeStore>) -> Self {
923 Self { worktree_store }
924 }
925}
926impl ContextProvider for BasicContextProvider {
927 fn build_context(
928 &self,
929 _: &TaskVariables,
930 location: ContextLocation<'_>,
931 _: Option<HashMap<String, String>>,
932 _: Arc<dyn LanguageToolchainStore>,
933 cx: &mut App,
934 ) -> Task<Result<TaskVariables>> {
935 let location = location.file_location;
936 let buffer = location.buffer.read(cx);
937 let buffer_snapshot = buffer.snapshot();
938 let symbols = buffer_snapshot.symbols_containing(location.range.start, None);
939 let symbol = symbols.unwrap_or_default().last().map(|symbol| {
940 let range = symbol
941 .name_ranges
942 .last()
943 .cloned()
944 .unwrap_or(0..symbol.text.len());
945 symbol.text[range].to_string()
946 });
947
948 let current_file = buffer
949 .file()
950 .and_then(|file| file.as_local())
951 .map(|file| file.abs_path(cx).to_sanitized_string());
952 let Point { row, column } = location.range.start.to_point(&buffer_snapshot);
953 let row = row + 1;
954 let column = column + 1;
955 let selected_text = buffer
956 .chars_for_range(location.range.clone())
957 .collect::<String>();
958
959 let mut task_variables = TaskVariables::from_iter([
960 (VariableName::Row, row.to_string()),
961 (VariableName::Column, column.to_string()),
962 ]);
963
964 if let Some(symbol) = symbol {
965 task_variables.insert(VariableName::Symbol, symbol);
966 }
967 if !selected_text.trim().is_empty() {
968 task_variables.insert(VariableName::SelectedText, selected_text);
969 }
970 let worktree_root_dir =
971 buffer
972 .file()
973 .map(|file| file.worktree_id(cx))
974 .and_then(|worktree_id| {
975 self.worktree_store
976 .read(cx)
977 .worktree_for_id(worktree_id, cx)
978 .and_then(|worktree| worktree.read(cx).root_dir())
979 });
980 if let Some(worktree_path) = worktree_root_dir {
981 task_variables.insert(
982 VariableName::WorktreeRoot,
983 worktree_path.to_sanitized_string(),
984 );
985 if let Some(full_path) = current_file.as_ref() {
986 let relative_path = pathdiff::diff_paths(full_path, worktree_path);
987 if let Some(relative_file) = relative_path {
988 task_variables.insert(
989 VariableName::RelativeFile,
990 relative_file.to_sanitized_string(),
991 );
992 if let Some(relative_dir) = relative_file.parent() {
993 task_variables.insert(
994 VariableName::RelativeDir,
995 if relative_dir.as_os_str().is_empty() {
996 String::from(".")
997 } else {
998 relative_dir.to_sanitized_string()
999 },
1000 );
1001 }
1002 }
1003 }
1004 }
1005
1006 if let Some(path_as_string) = current_file {
1007 let path = Path::new(&path_as_string);
1008 if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
1009 task_variables.insert(VariableName::Filename, String::from(filename));
1010 }
1011
1012 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
1013 task_variables.insert(VariableName::Stem, stem.into());
1014 }
1015
1016 if let Some(dirname) = path.parent().and_then(|s| s.to_str()) {
1017 task_variables.insert(VariableName::Dirname, dirname.into());
1018 }
1019
1020 task_variables.insert(VariableName::File, path_as_string);
1021 }
1022
1023 Task::ready(Ok(task_variables))
1024 }
1025}
1026
1027/// A ContextProvider that doesn't provide any task variables on it's own, though it has some associated tasks.
1028pub struct ContextProviderWithTasks {
1029 templates: TaskTemplates,
1030}
1031
1032impl ContextProviderWithTasks {
1033 pub fn new(definitions: TaskTemplates) -> Self {
1034 Self {
1035 templates: definitions,
1036 }
1037 }
1038}
1039
1040impl ContextProvider for ContextProviderWithTasks {
1041 fn associated_tasks(
1042 &self,
1043 _: Arc<dyn Fs>,
1044 _: Option<Arc<dyn File>>,
1045 _: &App,
1046 ) -> Task<Option<TaskTemplates>> {
1047 Task::ready(Some(self.templates.clone()))
1048 }
1049}
1050
1051#[cfg(test)]
1052mod tests {
1053 use fs::FakeFs;
1054 use gpui::TestAppContext;
1055 use paths::tasks_file;
1056 use pretty_assertions::assert_eq;
1057 use serde_json::json;
1058 use settings::SettingsLocation;
1059
1060 use crate::task_store::TaskStore;
1061
1062 use super::test_inventory::*;
1063 use super::*;
1064
1065 #[gpui::test]
1066 async fn test_task_list_sorting(cx: &mut TestAppContext) {
1067 init_test(cx);
1068 let fs = FakeFs::new(cx.executor());
1069 let inventory = cx.update(|cx| Inventory::new(fs, cx));
1070 let initial_tasks = resolved_task_names(&inventory, None, cx).await;
1071 assert!(
1072 initial_tasks.is_empty(),
1073 "No tasks expected for empty inventory, but got {initial_tasks:?}"
1074 );
1075 let initial_tasks = task_template_names(&inventory, None, cx).await;
1076 assert!(
1077 initial_tasks.is_empty(),
1078 "No tasks expected for empty inventory, but got {initial_tasks:?}"
1079 );
1080 cx.run_until_parked();
1081 let expected_initial_state = [
1082 "1_a_task".to_string(),
1083 "1_task".to_string(),
1084 "2_task".to_string(),
1085 "3_task".to_string(),
1086 ];
1087
1088 inventory.update(cx, |inventory, _| {
1089 inventory
1090 .update_file_based_tasks(
1091 TaskSettingsLocation::Global(tasks_file()),
1092 Some(&mock_tasks_from_names(
1093 expected_initial_state.iter().map(|name| name.as_str()),
1094 )),
1095 )
1096 .unwrap();
1097 });
1098 assert_eq!(
1099 task_template_names(&inventory, None, cx).await,
1100 &expected_initial_state,
1101 );
1102 assert_eq!(
1103 resolved_task_names(&inventory, None, cx).await,
1104 &expected_initial_state,
1105 "Tasks with equal amount of usages should be sorted alphanumerically"
1106 );
1107
1108 register_task_used(&inventory, "2_task", cx).await;
1109 assert_eq!(
1110 task_template_names(&inventory, None, cx).await,
1111 &expected_initial_state,
1112 );
1113 assert_eq!(
1114 resolved_task_names(&inventory, None, cx).await,
1115 vec![
1116 "2_task".to_string(),
1117 "1_a_task".to_string(),
1118 "1_task".to_string(),
1119 "3_task".to_string()
1120 ],
1121 );
1122
1123 register_task_used(&inventory, "1_task", cx).await;
1124 register_task_used(&inventory, "1_task", cx).await;
1125 register_task_used(&inventory, "1_task", cx).await;
1126 register_task_used(&inventory, "3_task", cx).await;
1127 assert_eq!(
1128 task_template_names(&inventory, None, cx).await,
1129 &expected_initial_state,
1130 );
1131 assert_eq!(
1132 resolved_task_names(&inventory, None, cx).await,
1133 vec![
1134 "3_task".to_string(),
1135 "1_task".to_string(),
1136 "2_task".to_string(),
1137 "1_a_task".to_string(),
1138 ],
1139 "Most recently used task should be at the top"
1140 );
1141
1142 let worktree_id = WorktreeId::from_usize(0);
1143 let local_worktree_location = SettingsLocation {
1144 worktree_id,
1145 path: Path::new("foo"),
1146 };
1147 inventory.update(cx, |inventory, _| {
1148 inventory
1149 .update_file_based_tasks(
1150 TaskSettingsLocation::Worktree(local_worktree_location),
1151 Some(&mock_tasks_from_names(["worktree_task_1"])),
1152 )
1153 .unwrap();
1154 });
1155 assert_eq!(
1156 resolved_task_names(&inventory, None, cx).await,
1157 vec![
1158 "3_task".to_string(),
1159 "1_task".to_string(),
1160 "2_task".to_string(),
1161 "1_a_task".to_string(),
1162 ],
1163 "Most recently used task should be at the top"
1164 );
1165 assert_eq!(
1166 resolved_task_names(&inventory, Some(worktree_id), cx).await,
1167 vec![
1168 "3_task".to_string(),
1169 "1_task".to_string(),
1170 "2_task".to_string(),
1171 "worktree_task_1".to_string(),
1172 "1_a_task".to_string(),
1173 ],
1174 );
1175 register_worktree_task_used(&inventory, worktree_id, "worktree_task_1", cx).await;
1176 assert_eq!(
1177 resolved_task_names(&inventory, Some(worktree_id), cx).await,
1178 vec![
1179 "worktree_task_1".to_string(),
1180 "3_task".to_string(),
1181 "1_task".to_string(),
1182 "2_task".to_string(),
1183 "1_a_task".to_string(),
1184 ],
1185 "Most recently used worktree task should be at the top"
1186 );
1187
1188 inventory.update(cx, |inventory, _| {
1189 inventory
1190 .update_file_based_tasks(
1191 TaskSettingsLocation::Global(tasks_file()),
1192 Some(&mock_tasks_from_names(
1193 ["10_hello", "11_hello"]
1194 .into_iter()
1195 .chain(expected_initial_state.iter().map(|name| name.as_str())),
1196 )),
1197 )
1198 .unwrap();
1199 });
1200 cx.run_until_parked();
1201 let expected_updated_state = [
1202 "10_hello".to_string(),
1203 "11_hello".to_string(),
1204 "1_a_task".to_string(),
1205 "1_task".to_string(),
1206 "2_task".to_string(),
1207 "3_task".to_string(),
1208 ];
1209 assert_eq!(
1210 task_template_names(&inventory, None, cx).await,
1211 &expected_updated_state,
1212 );
1213 assert_eq!(
1214 resolved_task_names(&inventory, None, cx).await,
1215 vec![
1216 "worktree_task_1".to_string(),
1217 "1_a_task".to_string(),
1218 "1_task".to_string(),
1219 "2_task".to_string(),
1220 "3_task".to_string(),
1221 "10_hello".to_string(),
1222 "11_hello".to_string(),
1223 ],
1224 "After global tasks update, worktree task usage is not erased and it's the first still; global task is back to regular order as its file was updated"
1225 );
1226
1227 register_task_used(&inventory, "11_hello", cx).await;
1228 assert_eq!(
1229 task_template_names(&inventory, None, cx).await,
1230 &expected_updated_state,
1231 );
1232 assert_eq!(
1233 resolved_task_names(&inventory, None, cx).await,
1234 vec![
1235 "11_hello".to_string(),
1236 "worktree_task_1".to_string(),
1237 "1_a_task".to_string(),
1238 "1_task".to_string(),
1239 "2_task".to_string(),
1240 "3_task".to_string(),
1241 "10_hello".to_string(),
1242 ],
1243 );
1244 }
1245
1246 #[gpui::test]
1247 async fn test_inventory_static_task_filters(cx: &mut TestAppContext) {
1248 init_test(cx);
1249 let fs = FakeFs::new(cx.executor());
1250 let inventory = cx.update(|cx| Inventory::new(fs, cx));
1251 let common_name = "common_task_name";
1252 let worktree_1 = WorktreeId::from_usize(1);
1253 let worktree_2 = WorktreeId::from_usize(2);
1254
1255 cx.run_until_parked();
1256 let worktree_independent_tasks = vec![
1257 (
1258 TaskSourceKind::AbsPath {
1259 id_base: "global tasks.json".into(),
1260 abs_path: paths::tasks_file().clone(),
1261 },
1262 common_name.to_string(),
1263 ),
1264 (
1265 TaskSourceKind::AbsPath {
1266 id_base: "global tasks.json".into(),
1267 abs_path: paths::tasks_file().clone(),
1268 },
1269 "static_source_1".to_string(),
1270 ),
1271 (
1272 TaskSourceKind::AbsPath {
1273 id_base: "global tasks.json".into(),
1274 abs_path: paths::tasks_file().clone(),
1275 },
1276 "static_source_2".to_string(),
1277 ),
1278 ];
1279 let worktree_1_tasks = [
1280 (
1281 TaskSourceKind::Worktree {
1282 id: worktree_1,
1283 directory_in_worktree: PathBuf::from(".zed"),
1284 id_base: "local worktree tasks from directory \".zed\"".into(),
1285 },
1286 common_name.to_string(),
1287 ),
1288 (
1289 TaskSourceKind::Worktree {
1290 id: worktree_1,
1291 directory_in_worktree: PathBuf::from(".zed"),
1292 id_base: "local worktree tasks from directory \".zed\"".into(),
1293 },
1294 "worktree_1".to_string(),
1295 ),
1296 ];
1297 let worktree_2_tasks = [
1298 (
1299 TaskSourceKind::Worktree {
1300 id: worktree_2,
1301 directory_in_worktree: PathBuf::from(".zed"),
1302 id_base: "local worktree tasks from directory \".zed\"".into(),
1303 },
1304 common_name.to_string(),
1305 ),
1306 (
1307 TaskSourceKind::Worktree {
1308 id: worktree_2,
1309 directory_in_worktree: PathBuf::from(".zed"),
1310 id_base: "local worktree tasks from directory \".zed\"".into(),
1311 },
1312 "worktree_2".to_string(),
1313 ),
1314 ];
1315
1316 inventory.update(cx, |inventory, _| {
1317 inventory
1318 .update_file_based_tasks(
1319 TaskSettingsLocation::Global(tasks_file()),
1320 Some(&mock_tasks_from_names(
1321 worktree_independent_tasks
1322 .iter()
1323 .map(|(_, name)| name.as_str()),
1324 )),
1325 )
1326 .unwrap();
1327 inventory
1328 .update_file_based_tasks(
1329 TaskSettingsLocation::Worktree(SettingsLocation {
1330 worktree_id: worktree_1,
1331 path: Path::new(".zed"),
1332 }),
1333 Some(&mock_tasks_from_names(
1334 worktree_1_tasks.iter().map(|(_, name)| name.as_str()),
1335 )),
1336 )
1337 .unwrap();
1338 inventory
1339 .update_file_based_tasks(
1340 TaskSettingsLocation::Worktree(SettingsLocation {
1341 worktree_id: worktree_2,
1342 path: Path::new(".zed"),
1343 }),
1344 Some(&mock_tasks_from_names(
1345 worktree_2_tasks.iter().map(|(_, name)| name.as_str()),
1346 )),
1347 )
1348 .unwrap();
1349 });
1350
1351 assert_eq!(
1352 list_tasks_sorted_by_last_used(&inventory, None, cx).await,
1353 worktree_independent_tasks,
1354 "Without a worktree, only worktree-independent tasks should be listed"
1355 );
1356 assert_eq!(
1357 list_tasks_sorted_by_last_used(&inventory, Some(worktree_1), cx).await,
1358 worktree_1_tasks
1359 .iter()
1360 .chain(worktree_independent_tasks.iter())
1361 .cloned()
1362 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
1363 .collect::<Vec<_>>(),
1364 );
1365 assert_eq!(
1366 list_tasks_sorted_by_last_used(&inventory, Some(worktree_2), cx).await,
1367 worktree_2_tasks
1368 .iter()
1369 .chain(worktree_independent_tasks.iter())
1370 .cloned()
1371 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
1372 .collect::<Vec<_>>(),
1373 );
1374
1375 assert_eq!(
1376 list_tasks(&inventory, None, cx).await,
1377 worktree_independent_tasks,
1378 "Without a worktree, only worktree-independent tasks should be listed"
1379 );
1380 assert_eq!(
1381 list_tasks(&inventory, Some(worktree_1), cx).await,
1382 worktree_1_tasks
1383 .iter()
1384 .chain(worktree_independent_tasks.iter())
1385 .cloned()
1386 .collect::<Vec<_>>(),
1387 );
1388 assert_eq!(
1389 list_tasks(&inventory, Some(worktree_2), cx).await,
1390 worktree_2_tasks
1391 .iter()
1392 .chain(worktree_independent_tasks.iter())
1393 .cloned()
1394 .collect::<Vec<_>>(),
1395 );
1396 }
1397
1398 fn init_test(_cx: &mut TestAppContext) {
1399 zlog::init_test();
1400 TaskStore::init(None);
1401 }
1402
1403 fn resolved_task_names(
1404 inventory: &Entity<Inventory>,
1405 worktree: Option<WorktreeId>,
1406 cx: &mut TestAppContext,
1407 ) -> Task<Vec<String>> {
1408 let tasks = inventory.update(cx, |inventory, cx| {
1409 let mut task_contexts = TaskContexts::default();
1410 task_contexts.active_worktree_context =
1411 worktree.map(|worktree| (worktree, TaskContext::default()));
1412
1413 inventory.used_and_current_resolved_tasks(Arc::new(task_contexts), cx)
1414 });
1415
1416 cx.background_spawn(async move {
1417 let (used, current) = tasks.await;
1418 used.into_iter()
1419 .chain(current)
1420 .map(|(_, task)| task.original_task().label.clone())
1421 .collect()
1422 })
1423 }
1424
1425 fn mock_tasks_from_names<'a>(task_names: impl IntoIterator<Item = &'a str> + 'a) -> String {
1426 serde_json::to_string(&serde_json::Value::Array(
1427 task_names
1428 .into_iter()
1429 .map(|task_name| {
1430 json!({
1431 "label": task_name,
1432 "command": "echo",
1433 "args": vec![task_name],
1434 })
1435 })
1436 .collect::<Vec<_>>(),
1437 ))
1438 .unwrap()
1439 }
1440
1441 async fn list_tasks_sorted_by_last_used(
1442 inventory: &Entity<Inventory>,
1443 worktree: Option<WorktreeId>,
1444 cx: &mut TestAppContext,
1445 ) -> Vec<(TaskSourceKind, String)> {
1446 let (used, current) = inventory
1447 .update(cx, |inventory, cx| {
1448 let mut task_contexts = TaskContexts::default();
1449 task_contexts.active_worktree_context =
1450 worktree.map(|worktree| (worktree, TaskContext::default()));
1451
1452 inventory.used_and_current_resolved_tasks(Arc::new(task_contexts), cx)
1453 })
1454 .await;
1455 let mut all = used;
1456 all.extend(current);
1457 all.into_iter()
1458 .map(|(source_kind, task)| (source_kind, task.resolved_label))
1459 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
1460 .collect()
1461 }
1462}