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 self.last_scheduled_tasks.retain(|(kind, _)| {
590 if let TaskSourceKind::AbsPath { abs_path, .. } = kind {
591 abs_path != path
592 } else {
593 true
594 }
595 });
596 }
597 TaskSettingsLocation::Worktree(location) => {
598 let new_templates = new_templates.collect::<Vec<_>>();
599 if new_templates.is_empty() {
600 if let Some(worktree_tasks) =
601 parsed_templates.worktree.get_mut(&location.worktree_id)
602 {
603 worktree_tasks.remove(location.path);
604 }
605 } else {
606 parsed_templates
607 .worktree
608 .entry(location.worktree_id)
609 .or_default()
610 .insert(Arc::from(location.path), new_templates);
611 }
612 self.last_scheduled_tasks.retain(|(kind, _)| {
613 if let TaskSourceKind::Worktree {
614 directory_in_worktree,
615 id,
616 ..
617 } = kind
618 {
619 *id != location.worktree_id || directory_in_worktree != location.path
620 } else {
621 true
622 }
623 });
624 }
625 }
626
627 Ok(())
628 }
629
630 /// Updates in-memory task metadata from the JSON string given.
631 /// Will fail if the JSON is not a valid array of objects, but will continue if any object will not parse into a [`TaskTemplate`].
632 ///
633 /// Global tasks are updated for no worktree provided, otherwise the worktree metadata for a given path will be updated.
634 pub(crate) fn update_file_based_scenarios(
635 &mut self,
636 location: TaskSettingsLocation<'_>,
637 raw_tasks_json: Option<&str>,
638 ) -> Result<(), InvalidSettingsError> {
639 let raw_tasks = match parse_json_with_comments::<Vec<serde_json::Value>>(
640 raw_tasks_json.unwrap_or("[]"),
641 ) {
642 Ok(tasks) => tasks,
643 Err(e) => {
644 return Err(InvalidSettingsError::Debug {
645 path: match location {
646 TaskSettingsLocation::Global(path) => path.to_owned(),
647 TaskSettingsLocation::Worktree(settings_location) => {
648 settings_location.path.join(debug_task_file_name())
649 }
650 },
651 message: format!("Failed to parse tasks file content as a JSON array: {e}"),
652 });
653 }
654 };
655
656 let new_templates = raw_tasks.into_iter().filter_map(|raw_template| {
657 serde_json::from_value::<DebugScenario>(raw_template).log_err()
658 });
659
660 let parsed_scenarios = &mut self.scenarios_from_settings;
661 match location {
662 TaskSettingsLocation::Global(path) => {
663 parsed_scenarios
664 .global
665 .entry(path.to_owned())
666 .insert_entry(new_templates.collect());
667 }
668 TaskSettingsLocation::Worktree(location) => {
669 let new_templates = new_templates.collect::<Vec<_>>();
670 if new_templates.is_empty() {
671 if let Some(worktree_tasks) =
672 parsed_scenarios.worktree.get_mut(&location.worktree_id)
673 {
674 worktree_tasks.remove(location.path);
675 }
676 } else {
677 parsed_scenarios
678 .worktree
679 .entry(location.worktree_id)
680 .or_default()
681 .insert(Arc::from(location.path), new_templates);
682 }
683 }
684 }
685
686 Ok(())
687 }
688}
689
690fn task_lru_comparator(
691 (kind_a, task_a, lru_score_a): &(TaskSourceKind, ResolvedTask, u32),
692 (kind_b, task_b, lru_score_b): &(TaskSourceKind, ResolvedTask, u32),
693) -> cmp::Ordering {
694 lru_score_a
695 // First, display recently used templates above all.
696 .cmp(lru_score_b)
697 // Then, ensure more specific sources are displayed first.
698 .then(task_source_kind_preference(kind_a).cmp(&task_source_kind_preference(kind_b)))
699 // After that, display first more specific tasks, using more template variables.
700 // Bonus points for tasks with symbol variables.
701 .then(task_variables_preference(task_a).cmp(&task_variables_preference(task_b)))
702 // Finally, sort by the resolved label, but a bit more specifically, to avoid mixing letters and digits.
703 .then({
704 NumericPrefixWithSuffix::from_numeric_prefixed_str(&task_a.resolved_label)
705 .cmp(&NumericPrefixWithSuffix::from_numeric_prefixed_str(
706 &task_b.resolved_label,
707 ))
708 .then(task_a.resolved_label.cmp(&task_b.resolved_label))
709 .then(kind_a.cmp(kind_b))
710 })
711}
712
713fn task_source_kind_preference(kind: &TaskSourceKind) -> u32 {
714 match kind {
715 TaskSourceKind::Lsp(..) => 0,
716 TaskSourceKind::Language { .. } => 1,
717 TaskSourceKind::UserInput => 2,
718 TaskSourceKind::Worktree { .. } => 3,
719 TaskSourceKind::AbsPath { .. } => 4,
720 }
721}
722
723fn task_variables_preference(task: &ResolvedTask) -> Reverse<usize> {
724 let task_variables = task.substituted_variables();
725 Reverse(if task_variables.contains(&VariableName::Symbol) {
726 task_variables.len() + 1
727 } else {
728 task_variables.len()
729 })
730}
731
732#[cfg(test)]
733mod test_inventory {
734 use gpui::{Entity, TestAppContext};
735 use itertools::Itertools;
736 use task::TaskContext;
737 use worktree::WorktreeId;
738
739 use crate::Inventory;
740
741 use super::TaskSourceKind;
742
743 pub(super) fn task_template_names(
744 inventory: &Entity<Inventory>,
745 worktree: Option<WorktreeId>,
746 cx: &mut TestAppContext,
747 ) -> Vec<String> {
748 inventory.update(cx, |inventory, cx| {
749 inventory
750 .list_tasks(None, None, worktree, cx)
751 .into_iter()
752 .map(|(_, task)| task.label)
753 .sorted()
754 .collect()
755 })
756 }
757
758 pub(super) fn register_task_used(
759 inventory: &Entity<Inventory>,
760 task_name: &str,
761 cx: &mut TestAppContext,
762 ) {
763 inventory.update(cx, |inventory, cx| {
764 let (task_source_kind, task) = inventory
765 .list_tasks(None, None, None, cx)
766 .into_iter()
767 .find(|(_, task)| task.label == task_name)
768 .unwrap_or_else(|| panic!("Failed to find task with name {task_name}"));
769 let id_base = task_source_kind.to_id_base();
770 inventory.task_scheduled(
771 task_source_kind.clone(),
772 task.resolve_task(&id_base, &TaskContext::default())
773 .unwrap_or_else(|| panic!("Failed to resolve task with name {task_name}")),
774 );
775 });
776 }
777
778 pub(super) fn register_worktree_task_used(
779 inventory: &Entity<Inventory>,
780 worktree_id: WorktreeId,
781 task_name: &str,
782 cx: &mut TestAppContext,
783 ) {
784 inventory.update(cx, |inventory, cx| {
785 let (task_source_kind, task) = inventory
786 .list_tasks(None, None, Some(worktree_id), cx)
787 .into_iter()
788 .find(|(_, task)| task.label == task_name)
789 .unwrap_or_else(|| panic!("Failed to find task with name {task_name}"));
790 let id_base = task_source_kind.to_id_base();
791 inventory.task_scheduled(
792 task_source_kind.clone(),
793 task.resolve_task(&id_base, &TaskContext::default())
794 .unwrap_or_else(|| panic!("Failed to resolve task with name {task_name}")),
795 );
796 });
797 }
798
799 pub(super) async fn list_tasks(
800 inventory: &Entity<Inventory>,
801 worktree: Option<WorktreeId>,
802 cx: &mut TestAppContext,
803 ) -> Vec<(TaskSourceKind, String)> {
804 inventory.update(cx, |inventory, cx| {
805 let task_context = &TaskContext::default();
806 inventory
807 .list_tasks(None, None, worktree, cx)
808 .into_iter()
809 .filter_map(|(source_kind, task)| {
810 let id_base = source_kind.to_id_base();
811 Some((source_kind, task.resolve_task(&id_base, task_context)?))
812 })
813 .map(|(source_kind, resolved_task)| (source_kind, resolved_task.resolved_label))
814 .collect()
815 })
816 }
817}
818
819/// A context provided that tries to provide values for all non-custom [`VariableName`] variants for a currently opened file.
820/// Applied as a base for every custom [`ContextProvider`] unless explicitly oped out.
821pub struct BasicContextProvider {
822 worktree_store: Entity<WorktreeStore>,
823}
824
825impl BasicContextProvider {
826 pub fn new(worktree_store: Entity<WorktreeStore>) -> Self {
827 Self { worktree_store }
828 }
829}
830impl ContextProvider for BasicContextProvider {
831 fn build_context(
832 &self,
833 _: &TaskVariables,
834 location: ContextLocation<'_>,
835 _: Option<HashMap<String, String>>,
836 _: Arc<dyn LanguageToolchainStore>,
837 cx: &mut App,
838 ) -> Task<Result<TaskVariables>> {
839 let location = location.file_location;
840 let buffer = location.buffer.read(cx);
841 let buffer_snapshot = buffer.snapshot();
842 let symbols = buffer_snapshot.symbols_containing(location.range.start, None);
843 let symbol = symbols.unwrap_or_default().last().map(|symbol| {
844 let range = symbol
845 .name_ranges
846 .last()
847 .cloned()
848 .unwrap_or(0..symbol.text.len());
849 symbol.text[range].to_string()
850 });
851
852 let current_file = buffer
853 .file()
854 .and_then(|file| file.as_local())
855 .map(|file| file.abs_path(cx).to_sanitized_string());
856 let Point { row, column } = location.range.start.to_point(&buffer_snapshot);
857 let row = row + 1;
858 let column = column + 1;
859 let selected_text = buffer
860 .chars_for_range(location.range.clone())
861 .collect::<String>();
862
863 let mut task_variables = TaskVariables::from_iter([
864 (VariableName::Row, row.to_string()),
865 (VariableName::Column, column.to_string()),
866 ]);
867
868 if let Some(symbol) = symbol {
869 task_variables.insert(VariableName::Symbol, symbol);
870 }
871 if !selected_text.trim().is_empty() {
872 task_variables.insert(VariableName::SelectedText, selected_text);
873 }
874 let worktree_root_dir =
875 buffer
876 .file()
877 .map(|file| file.worktree_id(cx))
878 .and_then(|worktree_id| {
879 self.worktree_store
880 .read(cx)
881 .worktree_for_id(worktree_id, cx)
882 .and_then(|worktree| worktree.read(cx).root_dir())
883 });
884 if let Some(worktree_path) = worktree_root_dir {
885 task_variables.insert(
886 VariableName::WorktreeRoot,
887 worktree_path.to_sanitized_string(),
888 );
889 if let Some(full_path) = current_file.as_ref() {
890 let relative_path = pathdiff::diff_paths(full_path, worktree_path);
891 if let Some(relative_file) = relative_path {
892 task_variables.insert(
893 VariableName::RelativeFile,
894 relative_file.to_sanitized_string(),
895 );
896 if let Some(relative_dir) = relative_file.parent() {
897 task_variables.insert(
898 VariableName::RelativeDir,
899 if relative_dir.as_os_str().is_empty() {
900 String::from(".")
901 } else {
902 relative_dir.to_sanitized_string()
903 },
904 );
905 }
906 }
907 }
908 }
909
910 if let Some(path_as_string) = current_file {
911 let path = Path::new(&path_as_string);
912 if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
913 task_variables.insert(VariableName::Filename, String::from(filename));
914 }
915
916 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
917 task_variables.insert(VariableName::Stem, stem.into());
918 }
919
920 if let Some(dirname) = path.parent().and_then(|s| s.to_str()) {
921 task_variables.insert(VariableName::Dirname, dirname.into());
922 }
923
924 task_variables.insert(VariableName::File, path_as_string);
925 }
926
927 Task::ready(Ok(task_variables))
928 }
929}
930
931/// A ContextProvider that doesn't provide any task variables on it's own, though it has some associated tasks.
932pub struct ContextProviderWithTasks {
933 templates: TaskTemplates,
934}
935
936impl ContextProviderWithTasks {
937 pub fn new(definitions: TaskTemplates) -> Self {
938 Self {
939 templates: definitions,
940 }
941 }
942}
943
944impl ContextProvider for ContextProviderWithTasks {
945 fn associated_tasks(
946 &self,
947 _: Option<Arc<dyn language::File>>,
948 _: &App,
949 ) -> Option<TaskTemplates> {
950 Some(self.templates.clone())
951 }
952}
953
954#[cfg(test)]
955mod tests {
956 use gpui::TestAppContext;
957 use paths::tasks_file;
958 use pretty_assertions::assert_eq;
959 use serde_json::json;
960 use settings::SettingsLocation;
961
962 use crate::task_store::TaskStore;
963
964 use super::test_inventory::*;
965 use super::*;
966
967 #[gpui::test]
968 async fn test_task_list_sorting(cx: &mut TestAppContext) {
969 init_test(cx);
970 let inventory = cx.update(Inventory::new);
971 let initial_tasks = resolved_task_names(&inventory, None, cx);
972 assert!(
973 initial_tasks.is_empty(),
974 "No tasks expected for empty inventory, but got {initial_tasks:?}"
975 );
976 let initial_tasks = task_template_names(&inventory, None, cx);
977 assert!(
978 initial_tasks.is_empty(),
979 "No tasks expected for empty inventory, but got {initial_tasks:?}"
980 );
981 cx.run_until_parked();
982 let expected_initial_state = [
983 "1_a_task".to_string(),
984 "1_task".to_string(),
985 "2_task".to_string(),
986 "3_task".to_string(),
987 ];
988
989 inventory.update(cx, |inventory, _| {
990 inventory
991 .update_file_based_tasks(
992 TaskSettingsLocation::Global(tasks_file()),
993 Some(&mock_tasks_from_names(
994 expected_initial_state.iter().map(|name| name.as_str()),
995 )),
996 )
997 .unwrap();
998 });
999 assert_eq!(
1000 task_template_names(&inventory, None, cx),
1001 &expected_initial_state,
1002 );
1003 assert_eq!(
1004 resolved_task_names(&inventory, None, cx),
1005 &expected_initial_state,
1006 "Tasks with equal amount of usages should be sorted alphanumerically"
1007 );
1008
1009 register_task_used(&inventory, "2_task", cx);
1010 assert_eq!(
1011 task_template_names(&inventory, None, cx),
1012 &expected_initial_state,
1013 );
1014 assert_eq!(
1015 resolved_task_names(&inventory, None, cx),
1016 vec![
1017 "2_task".to_string(),
1018 "1_a_task".to_string(),
1019 "1_task".to_string(),
1020 "3_task".to_string()
1021 ],
1022 );
1023
1024 register_task_used(&inventory, "1_task", cx);
1025 register_task_used(&inventory, "1_task", cx);
1026 register_task_used(&inventory, "1_task", cx);
1027 register_task_used(&inventory, "3_task", cx);
1028 assert_eq!(
1029 task_template_names(&inventory, None, cx),
1030 &expected_initial_state,
1031 );
1032 assert_eq!(
1033 resolved_task_names(&inventory, None, cx),
1034 vec![
1035 "3_task".to_string(),
1036 "1_task".to_string(),
1037 "2_task".to_string(),
1038 "1_a_task".to_string(),
1039 ],
1040 "Most recently used task should be at the top"
1041 );
1042
1043 let worktree_id = WorktreeId::from_usize(0);
1044 let local_worktree_location = SettingsLocation {
1045 worktree_id,
1046 path: Path::new("foo"),
1047 };
1048 inventory.update(cx, |inventory, _| {
1049 inventory
1050 .update_file_based_tasks(
1051 TaskSettingsLocation::Worktree(local_worktree_location),
1052 Some(&mock_tasks_from_names(["worktree_task_1"])),
1053 )
1054 .unwrap();
1055 });
1056 assert_eq!(
1057 resolved_task_names(&inventory, None, cx),
1058 vec![
1059 "3_task".to_string(),
1060 "1_task".to_string(),
1061 "2_task".to_string(),
1062 "1_a_task".to_string(),
1063 ],
1064 "Most recently used task should be at the top"
1065 );
1066 assert_eq!(
1067 resolved_task_names(&inventory, Some(worktree_id), cx),
1068 vec![
1069 "3_task".to_string(),
1070 "1_task".to_string(),
1071 "2_task".to_string(),
1072 "worktree_task_1".to_string(),
1073 "1_a_task".to_string(),
1074 ],
1075 );
1076 register_worktree_task_used(&inventory, worktree_id, "worktree_task_1", cx);
1077 assert_eq!(
1078 resolved_task_names(&inventory, Some(worktree_id), cx),
1079 vec![
1080 "worktree_task_1".to_string(),
1081 "3_task".to_string(),
1082 "1_task".to_string(),
1083 "2_task".to_string(),
1084 "1_a_task".to_string(),
1085 ],
1086 "Most recently used worktree task should be at the top"
1087 );
1088
1089 inventory.update(cx, |inventory, _| {
1090 inventory
1091 .update_file_based_tasks(
1092 TaskSettingsLocation::Global(tasks_file()),
1093 Some(&mock_tasks_from_names(
1094 ["10_hello", "11_hello"]
1095 .into_iter()
1096 .chain(expected_initial_state.iter().map(|name| name.as_str())),
1097 )),
1098 )
1099 .unwrap();
1100 });
1101 cx.run_until_parked();
1102 let expected_updated_state = [
1103 "10_hello".to_string(),
1104 "11_hello".to_string(),
1105 "1_a_task".to_string(),
1106 "1_task".to_string(),
1107 "2_task".to_string(),
1108 "3_task".to_string(),
1109 ];
1110 assert_eq!(
1111 task_template_names(&inventory, None, cx),
1112 &expected_updated_state,
1113 );
1114 assert_eq!(
1115 resolved_task_names(&inventory, None, cx),
1116 vec![
1117 "worktree_task_1".to_string(),
1118 "1_a_task".to_string(),
1119 "1_task".to_string(),
1120 "2_task".to_string(),
1121 "3_task".to_string(),
1122 "10_hello".to_string(),
1123 "11_hello".to_string(),
1124 ],
1125 "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"
1126 );
1127
1128 register_task_used(&inventory, "11_hello", cx);
1129 assert_eq!(
1130 task_template_names(&inventory, None, cx),
1131 &expected_updated_state,
1132 );
1133 assert_eq!(
1134 resolved_task_names(&inventory, None, cx),
1135 vec![
1136 "11_hello".to_string(),
1137 "worktree_task_1".to_string(),
1138 "1_a_task".to_string(),
1139 "1_task".to_string(),
1140 "2_task".to_string(),
1141 "3_task".to_string(),
1142 "10_hello".to_string(),
1143 ],
1144 );
1145 }
1146
1147 #[gpui::test]
1148 async fn test_inventory_static_task_filters(cx: &mut TestAppContext) {
1149 init_test(cx);
1150 let inventory = cx.update(Inventory::new);
1151 let common_name = "common_task_name";
1152 let worktree_1 = WorktreeId::from_usize(1);
1153 let worktree_2 = WorktreeId::from_usize(2);
1154
1155 cx.run_until_parked();
1156 let worktree_independent_tasks = vec![
1157 (
1158 TaskSourceKind::AbsPath {
1159 id_base: "global tasks.json".into(),
1160 abs_path: paths::tasks_file().clone(),
1161 },
1162 common_name.to_string(),
1163 ),
1164 (
1165 TaskSourceKind::AbsPath {
1166 id_base: "global tasks.json".into(),
1167 abs_path: paths::tasks_file().clone(),
1168 },
1169 "static_source_1".to_string(),
1170 ),
1171 (
1172 TaskSourceKind::AbsPath {
1173 id_base: "global tasks.json".into(),
1174 abs_path: paths::tasks_file().clone(),
1175 },
1176 "static_source_2".to_string(),
1177 ),
1178 ];
1179 let worktree_1_tasks = [
1180 (
1181 TaskSourceKind::Worktree {
1182 id: worktree_1,
1183 directory_in_worktree: PathBuf::from(".zed"),
1184 id_base: "local worktree tasks from directory \".zed\"".into(),
1185 },
1186 common_name.to_string(),
1187 ),
1188 (
1189 TaskSourceKind::Worktree {
1190 id: worktree_1,
1191 directory_in_worktree: PathBuf::from(".zed"),
1192 id_base: "local worktree tasks from directory \".zed\"".into(),
1193 },
1194 "worktree_1".to_string(),
1195 ),
1196 ];
1197 let worktree_2_tasks = [
1198 (
1199 TaskSourceKind::Worktree {
1200 id: worktree_2,
1201 directory_in_worktree: PathBuf::from(".zed"),
1202 id_base: "local worktree tasks from directory \".zed\"".into(),
1203 },
1204 common_name.to_string(),
1205 ),
1206 (
1207 TaskSourceKind::Worktree {
1208 id: worktree_2,
1209 directory_in_worktree: PathBuf::from(".zed"),
1210 id_base: "local worktree tasks from directory \".zed\"".into(),
1211 },
1212 "worktree_2".to_string(),
1213 ),
1214 ];
1215
1216 inventory.update(cx, |inventory, _| {
1217 inventory
1218 .update_file_based_tasks(
1219 TaskSettingsLocation::Global(tasks_file()),
1220 Some(&mock_tasks_from_names(
1221 worktree_independent_tasks
1222 .iter()
1223 .map(|(_, name)| name.as_str()),
1224 )),
1225 )
1226 .unwrap();
1227 inventory
1228 .update_file_based_tasks(
1229 TaskSettingsLocation::Worktree(SettingsLocation {
1230 worktree_id: worktree_1,
1231 path: Path::new(".zed"),
1232 }),
1233 Some(&mock_tasks_from_names(
1234 worktree_1_tasks.iter().map(|(_, name)| name.as_str()),
1235 )),
1236 )
1237 .unwrap();
1238 inventory
1239 .update_file_based_tasks(
1240 TaskSettingsLocation::Worktree(SettingsLocation {
1241 worktree_id: worktree_2,
1242 path: Path::new(".zed"),
1243 }),
1244 Some(&mock_tasks_from_names(
1245 worktree_2_tasks.iter().map(|(_, name)| name.as_str()),
1246 )),
1247 )
1248 .unwrap();
1249 });
1250
1251 assert_eq!(
1252 list_tasks_sorted_by_last_used(&inventory, None, cx).await,
1253 worktree_independent_tasks,
1254 "Without a worktree, only worktree-independent tasks should be listed"
1255 );
1256 assert_eq!(
1257 list_tasks_sorted_by_last_used(&inventory, Some(worktree_1), cx).await,
1258 worktree_1_tasks
1259 .iter()
1260 .chain(worktree_independent_tasks.iter())
1261 .cloned()
1262 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
1263 .collect::<Vec<_>>(),
1264 );
1265 assert_eq!(
1266 list_tasks_sorted_by_last_used(&inventory, Some(worktree_2), cx).await,
1267 worktree_2_tasks
1268 .iter()
1269 .chain(worktree_independent_tasks.iter())
1270 .cloned()
1271 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
1272 .collect::<Vec<_>>(),
1273 );
1274
1275 assert_eq!(
1276 list_tasks(&inventory, None, cx).await,
1277 worktree_independent_tasks,
1278 "Without a worktree, only worktree-independent tasks should be listed"
1279 );
1280 assert_eq!(
1281 list_tasks(&inventory, Some(worktree_1), cx).await,
1282 worktree_1_tasks
1283 .iter()
1284 .chain(worktree_independent_tasks.iter())
1285 .cloned()
1286 .collect::<Vec<_>>(),
1287 );
1288 assert_eq!(
1289 list_tasks(&inventory, Some(worktree_2), cx).await,
1290 worktree_2_tasks
1291 .iter()
1292 .chain(worktree_independent_tasks.iter())
1293 .cloned()
1294 .collect::<Vec<_>>(),
1295 );
1296 }
1297
1298 fn init_test(_cx: &mut TestAppContext) {
1299 zlog::init_test();
1300 TaskStore::init(None);
1301 }
1302
1303 fn resolved_task_names(
1304 inventory: &Entity<Inventory>,
1305 worktree: Option<WorktreeId>,
1306 cx: &mut TestAppContext,
1307 ) -> Vec<String> {
1308 inventory.update(cx, |inventory, cx| {
1309 let mut task_contexts = TaskContexts::default();
1310 task_contexts.active_worktree_context =
1311 worktree.map(|worktree| (worktree, TaskContext::default()));
1312 let (used, current) = inventory.used_and_current_resolved_tasks(&task_contexts, cx);
1313 used.into_iter()
1314 .chain(current)
1315 .map(|(_, task)| task.original_task().label.clone())
1316 .collect()
1317 })
1318 }
1319
1320 fn mock_tasks_from_names<'a>(task_names: impl IntoIterator<Item = &'a str> + 'a) -> String {
1321 serde_json::to_string(&serde_json::Value::Array(
1322 task_names
1323 .into_iter()
1324 .map(|task_name| {
1325 json!({
1326 "label": task_name,
1327 "command": "echo",
1328 "args": vec![task_name],
1329 })
1330 })
1331 .collect::<Vec<_>>(),
1332 ))
1333 .unwrap()
1334 }
1335
1336 async fn list_tasks_sorted_by_last_used(
1337 inventory: &Entity<Inventory>,
1338 worktree: Option<WorktreeId>,
1339 cx: &mut TestAppContext,
1340 ) -> Vec<(TaskSourceKind, String)> {
1341 inventory.update(cx, |inventory, cx| {
1342 let mut task_contexts = TaskContexts::default();
1343 task_contexts.active_worktree_context =
1344 worktree.map(|worktree| (worktree, TaskContext::default()));
1345 let (used, current) = inventory.used_and_current_resolved_tasks(&task_contexts, cx);
1346 let mut all = used;
1347 all.extend(current);
1348 all.into_iter()
1349 .map(|(source_kind, task)| (source_kind, task.resolved_label))
1350 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
1351 .collect()
1352 })
1353 }
1354}