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