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