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