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