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