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