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