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