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