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
724 .into_iter()
725 .filter_map(|raw_template| {
726 serde_json::from_value::<DebugScenario>(raw_template).log_err()
727 })
728 .collect::<Vec<_>>();
729
730 let parsed_scenarios = &mut self.scenarios_from_settings;
731 let mut new_definitions: HashMap<_, _> = new_templates
732 .iter()
733 .map(|template| (template.label.clone(), template.clone()))
734 .collect();
735 let previously_existing_scenarios;
736
737 match location {
738 TaskSettingsLocation::Global(path) => {
739 previously_existing_scenarios = parsed_scenarios
740 .global_scenarios()
741 .map(|(_, scenario)| scenario.label.clone())
742 .collect::<HashSet<_>>();
743 parsed_scenarios
744 .global
745 .entry(path.to_owned())
746 .insert_entry(new_templates);
747 }
748 TaskSettingsLocation::Worktree(location) => {
749 previously_existing_scenarios = parsed_scenarios
750 .worktree_scenarios(location.worktree_id)
751 .map(|(_, scenario)| scenario.label.clone())
752 .collect::<HashSet<_>>();
753
754 if new_templates.is_empty() {
755 if let Some(worktree_tasks) =
756 parsed_scenarios.worktree.get_mut(&location.worktree_id)
757 {
758 worktree_tasks.remove(location.path);
759 }
760 } else {
761 parsed_scenarios
762 .worktree
763 .entry(location.worktree_id)
764 .or_default()
765 .insert(Arc::from(location.path), new_templates);
766 }
767 }
768 }
769 self.last_scheduled_scenarios.retain_mut(|scenario| {
770 if !previously_existing_scenarios.contains(&scenario.label) {
771 return true;
772 }
773 if let Some(new_definition) = new_definitions.remove(&scenario.label) {
774 *scenario = new_definition;
775 true
776 } else {
777 false
778 }
779 });
780
781 Ok(())
782 }
783}
784
785fn task_lru_comparator(
786 (kind_a, task_a, lru_score_a): &(TaskSourceKind, ResolvedTask, u32),
787 (kind_b, task_b, lru_score_b): &(TaskSourceKind, ResolvedTask, u32),
788) -> cmp::Ordering {
789 lru_score_a
790 // First, display recently used templates above all.
791 .cmp(lru_score_b)
792 // Then, ensure more specific sources are displayed first.
793 .then(task_source_kind_preference(kind_a).cmp(&task_source_kind_preference(kind_b)))
794 // After that, display first more specific tasks, using more template variables.
795 // Bonus points for tasks with symbol variables.
796 .then(task_variables_preference(task_a).cmp(&task_variables_preference(task_b)))
797 // Finally, sort by the resolved label, but a bit more specifically, to avoid mixing letters and digits.
798 .then({
799 NumericPrefixWithSuffix::from_numeric_prefixed_str(&task_a.resolved_label)
800 .cmp(&NumericPrefixWithSuffix::from_numeric_prefixed_str(
801 &task_b.resolved_label,
802 ))
803 .then(task_a.resolved_label.cmp(&task_b.resolved_label))
804 .then(kind_a.cmp(kind_b))
805 })
806}
807
808fn task_source_kind_preference(kind: &TaskSourceKind) -> u32 {
809 match kind {
810 TaskSourceKind::Lsp { .. } => 0,
811 TaskSourceKind::Language { .. } => 1,
812 TaskSourceKind::UserInput => 2,
813 TaskSourceKind::Worktree { .. } => 3,
814 TaskSourceKind::AbsPath { .. } => 4,
815 }
816}
817
818fn task_variables_preference(task: &ResolvedTask) -> Reverse<usize> {
819 let task_variables = task.substituted_variables();
820 Reverse(if task_variables.contains(&VariableName::Symbol) {
821 task_variables.len() + 1
822 } else {
823 task_variables.len()
824 })
825}
826
827#[cfg(test)]
828mod test_inventory {
829 use gpui::{AppContext as _, Entity, Task, TestAppContext};
830 use itertools::Itertools;
831 use task::TaskContext;
832 use worktree::WorktreeId;
833
834 use crate::Inventory;
835
836 use super::TaskSourceKind;
837
838 pub(super) fn task_template_names(
839 inventory: &Entity<Inventory>,
840 worktree: Option<WorktreeId>,
841 cx: &mut TestAppContext,
842 ) -> Task<Vec<String>> {
843 let new_tasks = inventory.update(cx, |inventory, cx| {
844 inventory.list_tasks(None, None, worktree, cx)
845 });
846 cx.background_spawn(async move {
847 new_tasks
848 .await
849 .into_iter()
850 .map(|(_, task)| task.label)
851 .sorted()
852 .collect()
853 })
854 }
855
856 pub(super) fn register_task_used(
857 inventory: &Entity<Inventory>,
858 task_name: &str,
859 cx: &mut TestAppContext,
860 ) -> Task<()> {
861 let tasks = inventory.update(cx, |inventory, cx| {
862 inventory.list_tasks(None, None, None, cx)
863 });
864
865 let task_name = task_name.to_owned();
866 let inventory = inventory.clone();
867 cx.spawn(|mut cx| async move {
868 let (task_source_kind, task) = tasks
869 .await
870 .into_iter()
871 .find(|(_, task)| task.label == task_name)
872 .unwrap_or_else(|| panic!("Failed to find task with name {task_name}"));
873
874 let id_base = task_source_kind.to_id_base();
875 inventory
876 .update(&mut cx, |inventory, _| {
877 inventory.task_scheduled(
878 task_source_kind.clone(),
879 task.resolve_task(&id_base, &TaskContext::default())
880 .unwrap_or_else(|| {
881 panic!("Failed to resolve task with name {task_name}")
882 }),
883 )
884 })
885 .unwrap();
886 })
887 }
888
889 pub(super) fn register_worktree_task_used(
890 inventory: &Entity<Inventory>,
891 worktree_id: WorktreeId,
892 task_name: &str,
893 cx: &mut TestAppContext,
894 ) -> Task<()> {
895 let tasks = inventory.update(cx, |inventory, cx| {
896 inventory.list_tasks(None, None, Some(worktree_id), cx)
897 });
898
899 let inventory = inventory.clone();
900 let task_name = task_name.to_owned();
901 cx.spawn(|mut cx| async move {
902 let (task_source_kind, task) = tasks
903 .await
904 .into_iter()
905 .find(|(_, task)| task.label == task_name)
906 .unwrap_or_else(|| panic!("Failed to find task with name {task_name}"));
907 let id_base = task_source_kind.to_id_base();
908 inventory
909 .update(&mut cx, |inventory, _| {
910 inventory.task_scheduled(
911 task_source_kind.clone(),
912 task.resolve_task(&id_base, &TaskContext::default())
913 .unwrap_or_else(|| {
914 panic!("Failed to resolve task with name {task_name}")
915 }),
916 );
917 })
918 .unwrap();
919 })
920 }
921
922 pub(super) async fn list_tasks(
923 inventory: &Entity<Inventory>,
924 worktree: Option<WorktreeId>,
925 cx: &mut TestAppContext,
926 ) -> Vec<(TaskSourceKind, String)> {
927 let task_context = &TaskContext::default();
928 inventory
929 .update(cx, |inventory, cx| {
930 inventory.list_tasks(None, None, worktree, cx)
931 })
932 .await
933 .into_iter()
934 .filter_map(|(source_kind, task)| {
935 let id_base = source_kind.to_id_base();
936 Some((source_kind, task.resolve_task(&id_base, task_context)?))
937 })
938 .map(|(source_kind, resolved_task)| (source_kind, resolved_task.resolved_label))
939 .collect()
940 }
941}
942
943/// A context provided that tries to provide values for all non-custom [`VariableName`] variants for a currently opened file.
944/// Applied as a base for every custom [`ContextProvider`] unless explicitly oped out.
945pub struct BasicContextProvider {
946 worktree_store: Entity<WorktreeStore>,
947}
948
949impl BasicContextProvider {
950 pub fn new(worktree_store: Entity<WorktreeStore>) -> Self {
951 Self { worktree_store }
952 }
953}
954impl ContextProvider for BasicContextProvider {
955 fn build_context(
956 &self,
957 _: &TaskVariables,
958 location: ContextLocation<'_>,
959 _: Option<HashMap<String, String>>,
960 _: Arc<dyn LanguageToolchainStore>,
961 cx: &mut App,
962 ) -> Task<Result<TaskVariables>> {
963 let location = location.file_location;
964 let buffer = location.buffer.read(cx);
965 let buffer_snapshot = buffer.snapshot();
966 let symbols = buffer_snapshot.symbols_containing(location.range.start, None);
967 let symbol = symbols.unwrap_or_default().last().map(|symbol| {
968 let range = symbol
969 .name_ranges
970 .last()
971 .cloned()
972 .unwrap_or(0..symbol.text.len());
973 symbol.text[range].to_string()
974 });
975
976 let current_file = buffer
977 .file()
978 .and_then(|file| file.as_local())
979 .map(|file| file.abs_path(cx).to_sanitized_string());
980 let Point { row, column } = location.range.start.to_point(&buffer_snapshot);
981 let row = row + 1;
982 let column = column + 1;
983 let selected_text = buffer
984 .chars_for_range(location.range.clone())
985 .collect::<String>();
986
987 let mut task_variables = TaskVariables::from_iter([
988 (VariableName::Row, row.to_string()),
989 (VariableName::Column, column.to_string()),
990 ]);
991
992 if let Some(symbol) = symbol {
993 task_variables.insert(VariableName::Symbol, symbol);
994 }
995 if !selected_text.trim().is_empty() {
996 task_variables.insert(VariableName::SelectedText, selected_text);
997 }
998 let worktree_root_dir =
999 buffer
1000 .file()
1001 .map(|file| file.worktree_id(cx))
1002 .and_then(|worktree_id| {
1003 self.worktree_store
1004 .read(cx)
1005 .worktree_for_id(worktree_id, cx)
1006 .and_then(|worktree| worktree.read(cx).root_dir())
1007 });
1008 if let Some(worktree_path) = worktree_root_dir {
1009 task_variables.insert(
1010 VariableName::WorktreeRoot,
1011 worktree_path.to_sanitized_string(),
1012 );
1013 if let Some(full_path) = current_file.as_ref() {
1014 let relative_path = pathdiff::diff_paths(full_path, worktree_path);
1015 if let Some(relative_file) = relative_path {
1016 task_variables.insert(
1017 VariableName::RelativeFile,
1018 relative_file.to_sanitized_string(),
1019 );
1020 if let Some(relative_dir) = relative_file.parent() {
1021 task_variables.insert(
1022 VariableName::RelativeDir,
1023 if relative_dir.as_os_str().is_empty() {
1024 String::from(".")
1025 } else {
1026 relative_dir.to_sanitized_string()
1027 },
1028 );
1029 }
1030 }
1031 }
1032 }
1033
1034 if let Some(path_as_string) = current_file {
1035 let path = Path::new(&path_as_string);
1036 if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
1037 task_variables.insert(VariableName::Filename, String::from(filename));
1038 }
1039
1040 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
1041 task_variables.insert(VariableName::Stem, stem.into());
1042 }
1043
1044 if let Some(dirname) = path.parent().and_then(|s| s.to_str()) {
1045 task_variables.insert(VariableName::Dirname, dirname.into());
1046 }
1047
1048 task_variables.insert(VariableName::File, path_as_string);
1049 }
1050
1051 Task::ready(Ok(task_variables))
1052 }
1053}
1054
1055/// A ContextProvider that doesn't provide any task variables on it's own, though it has some associated tasks.
1056pub struct ContextProviderWithTasks {
1057 templates: TaskTemplates,
1058}
1059
1060impl ContextProviderWithTasks {
1061 pub fn new(definitions: TaskTemplates) -> Self {
1062 Self {
1063 templates: definitions,
1064 }
1065 }
1066}
1067
1068impl ContextProvider for ContextProviderWithTasks {
1069 fn associated_tasks(
1070 &self,
1071 _: Arc<dyn Fs>,
1072 _: Option<Arc<dyn File>>,
1073 _: &App,
1074 ) -> Task<Option<TaskTemplates>> {
1075 Task::ready(Some(self.templates.clone()))
1076 }
1077}
1078
1079#[cfg(test)]
1080mod tests {
1081 use fs::FakeFs;
1082 use gpui::TestAppContext;
1083 use paths::tasks_file;
1084 use pretty_assertions::assert_eq;
1085 use serde_json::json;
1086 use settings::SettingsLocation;
1087
1088 use crate::task_store::TaskStore;
1089
1090 use super::test_inventory::*;
1091 use super::*;
1092
1093 #[gpui::test]
1094 async fn test_task_list_sorting(cx: &mut TestAppContext) {
1095 init_test(cx);
1096 let fs = FakeFs::new(cx.executor());
1097 let inventory = cx.update(|cx| Inventory::new(fs, cx));
1098 let initial_tasks = resolved_task_names(&inventory, None, cx).await;
1099 assert!(
1100 initial_tasks.is_empty(),
1101 "No tasks expected for empty inventory, but got {initial_tasks:?}"
1102 );
1103 let initial_tasks = task_template_names(&inventory, None, cx).await;
1104 assert!(
1105 initial_tasks.is_empty(),
1106 "No tasks expected for empty inventory, but got {initial_tasks:?}"
1107 );
1108 cx.run_until_parked();
1109 let expected_initial_state = [
1110 "1_a_task".to_string(),
1111 "1_task".to_string(),
1112 "2_task".to_string(),
1113 "3_task".to_string(),
1114 ];
1115
1116 inventory.update(cx, |inventory, _| {
1117 inventory
1118 .update_file_based_tasks(
1119 TaskSettingsLocation::Global(tasks_file()),
1120 Some(&mock_tasks_from_names(
1121 expected_initial_state.iter().map(|name| name.as_str()),
1122 )),
1123 )
1124 .unwrap();
1125 });
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 &expected_initial_state,
1133 "Tasks with equal amount of usages should be sorted alphanumerically"
1134 );
1135
1136 register_task_used(&inventory, "2_task", cx).await;
1137 assert_eq!(
1138 task_template_names(&inventory, None, cx).await,
1139 &expected_initial_state,
1140 );
1141 assert_eq!(
1142 resolved_task_names(&inventory, None, cx).await,
1143 vec![
1144 "2_task".to_string(),
1145 "1_a_task".to_string(),
1146 "1_task".to_string(),
1147 "3_task".to_string()
1148 ],
1149 );
1150
1151 register_task_used(&inventory, "1_task", cx).await;
1152 register_task_used(&inventory, "1_task", cx).await;
1153 register_task_used(&inventory, "1_task", cx).await;
1154 register_task_used(&inventory, "3_task", cx).await;
1155 assert_eq!(
1156 task_template_names(&inventory, None, cx).await,
1157 &expected_initial_state,
1158 );
1159 assert_eq!(
1160 resolved_task_names(&inventory, None, cx).await,
1161 vec![
1162 "3_task".to_string(),
1163 "1_task".to_string(),
1164 "2_task".to_string(),
1165 "1_a_task".to_string(),
1166 ],
1167 "Most recently used task should be at the top"
1168 );
1169
1170 let worktree_id = WorktreeId::from_usize(0);
1171 let local_worktree_location = SettingsLocation {
1172 worktree_id,
1173 path: Path::new("foo"),
1174 };
1175 inventory.update(cx, |inventory, _| {
1176 inventory
1177 .update_file_based_tasks(
1178 TaskSettingsLocation::Worktree(local_worktree_location),
1179 Some(&mock_tasks_from_names(["worktree_task_1"])),
1180 )
1181 .unwrap();
1182 });
1183 assert_eq!(
1184 resolved_task_names(&inventory, None, cx).await,
1185 vec![
1186 "3_task".to_string(),
1187 "1_task".to_string(),
1188 "2_task".to_string(),
1189 "1_a_task".to_string(),
1190 ],
1191 "Most recently used task should be at the top"
1192 );
1193 assert_eq!(
1194 resolved_task_names(&inventory, Some(worktree_id), cx).await,
1195 vec![
1196 "3_task".to_string(),
1197 "1_task".to_string(),
1198 "2_task".to_string(),
1199 "worktree_task_1".to_string(),
1200 "1_a_task".to_string(),
1201 ],
1202 );
1203 register_worktree_task_used(&inventory, worktree_id, "worktree_task_1", cx).await;
1204 assert_eq!(
1205 resolved_task_names(&inventory, Some(worktree_id), cx).await,
1206 vec![
1207 "worktree_task_1".to_string(),
1208 "3_task".to_string(),
1209 "1_task".to_string(),
1210 "2_task".to_string(),
1211 "1_a_task".to_string(),
1212 ],
1213 "Most recently used worktree task should be at the top"
1214 );
1215
1216 inventory.update(cx, |inventory, _| {
1217 inventory
1218 .update_file_based_tasks(
1219 TaskSettingsLocation::Global(tasks_file()),
1220 Some(&mock_tasks_from_names(
1221 ["10_hello", "11_hello"]
1222 .into_iter()
1223 .chain(expected_initial_state.iter().map(|name| name.as_str())),
1224 )),
1225 )
1226 .unwrap();
1227 });
1228 cx.run_until_parked();
1229 let expected_updated_state = [
1230 "10_hello".to_string(),
1231 "11_hello".to_string(),
1232 "1_a_task".to_string(),
1233 "1_task".to_string(),
1234 "2_task".to_string(),
1235 "3_task".to_string(),
1236 ];
1237 assert_eq!(
1238 task_template_names(&inventory, None, cx).await,
1239 &expected_updated_state,
1240 );
1241 assert_eq!(
1242 resolved_task_names(&inventory, None, cx).await,
1243 vec![
1244 "worktree_task_1".to_string(),
1245 "1_a_task".to_string(),
1246 "1_task".to_string(),
1247 "2_task".to_string(),
1248 "3_task".to_string(),
1249 "10_hello".to_string(),
1250 "11_hello".to_string(),
1251 ],
1252 "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"
1253 );
1254
1255 register_task_used(&inventory, "11_hello", cx).await;
1256 assert_eq!(
1257 task_template_names(&inventory, None, cx).await,
1258 &expected_updated_state,
1259 );
1260 assert_eq!(
1261 resolved_task_names(&inventory, None, cx).await,
1262 vec![
1263 "11_hello".to_string(),
1264 "worktree_task_1".to_string(),
1265 "1_a_task".to_string(),
1266 "1_task".to_string(),
1267 "2_task".to_string(),
1268 "3_task".to_string(),
1269 "10_hello".to_string(),
1270 ],
1271 );
1272 }
1273
1274 #[gpui::test]
1275 async fn test_reloading_debug_scenarios(cx: &mut TestAppContext) {
1276 init_test(cx);
1277 let fs = FakeFs::new(cx.executor());
1278 let inventory = cx.update(|cx| Inventory::new(fs, cx));
1279 inventory.update(cx, |inventory, cx| {
1280 inventory
1281 .update_file_based_scenarios(
1282 TaskSettingsLocation::Global(Path::new("")),
1283 Some(
1284 r#"
1285 [{
1286 "label": "test scenario",
1287 "adapter": "CodeLLDB",
1288 "request": "launch",
1289 "program": "wowzer",
1290 }]
1291 "#,
1292 ),
1293 )
1294 .unwrap();
1295
1296 let (_, scenario) = inventory
1297 .list_debug_scenarios(&TaskContexts::default(), vec![], vec![], false, cx)
1298 .1
1299 .first()
1300 .unwrap()
1301 .clone();
1302
1303 inventory.scenario_scheduled(scenario.clone());
1304
1305 assert_eq!(
1306 inventory
1307 .list_debug_scenarios(&TaskContexts::default(), vec![], vec![], false, cx)
1308 .0
1309 .first()
1310 .unwrap()
1311 .clone(),
1312 scenario
1313 );
1314
1315 inventory
1316 .update_file_based_scenarios(
1317 TaskSettingsLocation::Global(Path::new("")),
1318 Some(
1319 r#"
1320 [{
1321 "label": "test scenario",
1322 "adapter": "Delve",
1323 "request": "launch",
1324 "program": "wowzer",
1325 }]
1326 "#,
1327 ),
1328 )
1329 .unwrap();
1330
1331 assert_eq!(
1332 inventory
1333 .list_debug_scenarios(&TaskContexts::default(), vec![], vec![], false, cx)
1334 .0
1335 .first()
1336 .unwrap()
1337 .adapter,
1338 "Delve",
1339 );
1340
1341 inventory
1342 .update_file_based_scenarios(
1343 TaskSettingsLocation::Global(Path::new("")),
1344 Some(
1345 r#"
1346 [{
1347 "label": "testing scenario",
1348 "adapter": "Delve",
1349 "request": "launch",
1350 "program": "wowzer",
1351 }]
1352 "#,
1353 ),
1354 )
1355 .unwrap();
1356
1357 assert_eq!(
1358 inventory
1359 .list_debug_scenarios(&TaskContexts::default(), vec![], vec![], false, cx)
1360 .0
1361 .first(),
1362 None
1363 );
1364 });
1365 }
1366
1367 #[gpui::test]
1368 async fn test_inventory_static_task_filters(cx: &mut TestAppContext) {
1369 init_test(cx);
1370 let fs = FakeFs::new(cx.executor());
1371 let inventory = cx.update(|cx| Inventory::new(fs, cx));
1372 let common_name = "common_task_name";
1373 let worktree_1 = WorktreeId::from_usize(1);
1374 let worktree_2 = WorktreeId::from_usize(2);
1375
1376 cx.run_until_parked();
1377 let worktree_independent_tasks = vec![
1378 (
1379 TaskSourceKind::AbsPath {
1380 id_base: "global tasks.json".into(),
1381 abs_path: paths::tasks_file().clone(),
1382 },
1383 common_name.to_string(),
1384 ),
1385 (
1386 TaskSourceKind::AbsPath {
1387 id_base: "global tasks.json".into(),
1388 abs_path: paths::tasks_file().clone(),
1389 },
1390 "static_source_1".to_string(),
1391 ),
1392 (
1393 TaskSourceKind::AbsPath {
1394 id_base: "global tasks.json".into(),
1395 abs_path: paths::tasks_file().clone(),
1396 },
1397 "static_source_2".to_string(),
1398 ),
1399 ];
1400 let worktree_1_tasks = [
1401 (
1402 TaskSourceKind::Worktree {
1403 id: worktree_1,
1404 directory_in_worktree: PathBuf::from(".zed"),
1405 id_base: "local worktree tasks from directory \".zed\"".into(),
1406 },
1407 common_name.to_string(),
1408 ),
1409 (
1410 TaskSourceKind::Worktree {
1411 id: worktree_1,
1412 directory_in_worktree: PathBuf::from(".zed"),
1413 id_base: "local worktree tasks from directory \".zed\"".into(),
1414 },
1415 "worktree_1".to_string(),
1416 ),
1417 ];
1418 let worktree_2_tasks = [
1419 (
1420 TaskSourceKind::Worktree {
1421 id: worktree_2,
1422 directory_in_worktree: PathBuf::from(".zed"),
1423 id_base: "local worktree tasks from directory \".zed\"".into(),
1424 },
1425 common_name.to_string(),
1426 ),
1427 (
1428 TaskSourceKind::Worktree {
1429 id: worktree_2,
1430 directory_in_worktree: PathBuf::from(".zed"),
1431 id_base: "local worktree tasks from directory \".zed\"".into(),
1432 },
1433 "worktree_2".to_string(),
1434 ),
1435 ];
1436
1437 inventory.update(cx, |inventory, _| {
1438 inventory
1439 .update_file_based_tasks(
1440 TaskSettingsLocation::Global(tasks_file()),
1441 Some(&mock_tasks_from_names(
1442 worktree_independent_tasks
1443 .iter()
1444 .map(|(_, name)| name.as_str()),
1445 )),
1446 )
1447 .unwrap();
1448 inventory
1449 .update_file_based_tasks(
1450 TaskSettingsLocation::Worktree(SettingsLocation {
1451 worktree_id: worktree_1,
1452 path: Path::new(".zed"),
1453 }),
1454 Some(&mock_tasks_from_names(
1455 worktree_1_tasks.iter().map(|(_, name)| name.as_str()),
1456 )),
1457 )
1458 .unwrap();
1459 inventory
1460 .update_file_based_tasks(
1461 TaskSettingsLocation::Worktree(SettingsLocation {
1462 worktree_id: worktree_2,
1463 path: Path::new(".zed"),
1464 }),
1465 Some(&mock_tasks_from_names(
1466 worktree_2_tasks.iter().map(|(_, name)| name.as_str()),
1467 )),
1468 )
1469 .unwrap();
1470 });
1471
1472 assert_eq!(
1473 list_tasks_sorted_by_last_used(&inventory, None, cx).await,
1474 worktree_independent_tasks,
1475 "Without a worktree, only worktree-independent tasks should be listed"
1476 );
1477 assert_eq!(
1478 list_tasks_sorted_by_last_used(&inventory, Some(worktree_1), cx).await,
1479 worktree_1_tasks
1480 .iter()
1481 .chain(worktree_independent_tasks.iter())
1482 .cloned()
1483 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
1484 .collect::<Vec<_>>(),
1485 );
1486 assert_eq!(
1487 list_tasks_sorted_by_last_used(&inventory, Some(worktree_2), cx).await,
1488 worktree_2_tasks
1489 .iter()
1490 .chain(worktree_independent_tasks.iter())
1491 .cloned()
1492 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
1493 .collect::<Vec<_>>(),
1494 );
1495
1496 assert_eq!(
1497 list_tasks(&inventory, None, cx).await,
1498 worktree_independent_tasks,
1499 "Without a worktree, only worktree-independent tasks should be listed"
1500 );
1501 assert_eq!(
1502 list_tasks(&inventory, Some(worktree_1), cx).await,
1503 worktree_1_tasks
1504 .iter()
1505 .chain(worktree_independent_tasks.iter())
1506 .cloned()
1507 .collect::<Vec<_>>(),
1508 );
1509 assert_eq!(
1510 list_tasks(&inventory, Some(worktree_2), cx).await,
1511 worktree_2_tasks
1512 .iter()
1513 .chain(worktree_independent_tasks.iter())
1514 .cloned()
1515 .collect::<Vec<_>>(),
1516 );
1517 }
1518
1519 fn init_test(_cx: &mut TestAppContext) {
1520 zlog::init_test();
1521 TaskStore::init(None);
1522 }
1523
1524 fn resolved_task_names(
1525 inventory: &Entity<Inventory>,
1526 worktree: Option<WorktreeId>,
1527 cx: &mut TestAppContext,
1528 ) -> Task<Vec<String>> {
1529 let tasks = inventory.update(cx, |inventory, cx| {
1530 let mut task_contexts = TaskContexts::default();
1531 task_contexts.active_worktree_context =
1532 worktree.map(|worktree| (worktree, TaskContext::default()));
1533
1534 inventory.used_and_current_resolved_tasks(Arc::new(task_contexts), cx)
1535 });
1536
1537 cx.background_spawn(async move {
1538 let (used, current) = tasks.await;
1539 used.into_iter()
1540 .chain(current)
1541 .map(|(_, task)| task.original_task().label.clone())
1542 .collect()
1543 })
1544 }
1545
1546 fn mock_tasks_from_names<'a>(task_names: impl IntoIterator<Item = &'a str> + 'a) -> String {
1547 serde_json::to_string(&serde_json::Value::Array(
1548 task_names
1549 .into_iter()
1550 .map(|task_name| {
1551 json!({
1552 "label": task_name,
1553 "command": "echo",
1554 "args": vec![task_name],
1555 })
1556 })
1557 .collect::<Vec<_>>(),
1558 ))
1559 .unwrap()
1560 }
1561
1562 async fn list_tasks_sorted_by_last_used(
1563 inventory: &Entity<Inventory>,
1564 worktree: Option<WorktreeId>,
1565 cx: &mut TestAppContext,
1566 ) -> Vec<(TaskSourceKind, String)> {
1567 let (used, current) = inventory
1568 .update(cx, |inventory, cx| {
1569 let mut task_contexts = TaskContexts::default();
1570 task_contexts.active_worktree_context =
1571 worktree.map(|worktree| (worktree, TaskContext::default()));
1572
1573 inventory.used_and_current_resolved_tasks(Arc::new(task_contexts), cx)
1574 })
1575 .await;
1576 let mut all = used;
1577 all.extend(current);
1578 all.into_iter()
1579 .map(|(source_kind, task)| (source_kind, task.resolved_label))
1580 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
1581 .collect()
1582 }
1583}