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