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