task_inventory.rs

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