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 gpui::{App, AppContext as _, Entity, SharedString, Task};
14use itertools::Itertools;
15use language::{
16 ContextProvider, File, Language, LanguageToolchainStore, Location,
17 language_settings::language_settings,
18};
19use lsp::{LanguageServerId, LanguageServerName};
20use settings::{InvalidSettingsError, TaskKind, parse_json_with_comments};
21use task::{
22 DebugTaskTemplate, ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskTemplates,
23 TaskVariables, VariableName,
24};
25use text::{BufferId, Point, ToPoint};
26use util::{NumericPrefixWithSuffix, ResultExt as _, paths::PathExt as _, post_inc};
27use worktree::WorktreeId;
28
29use crate::{task_store::TaskSettingsLocation, worktree_store::WorktreeStore};
30
31/// Inventory tracks available tasks for a given project.
32#[derive(Debug, Default)]
33pub struct Inventory {
34 last_scheduled_tasks: VecDeque<(TaskSourceKind, ResolvedTask)>,
35 templates_from_settings: ParsedTemplates,
36}
37
38#[derive(Debug, Default)]
39struct ParsedTemplates {
40 global: HashMap<PathBuf, Vec<TaskTemplate>>,
41 worktree: HashMap<WorktreeId, HashMap<(Arc<Path>, TaskKind), Vec<TaskTemplate>>>,
42}
43
44/// Kind of a source the tasks are fetched from, used to display more source information in the UI.
45#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
46pub enum TaskSourceKind {
47 /// bash-like commands spawned by users, not associated with any path
48 UserInput,
49 /// Tasks from the worktree's .zed/task.json
50 Worktree {
51 id: WorktreeId,
52 directory_in_worktree: PathBuf,
53 id_base: Cow<'static, str>,
54 },
55 /// ~/.config/zed/task.json - like global files with task definitions, applicable to any path
56 AbsPath {
57 id_base: Cow<'static, str>,
58 abs_path: PathBuf,
59 },
60 /// Languages-specific tasks coming from extensions.
61 Language { name: SharedString },
62 /// Language-specific tasks coming from LSP servers.
63 Lsp(LanguageServerId),
64}
65
66/// A collection of task contexts, derived from the current state of the workspace.
67/// Only contains worktrees that are visible and with their root being a directory.
68#[derive(Debug, Default)]
69pub struct TaskContexts {
70 /// A context, related to the currently opened item.
71 /// Item can be opened from an invisible worktree, or any other, not necessarily active worktree.
72 pub active_item_context: Option<(Option<WorktreeId>, Option<Location>, TaskContext)>,
73 /// A worktree that corresponds to the active item, or the only worktree in the workspace.
74 pub active_worktree_context: Option<(WorktreeId, TaskContext)>,
75 /// If there are multiple worktrees in the workspace, all non-active ones are included here.
76 pub other_worktree_contexts: Vec<(WorktreeId, TaskContext)>,
77 pub lsp_task_sources: HashMap<LanguageServerName, Vec<BufferId>>,
78 pub latest_selection: Option<text::Anchor>,
79}
80
81impl TaskContexts {
82 pub fn active_context(&self) -> Option<&TaskContext> {
83 self.active_item_context
84 .as_ref()
85 .map(|(_, _, context)| context)
86 .or_else(|| {
87 self.active_worktree_context
88 .as_ref()
89 .map(|(_, context)| context)
90 })
91 }
92
93 pub fn location(&self) -> Option<&Location> {
94 self.active_item_context
95 .as_ref()
96 .and_then(|(_, location, _)| location.as_ref())
97 }
98
99 pub fn worktree(&self) -> Option<WorktreeId> {
100 self.active_item_context
101 .as_ref()
102 .and_then(|(worktree_id, _, _)| worktree_id.as_ref())
103 .or_else(|| {
104 self.active_worktree_context
105 .as_ref()
106 .map(|(worktree_id, _)| worktree_id)
107 })
108 .copied()
109 }
110}
111
112impl TaskSourceKind {
113 pub fn to_id_base(&self) -> String {
114 match self {
115 Self::UserInput => "oneshot".to_string(),
116 Self::AbsPath { id_base, abs_path } => {
117 format!("{id_base}_{}", abs_path.display())
118 }
119 Self::Worktree {
120 id,
121 id_base,
122 directory_in_worktree,
123 } => {
124 format!("{id_base}_{id}_{}", directory_in_worktree.display())
125 }
126 Self::Language { name } => format!("language_{name}"),
127 Self::Lsp(server_id) => format!("lsp_{server_id}"),
128 }
129 }
130}
131
132impl Inventory {
133 pub fn new(cx: &mut App) -> Entity<Self> {
134 cx.new(|_| Self::default())
135 }
136
137 pub fn list_debug_tasks(&self) -> Vec<&TaskTemplate> {
138 self.templates_from_settings
139 .worktree
140 .values()
141 .flat_map(|tasks| {
142 tasks.iter().filter_map(|(kind, tasks)| {
143 if matches!(kind.1, TaskKind::Debug) {
144 Some(tasks)
145 } else {
146 None
147 }
148 })
149 })
150 .flatten()
151 .collect()
152 }
153 /// Pulls its task sources relevant to the worktree and the language given,
154 /// returns all task templates with their source kinds, worktree tasks first, language tasks second
155 /// and global tasks last. No specific order inside source kinds groups.
156 pub fn list_tasks(
157 &self,
158 file: Option<Arc<dyn File>>,
159 language: Option<Arc<Language>>,
160 worktree: Option<WorktreeId>,
161 cx: &App,
162 ) -> Vec<(TaskSourceKind, TaskTemplate)> {
163 let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
164 name: language.name().into(),
165 });
166 let global_tasks = self.global_templates_from_settings();
167 let language_tasks = language
168 .filter(|language| {
169 language_settings(Some(language.name()), file.as_ref(), cx)
170 .tasks
171 .enabled
172 })
173 .and_then(|language| language.context_provider()?.associated_tasks(file, cx))
174 .into_iter()
175 .flat_map(|tasks| tasks.0.into_iter())
176 .flat_map(|task| Some((task_source_kind.clone()?, task)))
177 .chain(global_tasks);
178
179 self.worktree_templates_from_settings(worktree)
180 .chain(language_tasks)
181 .collect()
182 }
183
184 /// Pulls its task sources relevant to the worktree and the language given and resolves them with the [`TaskContexts`] given.
185 /// Joins the new resolutions with the resolved tasks that were used (spawned) before,
186 /// orders them so that the most recently used come first, all equally used ones are ordered so that the most specific tasks come first.
187 /// Deduplicates the tasks by their labels and context and splits the ordered list into two: used tasks and the rest, newly resolved tasks.
188 pub fn used_and_current_resolved_tasks<'a>(
189 &'a self,
190 task_contexts: &'a TaskContexts,
191 cx: &'a App,
192 ) -> (
193 Vec<(TaskSourceKind, ResolvedTask)>,
194 Vec<(TaskSourceKind, ResolvedTask)>,
195 ) {
196 let worktree = task_contexts.worktree();
197 let location = task_contexts.location();
198 let language = location
199 .and_then(|location| location.buffer.read(cx).language_at(location.range.start));
200 let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
201 name: language.name().into(),
202 });
203 let file = location.and_then(|location| location.buffer.read(cx).file().cloned());
204
205 let mut task_labels_to_ids = HashMap::<String, HashSet<TaskId>>::default();
206 let mut lru_score = 0_u32;
207 let previously_spawned_tasks = self
208 .last_scheduled_tasks
209 .iter()
210 .rev()
211 .filter(|(task_kind, _)| {
212 if matches!(task_kind, TaskSourceKind::Language { .. }) {
213 Some(task_kind) == task_source_kind.as_ref()
214 } else {
215 true
216 }
217 })
218 .filter(|(_, resolved_task)| {
219 match task_labels_to_ids.entry(resolved_task.resolved_label.clone()) {
220 hash_map::Entry::Occupied(mut o) => {
221 o.get_mut().insert(resolved_task.id.clone());
222 // Neber allow duplicate reused tasks with the same labels
223 false
224 }
225 hash_map::Entry::Vacant(v) => {
226 v.insert(HashSet::from_iter(Some(resolved_task.id.clone())));
227 true
228 }
229 }
230 })
231 .map(|(task_source_kind, resolved_task)| {
232 (
233 task_source_kind.clone(),
234 resolved_task.clone(),
235 post_inc(&mut lru_score),
236 )
237 })
238 .sorted_unstable_by(task_lru_comparator)
239 .map(|(kind, task, _)| (kind, task))
240 .collect::<Vec<_>>();
241
242 let not_used_score = post_inc(&mut lru_score);
243 let global_tasks = self.global_templates_from_settings();
244
245 let language_tasks = language
246 .filter(|language| {
247 language_settings(Some(language.name()), file.as_ref(), cx)
248 .tasks
249 .enabled
250 })
251 .and_then(|language| language.context_provider()?.associated_tasks(file, cx))
252 .into_iter()
253 .flat_map(|tasks| tasks.0.into_iter())
254 .flat_map(|task| Some((task_source_kind.clone()?, task)));
255 let worktree_tasks = self
256 .worktree_templates_from_settings(worktree)
257 .chain(language_tasks)
258 .chain(global_tasks);
259
260 let new_resolved_tasks = worktree_tasks
261 .flat_map(|(kind, task)| {
262 let id_base = kind.to_id_base();
263 if let TaskSourceKind::Worktree { id, .. } = &kind {
264 None.or_else(|| {
265 let (_, _, item_context) = task_contexts
266 .active_item_context
267 .as_ref()
268 .filter(|(worktree_id, _, _)| Some(id) == worktree_id.as_ref())?;
269 task.resolve_task(&id_base, item_context)
270 })
271 .or_else(|| {
272 let (_, worktree_context) = task_contexts
273 .active_worktree_context
274 .as_ref()
275 .filter(|(worktree_id, _)| id == worktree_id)?;
276 task.resolve_task(&id_base, worktree_context)
277 })
278 .or_else(|| {
279 if let TaskSourceKind::Worktree { id, .. } = &kind {
280 let worktree_context = task_contexts
281 .other_worktree_contexts
282 .iter()
283 .find(|(worktree_id, _)| worktree_id == id)
284 .map(|(_, context)| context)?;
285 task.resolve_task(&id_base, worktree_context)
286 } else {
287 None
288 }
289 })
290 } else {
291 None.or_else(|| {
292 let (_, _, item_context) = task_contexts.active_item_context.as_ref()?;
293 task.resolve_task(&id_base, item_context)
294 })
295 .or_else(|| {
296 let (_, worktree_context) =
297 task_contexts.active_worktree_context.as_ref()?;
298 task.resolve_task(&id_base, worktree_context)
299 })
300 }
301 .or_else(|| task.resolve_task(&id_base, &TaskContext::default()))
302 .map(move |resolved_task| (kind.clone(), resolved_task, not_used_score))
303 })
304 .filter(|(_, resolved_task, _)| {
305 match task_labels_to_ids.entry(resolved_task.resolved_label.clone()) {
306 hash_map::Entry::Occupied(mut o) => {
307 // Allow new tasks with the same label, if their context is different
308 o.get_mut().insert(resolved_task.id.clone())
309 }
310 hash_map::Entry::Vacant(v) => {
311 v.insert(HashSet::from_iter(Some(resolved_task.id.clone())));
312 true
313 }
314 }
315 })
316 .sorted_unstable_by(task_lru_comparator)
317 .map(|(kind, task, _)| (kind, task))
318 .collect::<Vec<_>>();
319
320 (previously_spawned_tasks, new_resolved_tasks)
321 }
322
323 /// Returns the last scheduled task by task_id if provided.
324 /// Otherwise, returns the last scheduled task.
325 pub fn last_scheduled_task(
326 &self,
327 task_id: Option<&TaskId>,
328 ) -> Option<(TaskSourceKind, ResolvedTask)> {
329 if let Some(task_id) = task_id {
330 self.last_scheduled_tasks
331 .iter()
332 .find(|(_, task)| &task.id == task_id)
333 .cloned()
334 } else {
335 self.last_scheduled_tasks.back().cloned()
336 }
337 }
338
339 /// Registers task "usage" as being scheduled – to be used for LRU sorting when listing all tasks.
340 pub fn task_scheduled(
341 &mut self,
342 task_source_kind: TaskSourceKind,
343 resolved_task: ResolvedTask,
344 ) {
345 self.last_scheduled_tasks
346 .push_back((task_source_kind, resolved_task));
347 if self.last_scheduled_tasks.len() > 5_000 {
348 self.last_scheduled_tasks.pop_front();
349 }
350 }
351
352 /// Deletes a resolved task from history, using its id.
353 /// A similar may still resurface in `used_and_current_resolved_tasks` when its [`TaskTemplate`] is resolved again.
354 pub fn delete_previously_used(&mut self, id: &TaskId) {
355 self.last_scheduled_tasks.retain(|(_, task)| &task.id != id);
356 }
357
358 fn global_templates_from_settings(
359 &self,
360 ) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
361 self.templates_from_settings
362 .global
363 .iter()
364 .flat_map(|(file_path, templates)| {
365 templates.into_iter().map(|template| {
366 (
367 TaskSourceKind::AbsPath {
368 id_base: match template.task_type {
369 task::TaskType::Script => Cow::Borrowed("global tasks.json"),
370 task::TaskType::Debug(_) => Cow::Borrowed("global debug.json"),
371 },
372 abs_path: file_path.clone(),
373 },
374 template.clone(),
375 )
376 })
377 })
378 }
379
380 fn worktree_templates_from_settings(
381 &self,
382 worktree: Option<WorktreeId>,
383 ) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
384 worktree.into_iter().flat_map(|worktree| {
385 self.templates_from_settings
386 .worktree
387 .get(&worktree)
388 .into_iter()
389 .flatten()
390 .flat_map(|(directory, templates)| {
391 templates.iter().map(move |template| (directory, template))
392 })
393 .map(move |((directory, _task_kind), template)| {
394 (
395 TaskSourceKind::Worktree {
396 id: worktree,
397 directory_in_worktree: directory.to_path_buf(),
398 id_base: Cow::Owned(format!(
399 "local worktree tasks from directory {directory:?}"
400 )),
401 },
402 template.clone(),
403 )
404 })
405 })
406 }
407
408 /// Updates in-memory task metadata from the JSON string given.
409 /// Will fail if the JSON is not a valid array of objects, but will continue if any object will not parse into a [`TaskTemplate`].
410 ///
411 /// Global tasks are updated for no worktree provided, otherwise the worktree metadata for a given path will be updated.
412 pub(crate) fn update_file_based_tasks(
413 &mut self,
414 location: TaskSettingsLocation<'_>,
415 raw_tasks_json: Option<&str>,
416 task_kind: TaskKind,
417 ) -> Result<(), InvalidSettingsError> {
418 let raw_tasks = match parse_json_with_comments::<Vec<serde_json::Value>>(
419 raw_tasks_json.unwrap_or("[]"),
420 ) {
421 Ok(tasks) => tasks,
422 Err(e) => {
423 return Err(InvalidSettingsError::Tasks {
424 path: match location {
425 TaskSettingsLocation::Global(path) => path.to_owned(),
426 TaskSettingsLocation::Worktree(settings_location) => {
427 task_kind.config_in_dir(settings_location.path)
428 }
429 },
430 message: format!("Failed to parse tasks file content as a JSON array: {e}"),
431 });
432 }
433 };
434 let new_templates = raw_tasks
435 .into_iter()
436 .filter_map(|raw_template| match &task_kind {
437 TaskKind::Script => serde_json::from_value::<TaskTemplate>(raw_template).log_err(),
438 TaskKind::Debug => serde_json::from_value::<DebugTaskTemplate>(raw_template)
439 .log_err()
440 .map(|content| content.to_zed_format()),
441 });
442
443 let parsed_templates = &mut self.templates_from_settings;
444 match location {
445 TaskSettingsLocation::Global(path) => {
446 parsed_templates
447 .global
448 .entry(path.to_owned())
449 .insert_entry(new_templates.collect());
450 }
451 TaskSettingsLocation::Worktree(location) => {
452 let new_templates = new_templates.collect::<Vec<_>>();
453 if new_templates.is_empty() {
454 if let Some(worktree_tasks) =
455 parsed_templates.worktree.get_mut(&location.worktree_id)
456 {
457 worktree_tasks.remove(&(Arc::from(location.path), task_kind));
458 }
459 } else {
460 parsed_templates
461 .worktree
462 .entry(location.worktree_id)
463 .or_default()
464 .insert((Arc::from(location.path), task_kind), new_templates);
465 }
466 }
467 }
468
469 Ok(())
470 }
471}
472
473fn task_lru_comparator(
474 (kind_a, task_a, lru_score_a): &(TaskSourceKind, ResolvedTask, u32),
475 (kind_b, task_b, lru_score_b): &(TaskSourceKind, ResolvedTask, u32),
476) -> cmp::Ordering {
477 lru_score_a
478 // First, display recently used templates above all.
479 .cmp(lru_score_b)
480 // Then, ensure more specific sources are displayed first.
481 .then(task_source_kind_preference(kind_a).cmp(&task_source_kind_preference(kind_b)))
482 // After that, display first more specific tasks, using more template variables.
483 // Bonus points for tasks with symbol variables.
484 .then(task_variables_preference(task_a).cmp(&task_variables_preference(task_b)))
485 // Finally, sort by the resolved label, but a bit more specifically, to avoid mixing letters and digits.
486 .then({
487 NumericPrefixWithSuffix::from_numeric_prefixed_str(&task_a.resolved_label)
488 .cmp(&NumericPrefixWithSuffix::from_numeric_prefixed_str(
489 &task_b.resolved_label,
490 ))
491 .then(task_a.resolved_label.cmp(&task_b.resolved_label))
492 .then(kind_a.cmp(kind_b))
493 })
494}
495
496fn task_source_kind_preference(kind: &TaskSourceKind) -> u32 {
497 match kind {
498 TaskSourceKind::Lsp(..) => 0,
499 TaskSourceKind::Language { .. } => 1,
500 TaskSourceKind::UserInput => 2,
501 TaskSourceKind::Worktree { .. } => 3,
502 TaskSourceKind::AbsPath { .. } => 4,
503 }
504}
505
506fn task_variables_preference(task: &ResolvedTask) -> Reverse<usize> {
507 let task_variables = task.substituted_variables();
508 Reverse(if task_variables.contains(&VariableName::Symbol) {
509 task_variables.len() + 1
510 } else {
511 task_variables.len()
512 })
513}
514
515#[cfg(test)]
516mod test_inventory {
517 use gpui::{Entity, TestAppContext};
518 use itertools::Itertools;
519 use task::TaskContext;
520 use worktree::WorktreeId;
521
522 use crate::Inventory;
523
524 use super::TaskSourceKind;
525
526 pub(super) fn task_template_names(
527 inventory: &Entity<Inventory>,
528 worktree: Option<WorktreeId>,
529 cx: &mut TestAppContext,
530 ) -> Vec<String> {
531 inventory.update(cx, |inventory, cx| {
532 inventory
533 .list_tasks(None, None, worktree, cx)
534 .into_iter()
535 .map(|(_, task)| task.label)
536 .sorted()
537 .collect()
538 })
539 }
540
541 pub(super) fn register_task_used(
542 inventory: &Entity<Inventory>,
543 task_name: &str,
544 cx: &mut TestAppContext,
545 ) {
546 inventory.update(cx, |inventory, cx| {
547 let (task_source_kind, task) = inventory
548 .list_tasks(None, None, None, cx)
549 .into_iter()
550 .find(|(_, task)| task.label == task_name)
551 .unwrap_or_else(|| panic!("Failed to find task with name {task_name}"));
552 let id_base = task_source_kind.to_id_base();
553 inventory.task_scheduled(
554 task_source_kind.clone(),
555 task.resolve_task(&id_base, &TaskContext::default())
556 .unwrap_or_else(|| panic!("Failed to resolve task with name {task_name}")),
557 );
558 });
559 }
560
561 pub(super) async fn list_tasks(
562 inventory: &Entity<Inventory>,
563 worktree: Option<WorktreeId>,
564 cx: &mut TestAppContext,
565 ) -> Vec<(TaskSourceKind, String)> {
566 inventory.update(cx, |inventory, cx| {
567 let task_context = &TaskContext::default();
568 inventory
569 .list_tasks(None, None, worktree, cx)
570 .into_iter()
571 .filter_map(|(source_kind, task)| {
572 let id_base = source_kind.to_id_base();
573 Some((source_kind, task.resolve_task(&id_base, task_context)?))
574 })
575 .map(|(source_kind, resolved_task)| (source_kind, resolved_task.resolved_label))
576 .collect()
577 })
578 }
579}
580
581/// A context provided that tries to provide values for all non-custom [`VariableName`] variants for a currently opened file.
582/// Applied as a base for every custom [`ContextProvider`] unless explicitly oped out.
583pub struct BasicContextProvider {
584 worktree_store: Entity<WorktreeStore>,
585}
586
587impl BasicContextProvider {
588 pub fn new(worktree_store: Entity<WorktreeStore>) -> Self {
589 Self { worktree_store }
590 }
591}
592impl ContextProvider for BasicContextProvider {
593 fn build_context(
594 &self,
595 _: &TaskVariables,
596 location: &Location,
597 _: Option<HashMap<String, String>>,
598 _: Arc<dyn LanguageToolchainStore>,
599 cx: &mut App,
600 ) -> Task<Result<TaskVariables>> {
601 let buffer = location.buffer.read(cx);
602 let buffer_snapshot = buffer.snapshot();
603 let symbols = buffer_snapshot.symbols_containing(location.range.start, None);
604 let symbol = symbols.unwrap_or_default().last().map(|symbol| {
605 let range = symbol
606 .name_ranges
607 .last()
608 .cloned()
609 .unwrap_or(0..symbol.text.len());
610 symbol.text[range].to_string()
611 });
612
613 let current_file = buffer
614 .file()
615 .and_then(|file| file.as_local())
616 .map(|file| file.abs_path(cx).to_sanitized_string());
617 let Point { row, column } = location.range.start.to_point(&buffer_snapshot);
618 let row = row + 1;
619 let column = column + 1;
620 let selected_text = buffer
621 .chars_for_range(location.range.clone())
622 .collect::<String>();
623
624 let mut task_variables = TaskVariables::from_iter([
625 (VariableName::Row, row.to_string()),
626 (VariableName::Column, column.to_string()),
627 ]);
628
629 if let Some(symbol) = symbol {
630 task_variables.insert(VariableName::Symbol, symbol);
631 }
632 if !selected_text.trim().is_empty() {
633 task_variables.insert(VariableName::SelectedText, selected_text);
634 }
635 let worktree_root_dir =
636 buffer
637 .file()
638 .map(|file| file.worktree_id(cx))
639 .and_then(|worktree_id| {
640 self.worktree_store
641 .read(cx)
642 .worktree_for_id(worktree_id, cx)
643 .and_then(|worktree| worktree.read(cx).root_dir())
644 });
645 if let Some(worktree_path) = worktree_root_dir {
646 task_variables.insert(
647 VariableName::WorktreeRoot,
648 worktree_path.to_sanitized_string(),
649 );
650 if let Some(full_path) = current_file.as_ref() {
651 let relative_path = pathdiff::diff_paths(full_path, worktree_path);
652 if let Some(relative_path) = relative_path {
653 task_variables.insert(
654 VariableName::RelativeFile,
655 relative_path.to_sanitized_string(),
656 );
657 }
658 }
659 }
660
661 if let Some(path_as_string) = current_file {
662 let path = Path::new(&path_as_string);
663 if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
664 task_variables.insert(VariableName::Filename, String::from(filename));
665 }
666
667 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
668 task_variables.insert(VariableName::Stem, stem.into());
669 }
670
671 if let Some(dirname) = path.parent().and_then(|s| s.to_str()) {
672 task_variables.insert(VariableName::Dirname, dirname.into());
673 }
674
675 task_variables.insert(VariableName::File, path_as_string);
676 }
677
678 Task::ready(Ok(task_variables))
679 }
680}
681
682/// A ContextProvider that doesn't provide any task variables on it's own, though it has some associated tasks.
683pub struct ContextProviderWithTasks {
684 templates: TaskTemplates,
685}
686
687impl ContextProviderWithTasks {
688 pub fn new(definitions: TaskTemplates) -> Self {
689 Self {
690 templates: definitions,
691 }
692 }
693}
694
695impl ContextProvider for ContextProviderWithTasks {
696 fn associated_tasks(
697 &self,
698 _: Option<Arc<dyn language::File>>,
699 _: &App,
700 ) -> Option<TaskTemplates> {
701 Some(self.templates.clone())
702 }
703}
704
705#[cfg(test)]
706mod tests {
707 use gpui::TestAppContext;
708 use paths::tasks_file;
709 use pretty_assertions::assert_eq;
710 use serde_json::json;
711 use settings::SettingsLocation;
712
713 use crate::task_store::TaskStore;
714
715 use super::test_inventory::*;
716 use super::*;
717
718 #[gpui::test]
719 async fn test_task_list_sorting(cx: &mut TestAppContext) {
720 init_test(cx);
721 let inventory = cx.update(Inventory::new);
722 let initial_tasks = resolved_task_names(&inventory, None, cx);
723 assert!(
724 initial_tasks.is_empty(),
725 "No tasks expected for empty inventory, but got {initial_tasks:?}"
726 );
727 let initial_tasks = task_template_names(&inventory, None, cx);
728 assert!(
729 initial_tasks.is_empty(),
730 "No tasks expected for empty inventory, but got {initial_tasks:?}"
731 );
732 cx.run_until_parked();
733 let expected_initial_state = [
734 "1_a_task".to_string(),
735 "1_task".to_string(),
736 "2_task".to_string(),
737 "3_task".to_string(),
738 ];
739
740 inventory.update(cx, |inventory, _| {
741 inventory
742 .update_file_based_tasks(
743 TaskSettingsLocation::Global(tasks_file()),
744 Some(&mock_tasks_from_names(
745 expected_initial_state.iter().map(|name| name.as_str()),
746 )),
747 settings::TaskKind::Script,
748 )
749 .unwrap();
750 });
751 assert_eq!(
752 task_template_names(&inventory, None, cx),
753 &expected_initial_state,
754 );
755 assert_eq!(
756 resolved_task_names(&inventory, None, cx),
757 &expected_initial_state,
758 "Tasks with equal amount of usages should be sorted alphanumerically"
759 );
760
761 register_task_used(&inventory, "2_task", cx);
762 assert_eq!(
763 task_template_names(&inventory, None, cx),
764 &expected_initial_state,
765 );
766 assert_eq!(
767 resolved_task_names(&inventory, None, cx),
768 vec![
769 "2_task".to_string(),
770 "1_a_task".to_string(),
771 "1_task".to_string(),
772 "3_task".to_string()
773 ],
774 );
775
776 register_task_used(&inventory, "1_task", cx);
777 register_task_used(&inventory, "1_task", cx);
778 register_task_used(&inventory, "1_task", cx);
779 register_task_used(&inventory, "3_task", cx);
780 assert_eq!(
781 task_template_names(&inventory, None, cx),
782 &expected_initial_state,
783 );
784 assert_eq!(
785 resolved_task_names(&inventory, None, cx),
786 vec![
787 "3_task".to_string(),
788 "1_task".to_string(),
789 "2_task".to_string(),
790 "1_a_task".to_string(),
791 ],
792 );
793
794 inventory.update(cx, |inventory, _| {
795 inventory
796 .update_file_based_tasks(
797 TaskSettingsLocation::Global(tasks_file()),
798 Some(&mock_tasks_from_names(
799 ["10_hello", "11_hello"]
800 .into_iter()
801 .chain(expected_initial_state.iter().map(|name| name.as_str())),
802 )),
803 settings::TaskKind::Script,
804 )
805 .unwrap();
806 });
807 cx.run_until_parked();
808 let expected_updated_state = [
809 "10_hello".to_string(),
810 "11_hello".to_string(),
811 "1_a_task".to_string(),
812 "1_task".to_string(),
813 "2_task".to_string(),
814 "3_task".to_string(),
815 ];
816 assert_eq!(
817 task_template_names(&inventory, None, cx),
818 &expected_updated_state,
819 );
820 assert_eq!(
821 resolved_task_names(&inventory, None, cx),
822 vec![
823 "3_task".to_string(),
824 "1_task".to_string(),
825 "2_task".to_string(),
826 "1_a_task".to_string(),
827 "10_hello".to_string(),
828 "11_hello".to_string(),
829 ],
830 );
831
832 register_task_used(&inventory, "11_hello", cx);
833 assert_eq!(
834 task_template_names(&inventory, None, cx),
835 &expected_updated_state,
836 );
837 assert_eq!(
838 resolved_task_names(&inventory, None, cx),
839 vec![
840 "11_hello".to_string(),
841 "3_task".to_string(),
842 "1_task".to_string(),
843 "2_task".to_string(),
844 "1_a_task".to_string(),
845 "10_hello".to_string(),
846 ],
847 );
848 }
849
850 #[gpui::test]
851 async fn test_inventory_static_task_filters(cx: &mut TestAppContext) {
852 init_test(cx);
853 let inventory = cx.update(Inventory::new);
854 let common_name = "common_task_name";
855 let worktree_1 = WorktreeId::from_usize(1);
856 let worktree_2 = WorktreeId::from_usize(2);
857
858 cx.run_until_parked();
859 let worktree_independent_tasks = vec![
860 (
861 TaskSourceKind::AbsPath {
862 id_base: "global tasks.json".into(),
863 abs_path: paths::tasks_file().clone(),
864 },
865 common_name.to_string(),
866 ),
867 (
868 TaskSourceKind::AbsPath {
869 id_base: "global tasks.json".into(),
870 abs_path: paths::tasks_file().clone(),
871 },
872 "static_source_1".to_string(),
873 ),
874 (
875 TaskSourceKind::AbsPath {
876 id_base: "global tasks.json".into(),
877 abs_path: paths::tasks_file().clone(),
878 },
879 "static_source_2".to_string(),
880 ),
881 ];
882 let worktree_1_tasks = [
883 (
884 TaskSourceKind::Worktree {
885 id: worktree_1,
886 directory_in_worktree: PathBuf::from(".zed"),
887 id_base: "local worktree tasks from directory \".zed\"".into(),
888 },
889 common_name.to_string(),
890 ),
891 (
892 TaskSourceKind::Worktree {
893 id: worktree_1,
894 directory_in_worktree: PathBuf::from(".zed"),
895 id_base: "local worktree tasks from directory \".zed\"".into(),
896 },
897 "worktree_1".to_string(),
898 ),
899 ];
900 let worktree_2_tasks = [
901 (
902 TaskSourceKind::Worktree {
903 id: worktree_2,
904 directory_in_worktree: PathBuf::from(".zed"),
905 id_base: "local worktree tasks from directory \".zed\"".into(),
906 },
907 common_name.to_string(),
908 ),
909 (
910 TaskSourceKind::Worktree {
911 id: worktree_2,
912 directory_in_worktree: PathBuf::from(".zed"),
913 id_base: "local worktree tasks from directory \".zed\"".into(),
914 },
915 "worktree_2".to_string(),
916 ),
917 ];
918
919 inventory.update(cx, |inventory, _| {
920 inventory
921 .update_file_based_tasks(
922 TaskSettingsLocation::Global(tasks_file()),
923 Some(&mock_tasks_from_names(
924 worktree_independent_tasks
925 .iter()
926 .map(|(_, name)| name.as_str()),
927 )),
928 settings::TaskKind::Script,
929 )
930 .unwrap();
931 inventory
932 .update_file_based_tasks(
933 TaskSettingsLocation::Worktree(SettingsLocation {
934 worktree_id: worktree_1,
935 path: Path::new(".zed"),
936 }),
937 Some(&mock_tasks_from_names(
938 worktree_1_tasks.iter().map(|(_, name)| name.as_str()),
939 )),
940 settings::TaskKind::Script,
941 )
942 .unwrap();
943 inventory
944 .update_file_based_tasks(
945 TaskSettingsLocation::Worktree(SettingsLocation {
946 worktree_id: worktree_2,
947 path: Path::new(".zed"),
948 }),
949 Some(&mock_tasks_from_names(
950 worktree_2_tasks.iter().map(|(_, name)| name.as_str()),
951 )),
952 settings::TaskKind::Script,
953 )
954 .unwrap();
955 });
956
957 assert_eq!(
958 list_tasks_sorted_by_last_used(&inventory, None, cx).await,
959 worktree_independent_tasks,
960 "Without a worktree, only worktree-independent tasks should be listed"
961 );
962 assert_eq!(
963 list_tasks_sorted_by_last_used(&inventory, Some(worktree_1), cx).await,
964 worktree_1_tasks
965 .iter()
966 .chain(worktree_independent_tasks.iter())
967 .cloned()
968 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
969 .collect::<Vec<_>>(),
970 );
971 assert_eq!(
972 list_tasks_sorted_by_last_used(&inventory, Some(worktree_2), cx).await,
973 worktree_2_tasks
974 .iter()
975 .chain(worktree_independent_tasks.iter())
976 .cloned()
977 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
978 .collect::<Vec<_>>(),
979 );
980
981 assert_eq!(
982 list_tasks(&inventory, None, cx).await,
983 worktree_independent_tasks,
984 "Without a worktree, only worktree-independent tasks should be listed"
985 );
986 assert_eq!(
987 list_tasks(&inventory, Some(worktree_1), cx).await,
988 worktree_1_tasks
989 .iter()
990 .chain(worktree_independent_tasks.iter())
991 .cloned()
992 .collect::<Vec<_>>(),
993 );
994 assert_eq!(
995 list_tasks(&inventory, Some(worktree_2), cx).await,
996 worktree_2_tasks
997 .iter()
998 .chain(worktree_independent_tasks.iter())
999 .cloned()
1000 .collect::<Vec<_>>(),
1001 );
1002 }
1003
1004 fn init_test(_cx: &mut TestAppContext) {
1005 if std::env::var("RUST_LOG").is_ok() {
1006 env_logger::try_init().ok();
1007 }
1008 TaskStore::init(None);
1009 }
1010
1011 fn resolved_task_names(
1012 inventory: &Entity<Inventory>,
1013 worktree: Option<WorktreeId>,
1014 cx: &mut TestAppContext,
1015 ) -> Vec<String> {
1016 inventory.update(cx, |inventory, cx| {
1017 let mut task_contexts = TaskContexts::default();
1018 task_contexts.active_worktree_context =
1019 worktree.map(|worktree| (worktree, TaskContext::default()));
1020 let (used, current) = inventory.used_and_current_resolved_tasks(&task_contexts, cx);
1021 used.into_iter()
1022 .chain(current)
1023 .map(|(_, task)| task.original_task().label.clone())
1024 .collect()
1025 })
1026 }
1027
1028 fn mock_tasks_from_names<'a>(task_names: impl Iterator<Item = &'a str> + 'a) -> String {
1029 serde_json::to_string(&serde_json::Value::Array(
1030 task_names
1031 .map(|task_name| {
1032 json!({
1033 "label": task_name,
1034 "command": "echo",
1035 "args": vec![task_name],
1036 })
1037 })
1038 .collect::<Vec<_>>(),
1039 ))
1040 .unwrap()
1041 }
1042
1043 async fn list_tasks_sorted_by_last_used(
1044 inventory: &Entity<Inventory>,
1045 worktree: Option<WorktreeId>,
1046 cx: &mut TestAppContext,
1047 ) -> Vec<(TaskSourceKind, String)> {
1048 inventory.update(cx, |inventory, cx| {
1049 let mut task_contexts = TaskContexts::default();
1050 task_contexts.active_worktree_context =
1051 worktree.map(|worktree| (worktree, TaskContext::default()));
1052 let (used, current) = inventory.used_and_current_resolved_tasks(&task_contexts, cx);
1053 let mut all = used;
1054 all.extend(current);
1055 all.into_iter()
1056 .map(|(source_kind, task)| (source_kind, task.resolved_label))
1057 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
1058 .collect()
1059 })
1060 }
1061}