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