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