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