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