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.and_then(|location| location.buffer.read(cx).language());
416 let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
417 name: language.name().into(),
418 });
419 let file = location.and_then(|location| location.buffer.read(cx).file().cloned());
420
421 let mut task_labels_to_ids = HashMap::<String, HashSet<TaskId>>::default();
422 let mut lru_score = 0_u32;
423 let previously_spawned_tasks = self
424 .last_scheduled_tasks
425 .iter()
426 .rev()
427 .filter(|(task_kind, _)| {
428 if matches!(task_kind, TaskSourceKind::Language { .. }) {
429 Some(task_kind) == task_source_kind.as_ref()
430 } else {
431 true
432 }
433 })
434 .filter(|(_, resolved_task)| {
435 match task_labels_to_ids.entry(resolved_task.resolved_label.clone()) {
436 hash_map::Entry::Occupied(mut o) => {
437 o.get_mut().insert(resolved_task.id.clone());
438 // Neber allow duplicate reused tasks with the same labels
439 false
440 }
441 hash_map::Entry::Vacant(v) => {
442 v.insert(HashSet::from_iter(Some(resolved_task.id.clone())));
443 true
444 }
445 }
446 })
447 .map(|(task_source_kind, resolved_task)| {
448 (
449 task_source_kind.clone(),
450 resolved_task.clone(),
451 post_inc(&mut lru_score),
452 )
453 })
454 .sorted_unstable_by(task_lru_comparator)
455 .map(|(kind, task, _)| (kind, task))
456 .collect::<Vec<_>>();
457
458 let not_used_score = post_inc(&mut lru_score);
459 let global_tasks = self.global_templates_from_settings().collect::<Vec<_>>();
460 let associated_tasks = language
461 .filter(|language| {
462 language_settings(Some(language.name()), file.as_ref(), cx)
463 .tasks
464 .enabled
465 })
466 .and_then(|language| {
467 language
468 .context_provider()
469 .map(|provider| provider.associated_tasks(fs, file, cx))
470 });
471 let worktree_tasks = worktree
472 .into_iter()
473 .flat_map(|worktree| self.worktree_templates_from_settings(worktree))
474 .collect::<Vec<_>>();
475 let task_contexts = task_contexts.clone();
476 cx.background_spawn(async move {
477 let language_tasks = if let Some(task) = associated_tasks {
478 task.await.map(|templates| {
479 templates
480 .0
481 .into_iter()
482 .flat_map(|task| Some((task_source_kind.clone()?, task)))
483 })
484 } else {
485 None
486 };
487
488 let worktree_tasks = worktree_tasks
489 .into_iter()
490 .chain(language_tasks.into_iter().flatten())
491 .chain(global_tasks);
492
493 let new_resolved_tasks = worktree_tasks
494 .flat_map(|(kind, task)| {
495 let id_base = kind.to_id_base();
496 if let TaskSourceKind::Worktree { id, .. } = &kind {
497 None.or_else(|| {
498 let (_, _, item_context) =
499 task_contexts.active_item_context.as_ref().filter(
500 |(worktree_id, _, _)| Some(id) == worktree_id.as_ref(),
501 )?;
502 task.resolve_task(&id_base, item_context)
503 })
504 .or_else(|| {
505 let (_, worktree_context) = task_contexts
506 .active_worktree_context
507 .as_ref()
508 .filter(|(worktree_id, _)| id == worktree_id)?;
509 task.resolve_task(&id_base, worktree_context)
510 })
511 .or_else(|| {
512 if let TaskSourceKind::Worktree { id, .. } = &kind {
513 let worktree_context = task_contexts
514 .other_worktree_contexts
515 .iter()
516 .find(|(worktree_id, _)| worktree_id == id)
517 .map(|(_, context)| context)?;
518 task.resolve_task(&id_base, worktree_context)
519 } else {
520 None
521 }
522 })
523 } else {
524 None.or_else(|| {
525 let (_, _, item_context) =
526 task_contexts.active_item_context.as_ref()?;
527 task.resolve_task(&id_base, item_context)
528 })
529 .or_else(|| {
530 let (_, worktree_context) =
531 task_contexts.active_worktree_context.as_ref()?;
532 task.resolve_task(&id_base, worktree_context)
533 })
534 }
535 .or_else(|| task.resolve_task(&id_base, &TaskContext::default()))
536 .map(move |resolved_task| (kind.clone(), resolved_task, not_used_score))
537 })
538 .filter(|(_, resolved_task, _)| {
539 match task_labels_to_ids.entry(resolved_task.resolved_label.clone()) {
540 hash_map::Entry::Occupied(mut o) => {
541 // Allow new tasks with the same label, if their context is different
542 o.get_mut().insert(resolved_task.id.clone())
543 }
544 hash_map::Entry::Vacant(v) => {
545 v.insert(HashSet::from_iter(Some(resolved_task.id.clone())));
546 true
547 }
548 }
549 })
550 .sorted_unstable_by(task_lru_comparator)
551 .map(|(kind, task, _)| (kind, task))
552 .collect::<Vec<_>>();
553
554 (previously_spawned_tasks, new_resolved_tasks)
555 })
556 }
557
558 /// Returns the last scheduled task by task_id if provided.
559 /// Otherwise, returns the last scheduled task.
560 pub fn last_scheduled_task(
561 &self,
562 task_id: Option<&TaskId>,
563 ) -> Option<(TaskSourceKind, ResolvedTask)> {
564 if let Some(task_id) = task_id {
565 self.last_scheduled_tasks
566 .iter()
567 .find(|(_, task)| &task.id == task_id)
568 .cloned()
569 } else {
570 self.last_scheduled_tasks.back().cloned()
571 }
572 }
573
574 /// Registers task "usage" as being scheduled – to be used for LRU sorting when listing all tasks.
575 pub fn task_scheduled(
576 &mut self,
577 task_source_kind: TaskSourceKind,
578 resolved_task: ResolvedTask,
579 ) {
580 self.last_scheduled_tasks
581 .push_back((task_source_kind, resolved_task));
582 if self.last_scheduled_tasks.len() > 5_000 {
583 self.last_scheduled_tasks.pop_front();
584 }
585 }
586
587 /// Deletes a resolved task from history, using its id.
588 /// A similar may still resurface in `used_and_current_resolved_tasks` when its [`TaskTemplate`] is resolved again.
589 pub fn delete_previously_used(&mut self, id: &TaskId) {
590 self.last_scheduled_tasks.retain(|(_, task)| &task.id != id);
591 }
592
593 fn global_templates_from_settings(
594 &self,
595 ) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
596 self.templates_from_settings.global_scenarios()
597 }
598
599 fn global_debug_scenarios_from_settings(
600 &self,
601 ) -> impl '_ + Iterator<Item = (TaskSourceKind, DebugScenario)> {
602 self.scenarios_from_settings.global_scenarios()
603 }
604
605 fn worktree_scenarios_from_settings(
606 &self,
607 worktree: WorktreeId,
608 ) -> impl '_ + Iterator<Item = (TaskSourceKind, DebugScenario)> {
609 self.scenarios_from_settings.worktree_scenarios(worktree)
610 }
611
612 fn worktree_templates_from_settings(
613 &self,
614 worktree: WorktreeId,
615 ) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
616 self.templates_from_settings.worktree_scenarios(worktree)
617 }
618
619 /// Updates in-memory task metadata from the JSON string given.
620 /// Will fail if the JSON is not a valid array of objects, but will continue if any object will not parse into a [`TaskTemplate`].
621 ///
622 /// Global tasks are updated for no worktree provided, otherwise the worktree metadata for a given path will be updated.
623 pub(crate) fn update_file_based_tasks(
624 &mut self,
625 location: TaskSettingsLocation<'_>,
626 raw_tasks_json: Option<&str>,
627 ) -> Result<(), InvalidSettingsError> {
628 let raw_tasks = match parse_json_with_comments::<Vec<serde_json::Value>>(
629 raw_tasks_json.unwrap_or("[]"),
630 ) {
631 Ok(tasks) => tasks,
632 Err(e) => {
633 return Err(InvalidSettingsError::Tasks {
634 path: match location {
635 TaskSettingsLocation::Global(path) => path.to_owned(),
636 TaskSettingsLocation::Worktree(settings_location) => {
637 settings_location.path.join(task_file_name())
638 }
639 },
640 message: format!("Failed to parse tasks file content as a JSON array: {e}"),
641 });
642 }
643 };
644 let new_templates = raw_tasks.into_iter().filter_map(|raw_template| {
645 serde_json::from_value::<TaskTemplate>(raw_template).log_err()
646 });
647
648 let parsed_templates = &mut self.templates_from_settings;
649 match location {
650 TaskSettingsLocation::Global(path) => {
651 parsed_templates
652 .global
653 .entry(path.to_owned())
654 .insert_entry(new_templates.collect());
655 self.last_scheduled_tasks.retain(|(kind, _)| {
656 if let TaskSourceKind::AbsPath { abs_path, .. } = kind {
657 abs_path != path
658 } else {
659 true
660 }
661 });
662 }
663 TaskSettingsLocation::Worktree(location) => {
664 let new_templates = new_templates.collect::<Vec<_>>();
665 if new_templates.is_empty() {
666 if let Some(worktree_tasks) =
667 parsed_templates.worktree.get_mut(&location.worktree_id)
668 {
669 worktree_tasks.remove(location.path);
670 }
671 } else {
672 parsed_templates
673 .worktree
674 .entry(location.worktree_id)
675 .or_default()
676 .insert(Arc::from(location.path), new_templates);
677 }
678 self.last_scheduled_tasks.retain(|(kind, _)| {
679 if let TaskSourceKind::Worktree {
680 directory_in_worktree,
681 id,
682 ..
683 } = kind
684 {
685 *id != location.worktree_id || directory_in_worktree != location.path
686 } else {
687 true
688 }
689 });
690 }
691 }
692
693 Ok(())
694 }
695
696 /// Updates in-memory task metadata from the JSON string given.
697 /// Will fail if the JSON is not a valid array of objects, but will continue if any object will not parse into a [`TaskTemplate`].
698 ///
699 /// Global tasks are updated for no worktree provided, otherwise the worktree metadata for a given path will be updated.
700 pub(crate) fn update_file_based_scenarios(
701 &mut self,
702 location: TaskSettingsLocation<'_>,
703 raw_tasks_json: Option<&str>,
704 ) -> Result<(), InvalidSettingsError> {
705 let raw_tasks = match parse_json_with_comments::<Vec<serde_json::Value>>(
706 raw_tasks_json.unwrap_or("[]"),
707 ) {
708 Ok(tasks) => tasks,
709 Err(e) => {
710 return Err(InvalidSettingsError::Debug {
711 path: match location {
712 TaskSettingsLocation::Global(path) => path.to_owned(),
713 TaskSettingsLocation::Worktree(settings_location) => {
714 settings_location.path.join(debug_task_file_name())
715 }
716 },
717 message: format!("Failed to parse tasks file content as a JSON array: {e}"),
718 });
719 }
720 };
721
722 let new_templates = raw_tasks.into_iter().filter_map(|raw_template| {
723 serde_json::from_value::<DebugScenario>(raw_template).log_err()
724 });
725
726 let parsed_scenarios = &mut self.scenarios_from_settings;
727 match location {
728 TaskSettingsLocation::Global(path) => {
729 parsed_scenarios
730 .global
731 .entry(path.to_owned())
732 .insert_entry(new_templates.collect());
733 }
734 TaskSettingsLocation::Worktree(location) => {
735 let new_templates = new_templates.collect::<Vec<_>>();
736 if new_templates.is_empty() {
737 if let Some(worktree_tasks) =
738 parsed_scenarios.worktree.get_mut(&location.worktree_id)
739 {
740 worktree_tasks.remove(location.path);
741 }
742 } else {
743 parsed_scenarios
744 .worktree
745 .entry(location.worktree_id)
746 .or_default()
747 .insert(Arc::from(location.path), new_templates);
748 }
749 }
750 }
751
752 Ok(())
753 }
754}
755
756fn task_lru_comparator(
757 (kind_a, task_a, lru_score_a): &(TaskSourceKind, ResolvedTask, u32),
758 (kind_b, task_b, lru_score_b): &(TaskSourceKind, ResolvedTask, u32),
759) -> cmp::Ordering {
760 lru_score_a
761 // First, display recently used templates above all.
762 .cmp(lru_score_b)
763 // Then, ensure more specific sources are displayed first.
764 .then(task_source_kind_preference(kind_a).cmp(&task_source_kind_preference(kind_b)))
765 // After that, display first more specific tasks, using more template variables.
766 // Bonus points for tasks with symbol variables.
767 .then(task_variables_preference(task_a).cmp(&task_variables_preference(task_b)))
768 // Finally, sort by the resolved label, but a bit more specifically, to avoid mixing letters and digits.
769 .then({
770 NumericPrefixWithSuffix::from_numeric_prefixed_str(&task_a.resolved_label)
771 .cmp(&NumericPrefixWithSuffix::from_numeric_prefixed_str(
772 &task_b.resolved_label,
773 ))
774 .then(task_a.resolved_label.cmp(&task_b.resolved_label))
775 .then(kind_a.cmp(kind_b))
776 })
777}
778
779fn task_source_kind_preference(kind: &TaskSourceKind) -> u32 {
780 match kind {
781 TaskSourceKind::Lsp { .. } => 0,
782 TaskSourceKind::Language { .. } => 1,
783 TaskSourceKind::UserInput => 2,
784 TaskSourceKind::Worktree { .. } => 3,
785 TaskSourceKind::AbsPath { .. } => 4,
786 }
787}
788
789fn task_variables_preference(task: &ResolvedTask) -> Reverse<usize> {
790 let task_variables = task.substituted_variables();
791 Reverse(if task_variables.contains(&VariableName::Symbol) {
792 task_variables.len() + 1
793 } else {
794 task_variables.len()
795 })
796}
797
798#[cfg(test)]
799mod test_inventory {
800 use gpui::{AppContext as _, Entity, Task, TestAppContext};
801 use itertools::Itertools;
802 use task::TaskContext;
803 use worktree::WorktreeId;
804
805 use crate::Inventory;
806
807 use super::TaskSourceKind;
808
809 pub(super) fn task_template_names(
810 inventory: &Entity<Inventory>,
811 worktree: Option<WorktreeId>,
812 cx: &mut TestAppContext,
813 ) -> Task<Vec<String>> {
814 let new_tasks = inventory.update(cx, |inventory, cx| {
815 inventory.list_tasks(None, None, worktree, cx)
816 });
817 cx.background_spawn(async move {
818 new_tasks
819 .await
820 .into_iter()
821 .map(|(_, task)| task.label)
822 .sorted()
823 .collect()
824 })
825 }
826
827 pub(super) fn register_task_used(
828 inventory: &Entity<Inventory>,
829 task_name: &str,
830 cx: &mut TestAppContext,
831 ) -> Task<()> {
832 let tasks = inventory.update(cx, |inventory, cx| {
833 inventory.list_tasks(None, None, None, cx)
834 });
835
836 let task_name = task_name.to_owned();
837 let inventory = inventory.clone();
838 cx.spawn(|mut cx| async move {
839 let (task_source_kind, task) = tasks
840 .await
841 .into_iter()
842 .find(|(_, task)| task.label == task_name)
843 .unwrap_or_else(|| panic!("Failed to find task with name {task_name}"));
844
845 let id_base = task_source_kind.to_id_base();
846 inventory
847 .update(&mut cx, |inventory, _| {
848 inventory.task_scheduled(
849 task_source_kind.clone(),
850 task.resolve_task(&id_base, &TaskContext::default())
851 .unwrap_or_else(|| {
852 panic!("Failed to resolve task with name {task_name}")
853 }),
854 )
855 })
856 .unwrap();
857 })
858 }
859
860 pub(super) fn register_worktree_task_used(
861 inventory: &Entity<Inventory>,
862 worktree_id: WorktreeId,
863 task_name: &str,
864 cx: &mut TestAppContext,
865 ) -> Task<()> {
866 let tasks = inventory.update(cx, |inventory, cx| {
867 inventory.list_tasks(None, None, Some(worktree_id), cx)
868 });
869
870 let inventory = inventory.clone();
871 let task_name = task_name.to_owned();
872 cx.spawn(|mut cx| async move {
873 let (task_source_kind, task) = tasks
874 .await
875 .into_iter()
876 .find(|(_, task)| task.label == task_name)
877 .unwrap_or_else(|| panic!("Failed to find task with name {task_name}"));
878 let id_base = task_source_kind.to_id_base();
879 inventory
880 .update(&mut cx, |inventory, _| {
881 inventory.task_scheduled(
882 task_source_kind.clone(),
883 task.resolve_task(&id_base, &TaskContext::default())
884 .unwrap_or_else(|| {
885 panic!("Failed to resolve task with name {task_name}")
886 }),
887 );
888 })
889 .unwrap();
890 })
891 }
892
893 pub(super) async fn list_tasks(
894 inventory: &Entity<Inventory>,
895 worktree: Option<WorktreeId>,
896 cx: &mut TestAppContext,
897 ) -> Vec<(TaskSourceKind, String)> {
898 let task_context = &TaskContext::default();
899 inventory
900 .update(cx, |inventory, cx| {
901 inventory.list_tasks(None, None, worktree, cx)
902 })
903 .await
904 .into_iter()
905 .filter_map(|(source_kind, task)| {
906 let id_base = source_kind.to_id_base();
907 Some((source_kind, task.resolve_task(&id_base, task_context)?))
908 })
909 .map(|(source_kind, resolved_task)| (source_kind, resolved_task.resolved_label))
910 .collect()
911 }
912}
913
914/// A context provided that tries to provide values for all non-custom [`VariableName`] variants for a currently opened file.
915/// Applied as a base for every custom [`ContextProvider`] unless explicitly oped out.
916pub struct BasicContextProvider {
917 worktree_store: Entity<WorktreeStore>,
918}
919
920impl BasicContextProvider {
921 pub fn new(worktree_store: Entity<WorktreeStore>) -> Self {
922 Self { worktree_store }
923 }
924}
925impl ContextProvider for BasicContextProvider {
926 fn build_context(
927 &self,
928 _: &TaskVariables,
929 location: ContextLocation<'_>,
930 _: Option<HashMap<String, String>>,
931 _: Arc<dyn LanguageToolchainStore>,
932 cx: &mut App,
933 ) -> Task<Result<TaskVariables>> {
934 let location = location.file_location;
935 let buffer = location.buffer.read(cx);
936 let buffer_snapshot = buffer.snapshot();
937 let symbols = buffer_snapshot.symbols_containing(location.range.start, None);
938 let symbol = symbols.unwrap_or_default().last().map(|symbol| {
939 let range = symbol
940 .name_ranges
941 .last()
942 .cloned()
943 .unwrap_or(0..symbol.text.len());
944 symbol.text[range].to_string()
945 });
946
947 let current_file = buffer
948 .file()
949 .and_then(|file| file.as_local())
950 .map(|file| file.abs_path(cx).to_sanitized_string());
951 let Point { row, column } = location.range.start.to_point(&buffer_snapshot);
952 let row = row + 1;
953 let column = column + 1;
954 let selected_text = buffer
955 .chars_for_range(location.range.clone())
956 .collect::<String>();
957
958 let mut task_variables = TaskVariables::from_iter([
959 (VariableName::Row, row.to_string()),
960 (VariableName::Column, column.to_string()),
961 ]);
962
963 if let Some(symbol) = symbol {
964 task_variables.insert(VariableName::Symbol, symbol);
965 }
966 if !selected_text.trim().is_empty() {
967 task_variables.insert(VariableName::SelectedText, selected_text);
968 }
969 let worktree_root_dir =
970 buffer
971 .file()
972 .map(|file| file.worktree_id(cx))
973 .and_then(|worktree_id| {
974 self.worktree_store
975 .read(cx)
976 .worktree_for_id(worktree_id, cx)
977 .and_then(|worktree| worktree.read(cx).root_dir())
978 });
979 if let Some(worktree_path) = worktree_root_dir {
980 task_variables.insert(
981 VariableName::WorktreeRoot,
982 worktree_path.to_sanitized_string(),
983 );
984 if let Some(full_path) = current_file.as_ref() {
985 let relative_path = pathdiff::diff_paths(full_path, worktree_path);
986 if let Some(relative_file) = relative_path {
987 task_variables.insert(
988 VariableName::RelativeFile,
989 relative_file.to_sanitized_string(),
990 );
991 if let Some(relative_dir) = relative_file.parent() {
992 task_variables.insert(
993 VariableName::RelativeDir,
994 if relative_dir.as_os_str().is_empty() {
995 String::from(".")
996 } else {
997 relative_dir.to_sanitized_string()
998 },
999 );
1000 }
1001 }
1002 }
1003 }
1004
1005 if let Some(path_as_string) = current_file {
1006 let path = Path::new(&path_as_string);
1007 if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
1008 task_variables.insert(VariableName::Filename, String::from(filename));
1009 }
1010
1011 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
1012 task_variables.insert(VariableName::Stem, stem.into());
1013 }
1014
1015 if let Some(dirname) = path.parent().and_then(|s| s.to_str()) {
1016 task_variables.insert(VariableName::Dirname, dirname.into());
1017 }
1018
1019 task_variables.insert(VariableName::File, path_as_string);
1020 }
1021
1022 Task::ready(Ok(task_variables))
1023 }
1024}
1025
1026/// A ContextProvider that doesn't provide any task variables on it's own, though it has some associated tasks.
1027pub struct ContextProviderWithTasks {
1028 templates: TaskTemplates,
1029}
1030
1031impl ContextProviderWithTasks {
1032 pub fn new(definitions: TaskTemplates) -> Self {
1033 Self {
1034 templates: definitions,
1035 }
1036 }
1037}
1038
1039impl ContextProvider for ContextProviderWithTasks {
1040 fn associated_tasks(
1041 &self,
1042 _: Arc<dyn Fs>,
1043 _: Option<Arc<dyn File>>,
1044 _: &App,
1045 ) -> Task<Option<TaskTemplates>> {
1046 Task::ready(Some(self.templates.clone()))
1047 }
1048}
1049
1050#[cfg(test)]
1051mod tests {
1052 use fs::FakeFs;
1053 use gpui::TestAppContext;
1054 use paths::tasks_file;
1055 use pretty_assertions::assert_eq;
1056 use serde_json::json;
1057 use settings::SettingsLocation;
1058
1059 use crate::task_store::TaskStore;
1060
1061 use super::test_inventory::*;
1062 use super::*;
1063
1064 #[gpui::test]
1065 async fn test_task_list_sorting(cx: &mut TestAppContext) {
1066 init_test(cx);
1067 let fs = FakeFs::new(cx.executor());
1068 let inventory = cx.update(|cx| Inventory::new(fs, cx));
1069 let initial_tasks = resolved_task_names(&inventory, None, cx).await;
1070 assert!(
1071 initial_tasks.is_empty(),
1072 "No tasks expected for empty inventory, but got {initial_tasks:?}"
1073 );
1074 let initial_tasks = task_template_names(&inventory, None, cx).await;
1075 assert!(
1076 initial_tasks.is_empty(),
1077 "No tasks expected for empty inventory, but got {initial_tasks:?}"
1078 );
1079 cx.run_until_parked();
1080 let expected_initial_state = [
1081 "1_a_task".to_string(),
1082 "1_task".to_string(),
1083 "2_task".to_string(),
1084 "3_task".to_string(),
1085 ];
1086
1087 inventory.update(cx, |inventory, _| {
1088 inventory
1089 .update_file_based_tasks(
1090 TaskSettingsLocation::Global(tasks_file()),
1091 Some(&mock_tasks_from_names(
1092 expected_initial_state.iter().map(|name| name.as_str()),
1093 )),
1094 )
1095 .unwrap();
1096 });
1097 assert_eq!(
1098 task_template_names(&inventory, None, cx).await,
1099 &expected_initial_state,
1100 );
1101 assert_eq!(
1102 resolved_task_names(&inventory, None, cx).await,
1103 &expected_initial_state,
1104 "Tasks with equal amount of usages should be sorted alphanumerically"
1105 );
1106
1107 register_task_used(&inventory, "2_task", cx).await;
1108 assert_eq!(
1109 task_template_names(&inventory, None, cx).await,
1110 &expected_initial_state,
1111 );
1112 assert_eq!(
1113 resolved_task_names(&inventory, None, cx).await,
1114 vec![
1115 "2_task".to_string(),
1116 "1_a_task".to_string(),
1117 "1_task".to_string(),
1118 "3_task".to_string()
1119 ],
1120 );
1121
1122 register_task_used(&inventory, "1_task", cx).await;
1123 register_task_used(&inventory, "1_task", cx).await;
1124 register_task_used(&inventory, "1_task", cx).await;
1125 register_task_used(&inventory, "3_task", cx).await;
1126 assert_eq!(
1127 task_template_names(&inventory, None, cx).await,
1128 &expected_initial_state,
1129 );
1130 assert_eq!(
1131 resolved_task_names(&inventory, None, cx).await,
1132 vec![
1133 "3_task".to_string(),
1134 "1_task".to_string(),
1135 "2_task".to_string(),
1136 "1_a_task".to_string(),
1137 ],
1138 "Most recently used task should be at the top"
1139 );
1140
1141 let worktree_id = WorktreeId::from_usize(0);
1142 let local_worktree_location = SettingsLocation {
1143 worktree_id,
1144 path: Path::new("foo"),
1145 };
1146 inventory.update(cx, |inventory, _| {
1147 inventory
1148 .update_file_based_tasks(
1149 TaskSettingsLocation::Worktree(local_worktree_location),
1150 Some(&mock_tasks_from_names(["worktree_task_1"])),
1151 )
1152 .unwrap();
1153 });
1154 assert_eq!(
1155 resolved_task_names(&inventory, None, cx).await,
1156 vec![
1157 "3_task".to_string(),
1158 "1_task".to_string(),
1159 "2_task".to_string(),
1160 "1_a_task".to_string(),
1161 ],
1162 "Most recently used task should be at the top"
1163 );
1164 assert_eq!(
1165 resolved_task_names(&inventory, Some(worktree_id), cx).await,
1166 vec![
1167 "3_task".to_string(),
1168 "1_task".to_string(),
1169 "2_task".to_string(),
1170 "worktree_task_1".to_string(),
1171 "1_a_task".to_string(),
1172 ],
1173 );
1174 register_worktree_task_used(&inventory, worktree_id, "worktree_task_1", cx).await;
1175 assert_eq!(
1176 resolved_task_names(&inventory, Some(worktree_id), cx).await,
1177 vec![
1178 "worktree_task_1".to_string(),
1179 "3_task".to_string(),
1180 "1_task".to_string(),
1181 "2_task".to_string(),
1182 "1_a_task".to_string(),
1183 ],
1184 "Most recently used worktree task should be at the top"
1185 );
1186
1187 inventory.update(cx, |inventory, _| {
1188 inventory
1189 .update_file_based_tasks(
1190 TaskSettingsLocation::Global(tasks_file()),
1191 Some(&mock_tasks_from_names(
1192 ["10_hello", "11_hello"]
1193 .into_iter()
1194 .chain(expected_initial_state.iter().map(|name| name.as_str())),
1195 )),
1196 )
1197 .unwrap();
1198 });
1199 cx.run_until_parked();
1200 let expected_updated_state = [
1201 "10_hello".to_string(),
1202 "11_hello".to_string(),
1203 "1_a_task".to_string(),
1204 "1_task".to_string(),
1205 "2_task".to_string(),
1206 "3_task".to_string(),
1207 ];
1208 assert_eq!(
1209 task_template_names(&inventory, None, cx).await,
1210 &expected_updated_state,
1211 );
1212 assert_eq!(
1213 resolved_task_names(&inventory, None, cx).await,
1214 vec![
1215 "worktree_task_1".to_string(),
1216 "1_a_task".to_string(),
1217 "1_task".to_string(),
1218 "2_task".to_string(),
1219 "3_task".to_string(),
1220 "10_hello".to_string(),
1221 "11_hello".to_string(),
1222 ],
1223 "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"
1224 );
1225
1226 register_task_used(&inventory, "11_hello", cx).await;
1227 assert_eq!(
1228 task_template_names(&inventory, None, cx).await,
1229 &expected_updated_state,
1230 );
1231 assert_eq!(
1232 resolved_task_names(&inventory, None, cx).await,
1233 vec![
1234 "11_hello".to_string(),
1235 "worktree_task_1".to_string(),
1236 "1_a_task".to_string(),
1237 "1_task".to_string(),
1238 "2_task".to_string(),
1239 "3_task".to_string(),
1240 "10_hello".to_string(),
1241 ],
1242 );
1243 }
1244
1245 #[gpui::test]
1246 async fn test_inventory_static_task_filters(cx: &mut TestAppContext) {
1247 init_test(cx);
1248 let fs = FakeFs::new(cx.executor());
1249 let inventory = cx.update(|cx| Inventory::new(fs, cx));
1250 let common_name = "common_task_name";
1251 let worktree_1 = WorktreeId::from_usize(1);
1252 let worktree_2 = WorktreeId::from_usize(2);
1253
1254 cx.run_until_parked();
1255 let worktree_independent_tasks = vec![
1256 (
1257 TaskSourceKind::AbsPath {
1258 id_base: "global tasks.json".into(),
1259 abs_path: paths::tasks_file().clone(),
1260 },
1261 common_name.to_string(),
1262 ),
1263 (
1264 TaskSourceKind::AbsPath {
1265 id_base: "global tasks.json".into(),
1266 abs_path: paths::tasks_file().clone(),
1267 },
1268 "static_source_1".to_string(),
1269 ),
1270 (
1271 TaskSourceKind::AbsPath {
1272 id_base: "global tasks.json".into(),
1273 abs_path: paths::tasks_file().clone(),
1274 },
1275 "static_source_2".to_string(),
1276 ),
1277 ];
1278 let worktree_1_tasks = [
1279 (
1280 TaskSourceKind::Worktree {
1281 id: worktree_1,
1282 directory_in_worktree: PathBuf::from(".zed"),
1283 id_base: "local worktree tasks from directory \".zed\"".into(),
1284 },
1285 common_name.to_string(),
1286 ),
1287 (
1288 TaskSourceKind::Worktree {
1289 id: worktree_1,
1290 directory_in_worktree: PathBuf::from(".zed"),
1291 id_base: "local worktree tasks from directory \".zed\"".into(),
1292 },
1293 "worktree_1".to_string(),
1294 ),
1295 ];
1296 let worktree_2_tasks = [
1297 (
1298 TaskSourceKind::Worktree {
1299 id: worktree_2,
1300 directory_in_worktree: PathBuf::from(".zed"),
1301 id_base: "local worktree tasks from directory \".zed\"".into(),
1302 },
1303 common_name.to_string(),
1304 ),
1305 (
1306 TaskSourceKind::Worktree {
1307 id: worktree_2,
1308 directory_in_worktree: PathBuf::from(".zed"),
1309 id_base: "local worktree tasks from directory \".zed\"".into(),
1310 },
1311 "worktree_2".to_string(),
1312 ),
1313 ];
1314
1315 inventory.update(cx, |inventory, _| {
1316 inventory
1317 .update_file_based_tasks(
1318 TaskSettingsLocation::Global(tasks_file()),
1319 Some(&mock_tasks_from_names(
1320 worktree_independent_tasks
1321 .iter()
1322 .map(|(_, name)| name.as_str()),
1323 )),
1324 )
1325 .unwrap();
1326 inventory
1327 .update_file_based_tasks(
1328 TaskSettingsLocation::Worktree(SettingsLocation {
1329 worktree_id: worktree_1,
1330 path: Path::new(".zed"),
1331 }),
1332 Some(&mock_tasks_from_names(
1333 worktree_1_tasks.iter().map(|(_, name)| name.as_str()),
1334 )),
1335 )
1336 .unwrap();
1337 inventory
1338 .update_file_based_tasks(
1339 TaskSettingsLocation::Worktree(SettingsLocation {
1340 worktree_id: worktree_2,
1341 path: Path::new(".zed"),
1342 }),
1343 Some(&mock_tasks_from_names(
1344 worktree_2_tasks.iter().map(|(_, name)| name.as_str()),
1345 )),
1346 )
1347 .unwrap();
1348 });
1349
1350 assert_eq!(
1351 list_tasks_sorted_by_last_used(&inventory, None, cx).await,
1352 worktree_independent_tasks,
1353 "Without a worktree, only worktree-independent tasks should be listed"
1354 );
1355 assert_eq!(
1356 list_tasks_sorted_by_last_used(&inventory, Some(worktree_1), cx).await,
1357 worktree_1_tasks
1358 .iter()
1359 .chain(worktree_independent_tasks.iter())
1360 .cloned()
1361 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
1362 .collect::<Vec<_>>(),
1363 );
1364 assert_eq!(
1365 list_tasks_sorted_by_last_used(&inventory, Some(worktree_2), cx).await,
1366 worktree_2_tasks
1367 .iter()
1368 .chain(worktree_independent_tasks.iter())
1369 .cloned()
1370 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
1371 .collect::<Vec<_>>(),
1372 );
1373
1374 assert_eq!(
1375 list_tasks(&inventory, None, cx).await,
1376 worktree_independent_tasks,
1377 "Without a worktree, only worktree-independent tasks should be listed"
1378 );
1379 assert_eq!(
1380 list_tasks(&inventory, Some(worktree_1), cx).await,
1381 worktree_1_tasks
1382 .iter()
1383 .chain(worktree_independent_tasks.iter())
1384 .cloned()
1385 .collect::<Vec<_>>(),
1386 );
1387 assert_eq!(
1388 list_tasks(&inventory, Some(worktree_2), cx).await,
1389 worktree_2_tasks
1390 .iter()
1391 .chain(worktree_independent_tasks.iter())
1392 .cloned()
1393 .collect::<Vec<_>>(),
1394 );
1395 }
1396
1397 fn init_test(_cx: &mut TestAppContext) {
1398 zlog::init_test();
1399 TaskStore::init(None);
1400 }
1401
1402 fn resolved_task_names(
1403 inventory: &Entity<Inventory>,
1404 worktree: Option<WorktreeId>,
1405 cx: &mut TestAppContext,
1406 ) -> Task<Vec<String>> {
1407 let tasks = inventory.update(cx, |inventory, cx| {
1408 let mut task_contexts = TaskContexts::default();
1409 task_contexts.active_worktree_context =
1410 worktree.map(|worktree| (worktree, TaskContext::default()));
1411
1412 inventory.used_and_current_resolved_tasks(Arc::new(task_contexts), cx)
1413 });
1414
1415 cx.background_spawn(async move {
1416 let (used, current) = tasks.await;
1417 used.into_iter()
1418 .chain(current)
1419 .map(|(_, task)| task.original_task().label.clone())
1420 .collect()
1421 })
1422 }
1423
1424 fn mock_tasks_from_names<'a>(task_names: impl IntoIterator<Item = &'a str> + 'a) -> String {
1425 serde_json::to_string(&serde_json::Value::Array(
1426 task_names
1427 .into_iter()
1428 .map(|task_name| {
1429 json!({
1430 "label": task_name,
1431 "command": "echo",
1432 "args": vec![task_name],
1433 })
1434 })
1435 .collect::<Vec<_>>(),
1436 ))
1437 .unwrap()
1438 }
1439
1440 async fn list_tasks_sorted_by_last_used(
1441 inventory: &Entity<Inventory>,
1442 worktree: Option<WorktreeId>,
1443 cx: &mut TestAppContext,
1444 ) -> Vec<(TaskSourceKind, String)> {
1445 let (used, current) = inventory
1446 .update(cx, |inventory, cx| {
1447 let mut task_contexts = TaskContexts::default();
1448 task_contexts.active_worktree_context =
1449 worktree.map(|worktree| (worktree, TaskContext::default()));
1450
1451 inventory.used_and_current_resolved_tasks(Arc::new(task_contexts), cx)
1452 })
1453 .await;
1454 let mut all = used;
1455 all.extend(current);
1456 all.into_iter()
1457 .map(|(source_kind, task)| (source_kind, task.resolved_label))
1458 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
1459 .collect()
1460 }
1461}