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