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, 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 (file, language) = {
316 let buffer = location.buffer.read(cx);
317 (buffer.file(), buffer.language())
318 };
319 let language_name = language.as_ref().map(|l| l.name());
320 let adapter = language_settings(language_name, file, cx)
321 .debuggers
322 .first()
323 .map(SharedString::from)
324 .or_else(|| {
325 language.and_then(|l| l.config().debuggers.first().map(SharedString::from))
326 });
327 adapter.map(|adapter| (adapter, DapRegistry::global(cx).locators()))
328 });
329 cx.background_spawn(async move {
330 if let Some((adapter, locators)) = adapter {
331 for (kind, task) in
332 lsp_tasks
333 .into_iter()
334 .chain(current_resolved_tasks.into_iter().filter(|(kind, _)| {
335 add_current_language_tasks
336 || !matches!(kind, TaskSourceKind::Language { .. })
337 }))
338 {
339 let adapter = adapter.clone().into();
340
341 for locator in locators.values() {
342 if let Some(scenario) = locator
343 .create_scenario(task.original_task(), task.display_label(), &adapter)
344 .await
345 {
346 scenarios.push((kind, scenario));
347 break;
348 }
349 }
350 }
351 }
352 (last_scheduled_scenarios, scenarios)
353 })
354 }
355
356 pub fn task_template_by_label(
357 &self,
358 buffer: Option<Entity<Buffer>>,
359 worktree_id: Option<WorktreeId>,
360 label: &str,
361 cx: &App,
362 ) -> Task<Option<TaskTemplate>> {
363 let (buffer_worktree_id, file, language) = buffer
364 .map(|buffer| {
365 let buffer = buffer.read(cx);
366 let file = buffer.file().cloned();
367 (
368 file.as_ref().map(|file| file.worktree_id(cx)),
369 file,
370 buffer.language().cloned(),
371 )
372 })
373 .unwrap_or((None, None, None));
374
375 let tasks = self.list_tasks(file, language, worktree_id.or(buffer_worktree_id), cx);
376 let label = label.to_owned();
377 cx.background_spawn(async move {
378 tasks
379 .await
380 .into_iter()
381 .find(|(_, template)| template.label == label)
382 .map(|val| val.1)
383 })
384 }
385
386 /// Pulls its task sources relevant to the worktree and the language given,
387 /// returns all task templates with their source kinds, worktree tasks first, language tasks second
388 /// and global tasks last. No specific order inside source kinds groups.
389 pub fn list_tasks(
390 &self,
391 file: Option<Arc<dyn File>>,
392 language: Option<Arc<Language>>,
393 worktree: Option<WorktreeId>,
394 cx: &App,
395 ) -> Task<Vec<(TaskSourceKind, TaskTemplate)>> {
396 let global_tasks = self.global_templates_from_settings().collect::<Vec<_>>();
397 let mut worktree_tasks = worktree
398 .into_iter()
399 .flat_map(|worktree| self.worktree_templates_from_settings(worktree))
400 .collect::<Vec<_>>();
401
402 let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
403 name: language.name().into(),
404 });
405 let language_tasks = language
406 .filter(|language| {
407 language_settings(Some(language.name()), file.as_ref(), cx)
408 .tasks
409 .enabled
410 })
411 .and_then(|language| {
412 language
413 .context_provider()
414 .map(|provider| provider.associated_tasks(file, cx))
415 });
416 cx.background_spawn(async move {
417 if let Some(t) = language_tasks {
418 worktree_tasks.extend(t.await.into_iter().flat_map(|tasks| {
419 tasks
420 .0
421 .into_iter()
422 .filter_map(|task| Some((task_source_kind.clone()?, task)))
423 }));
424 }
425 worktree_tasks.extend(global_tasks);
426 worktree_tasks
427 })
428 }
429
430 /// Pulls its task sources relevant to the worktree and the language given and resolves them with the [`TaskContexts`] given.
431 /// Joins the new resolutions with the resolved tasks that were used (spawned) before,
432 /// orders them so that the most recently used come first, all equally used ones are ordered so that the most specific tasks come first.
433 /// Deduplicates the tasks by their labels and context and splits the ordered list into two: used tasks and the rest, newly resolved tasks.
434 pub fn used_and_current_resolved_tasks(
435 &self,
436 task_contexts: Arc<TaskContexts>,
437 cx: &mut Context<Self>,
438 ) -> Task<(
439 Vec<(TaskSourceKind, ResolvedTask)>,
440 Vec<(TaskSourceKind, ResolvedTask)>,
441 )> {
442 let worktree = task_contexts.worktree();
443 let location = task_contexts.location();
444 let language = location.and_then(|location| location.buffer.read(cx).language());
445 let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
446 name: language.name().into(),
447 });
448 let file = location.and_then(|location| location.buffer.read(cx).file().cloned());
449
450 let worktrees_with_zed_tasks: HashSet<WorktreeId> = self
451 .templates_from_settings
452 .worktree
453 .iter()
454 .filter(|(_, dirs)| {
455 dirs.keys()
456 .any(|dir| dir.file_name().is_some_and(|name| name == ".zed"))
457 })
458 .map(|(id, _)| *id)
459 .collect();
460
461 let mut task_labels_to_ids = HashMap::<String, HashSet<TaskId>>::default();
462 let mut lru_score = 0_u32;
463 let previously_spawned_tasks = self
464 .last_scheduled_tasks
465 .iter()
466 .rev()
467 .filter(|(task_kind, _)| {
468 if matches!(task_kind, TaskSourceKind::Language { .. }) {
469 Some(task_kind) == task_source_kind.as_ref()
470 } else if let TaskSourceKind::Worktree {
471 id,
472 directory_in_worktree: dir,
473 ..
474 } = task_kind
475 {
476 !(worktrees_with_zed_tasks.contains(id)
477 && dir.file_name().is_some_and(|name| name == ".vscode"))
478 } else {
479 true
480 }
481 })
482 .filter(|(_, resolved_task)| {
483 match task_labels_to_ids.entry(resolved_task.resolved_label.clone()) {
484 hash_map::Entry::Occupied(mut o) => {
485 o.get_mut().insert(resolved_task.id.clone());
486 // Neber allow duplicate reused tasks with the same labels
487 false
488 }
489 hash_map::Entry::Vacant(v) => {
490 v.insert(HashSet::from_iter(Some(resolved_task.id.clone())));
491 true
492 }
493 }
494 })
495 .map(|(task_source_kind, resolved_task)| {
496 (
497 task_source_kind.clone(),
498 resolved_task.clone(),
499 post_inc(&mut lru_score),
500 )
501 })
502 .sorted_unstable_by(task_lru_comparator)
503 .map(|(kind, task, _)| (kind, task))
504 .collect::<Vec<_>>();
505
506 let not_used_score = post_inc(&mut lru_score);
507 let global_tasks = self.global_templates_from_settings().collect::<Vec<_>>();
508 let associated_tasks = language
509 .filter(|language| {
510 language_settings(Some(language.name()), file.as_ref(), cx)
511 .tasks
512 .enabled
513 })
514 .and_then(|language| {
515 language
516 .context_provider()
517 .map(|provider| provider.associated_tasks(file, cx))
518 });
519 let worktree_tasks = worktree
520 .into_iter()
521 .flat_map(|worktree| self.worktree_templates_from_settings(worktree))
522 .collect::<Vec<_>>();
523 let task_contexts = task_contexts.clone();
524 cx.background_spawn(async move {
525 let language_tasks = if let Some(task) = associated_tasks {
526 task.await.map(|templates| {
527 templates
528 .0
529 .into_iter()
530 .flat_map(|task| Some((task_source_kind.clone()?, task)))
531 })
532 } else {
533 None
534 };
535
536 let worktree_tasks = worktree_tasks
537 .into_iter()
538 .chain(language_tasks.into_iter().flatten())
539 .chain(global_tasks);
540
541 let new_resolved_tasks = worktree_tasks
542 .flat_map(|(kind, task)| {
543 let id_base = kind.to_id_base();
544
545 if let TaskSourceKind::Worktree { id, .. } = &kind {
546 None.or_else(|| {
547 let (_, _, item_context) =
548 task_contexts.active_item_context.as_ref().filter(
549 |(worktree_id, _, _)| Some(id) == worktree_id.as_ref(),
550 )?;
551 task.resolve_task(&id_base, item_context)
552 })
553 .or_else(|| {
554 let (_, worktree_context) = task_contexts
555 .active_worktree_context
556 .as_ref()
557 .filter(|(worktree_id, _)| id == worktree_id)?;
558 task.resolve_task(&id_base, worktree_context)
559 })
560 .or_else(|| {
561 if let TaskSourceKind::Worktree { id, .. } = &kind {
562 let worktree_context = task_contexts
563 .other_worktree_contexts
564 .iter()
565 .find(|(worktree_id, _)| worktree_id == id)
566 .map(|(_, context)| context)?;
567 task.resolve_task(&id_base, worktree_context)
568 } else {
569 None
570 }
571 })
572 } else {
573 None.or_else(|| {
574 let (_, _, item_context) =
575 task_contexts.active_item_context.as_ref()?;
576 task.resolve_task(&id_base, item_context)
577 })
578 .or_else(|| {
579 let (_, worktree_context) =
580 task_contexts.active_worktree_context.as_ref()?;
581 task.resolve_task(&id_base, worktree_context)
582 })
583 }
584 .or_else(|| task.resolve_task(&id_base, &TaskContext::default()))
585 .map(move |resolved_task| (kind.clone(), resolved_task, not_used_score))
586 })
587 .filter(|(_, resolved_task, _)| {
588 match task_labels_to_ids.entry(resolved_task.resolved_label.clone()) {
589 hash_map::Entry::Occupied(mut o) => {
590 // Allow new tasks with the same label, if their context is different
591 o.get_mut().insert(resolved_task.id.clone())
592 }
593 hash_map::Entry::Vacant(v) => {
594 v.insert(HashSet::from_iter(Some(resolved_task.id.clone())));
595 true
596 }
597 }
598 })
599 .sorted_unstable_by(task_lru_comparator)
600 .map(|(kind, task, _)| (kind, task))
601 .collect::<Vec<_>>();
602
603 (previously_spawned_tasks, new_resolved_tasks)
604 })
605 }
606
607 /// Returns the last scheduled task by task_id if provided.
608 /// Otherwise, returns the last scheduled task.
609 pub fn last_scheduled_task(
610 &self,
611 task_id: Option<&TaskId>,
612 ) -> Option<(TaskSourceKind, ResolvedTask)> {
613 if let Some(task_id) = task_id {
614 self.last_scheduled_tasks
615 .iter()
616 .find(|(_, task)| &task.id == task_id)
617 .cloned()
618 } else {
619 self.last_scheduled_tasks.back().cloned()
620 }
621 }
622
623 /// Registers task "usage" as being scheduled – to be used for LRU sorting when listing all tasks.
624 pub fn task_scheduled(
625 &mut self,
626 task_source_kind: TaskSourceKind,
627 resolved_task: ResolvedTask,
628 ) {
629 self.last_scheduled_tasks
630 .push_back((task_source_kind, resolved_task));
631 if self.last_scheduled_tasks.len() > 5_000 {
632 self.last_scheduled_tasks.pop_front();
633 }
634 }
635
636 /// Deletes a resolved task from history, using its id.
637 /// A similar may still resurface in `used_and_current_resolved_tasks` when its [`TaskTemplate`] is resolved again.
638 pub fn delete_previously_used(&mut self, id: &TaskId) {
639 self.last_scheduled_tasks.retain(|(_, task)| &task.id != id);
640 }
641
642 fn global_templates_from_settings(
643 &self,
644 ) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
645 self.templates_from_settings.global_scenarios()
646 }
647
648 fn global_debug_scenarios_from_settings(
649 &self,
650 ) -> impl '_ + Iterator<Item = (TaskSourceKind, DebugScenario)> {
651 self.scenarios_from_settings.global_scenarios()
652 }
653
654 fn worktree_scenarios_from_settings(
655 &self,
656 worktree: WorktreeId,
657 ) -> impl '_ + Iterator<Item = (TaskSourceKind, DebugScenario)> {
658 self.scenarios_from_settings.worktree_scenarios(worktree)
659 }
660
661 fn worktree_templates_from_settings(
662 &self,
663 worktree: WorktreeId,
664 ) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
665 self.templates_from_settings.worktree_scenarios(worktree)
666 }
667
668 /// Updates in-memory task metadata from the JSON string given.
669 /// Will fail if the JSON is not a valid array of objects, but will continue if any object will not parse into a [`TaskTemplate`].
670 ///
671 /// Global tasks are updated for no worktree provided, otherwise the worktree metadata for a given path will be updated.
672 pub fn update_file_based_tasks(
673 &mut self,
674 location: TaskSettingsLocation<'_>,
675 raw_tasks_json: Option<&str>,
676 ) -> Result<(), InvalidSettingsError> {
677 let raw_tasks = match parse_json_with_comments::<Vec<serde_json::Value>>(
678 raw_tasks_json.unwrap_or("[]"),
679 ) {
680 Ok(tasks) => tasks,
681 Err(e) => {
682 return Err(InvalidSettingsError::Tasks {
683 path: match location {
684 TaskSettingsLocation::Global(path) => path.to_owned(),
685 TaskSettingsLocation::Worktree(settings_location) => {
686 settings_location.path.as_std_path().join(task_file_name())
687 }
688 },
689 message: format!("Failed to parse tasks file content as a JSON array: {e}"),
690 });
691 }
692 };
693
694 let mut validation_errors = Vec::new();
695 let new_templates = raw_tasks.into_iter().filter_map(|raw_template| {
696 let template = serde_json::from_value::<TaskTemplate>(raw_template).log_err()?;
697
698 // Validate the variable names used in the `TaskTemplate`.
699 let unknown_variables = template.unknown_variables();
700 if !unknown_variables.is_empty() {
701 let variables_list = unknown_variables
702 .iter()
703 .map(|variable| format!("${variable}"))
704 .collect::<Vec<_>>()
705 .join(", ");
706
707 validation_errors.push(format!(
708 "Task '{}' uses unknown variables: {}",
709 template.label, variables_list
710 ));
711
712 // Skip this template, since it uses unknown variable names, but
713 // continue processing others.
714 return None;
715 }
716
717 Some(template)
718 });
719
720 let parsed_templates = &mut self.templates_from_settings;
721 match location {
722 TaskSettingsLocation::Global(path) => {
723 parsed_templates
724 .global
725 .entry(path.to_owned())
726 .insert_entry(new_templates.collect());
727 self.last_scheduled_tasks.retain(|(kind, _)| {
728 if let TaskSourceKind::AbsPath { abs_path, .. } = kind {
729 abs_path != path
730 } else {
731 true
732 }
733 });
734 }
735 TaskSettingsLocation::Worktree(location) => {
736 let new_templates = new_templates.collect::<Vec<_>>();
737 if new_templates.is_empty() {
738 if let Some(worktree_tasks) =
739 parsed_templates.worktree.get_mut(&location.worktree_id)
740 {
741 worktree_tasks.remove(location.path);
742 }
743 } else {
744 parsed_templates
745 .worktree
746 .entry(location.worktree_id)
747 .or_default()
748 .insert(Arc::from(location.path), new_templates);
749 }
750 self.last_scheduled_tasks.retain(|(kind, _)| {
751 if let TaskSourceKind::Worktree {
752 directory_in_worktree,
753 id,
754 ..
755 } = kind
756 {
757 *id != location.worktree_id
758 || directory_in_worktree.as_ref() != location.path
759 } else {
760 true
761 }
762 });
763 }
764 }
765
766 if !validation_errors.is_empty() {
767 return Err(InvalidSettingsError::Tasks {
768 path: match &location {
769 TaskSettingsLocation::Global(path) => path.to_path_buf(),
770 TaskSettingsLocation::Worktree(location) => {
771 location.path.as_std_path().join(task_file_name())
772 }
773 },
774 message: validation_errors.join("\n"),
775 });
776 }
777
778 Ok(())
779 }
780
781 /// Updates in-memory task metadata from the JSON string given.
782 /// Will fail if the JSON is not a valid array of objects, but will continue if any object will not parse into a [`TaskTemplate`].
783 ///
784 /// Global tasks are updated for no worktree provided, otherwise the worktree metadata for a given path will be updated.
785 pub fn update_file_based_scenarios(
786 &mut self,
787 location: TaskSettingsLocation<'_>,
788 raw_tasks_json: Option<&str>,
789 ) -> Result<(), InvalidSettingsError> {
790 let raw_tasks = match parse_json_with_comments::<Vec<serde_json::Value>>(
791 raw_tasks_json.unwrap_or("[]"),
792 ) {
793 Ok(tasks) => tasks,
794 Err(e) => {
795 return Err(InvalidSettingsError::Debug {
796 path: match location {
797 TaskSettingsLocation::Global(path) => path.to_owned(),
798 TaskSettingsLocation::Worktree(settings_location) => settings_location
799 .path
800 .as_std_path()
801 .join(debug_task_file_name()),
802 },
803 message: format!("Failed to parse tasks file content as a JSON array: {e}"),
804 });
805 }
806 };
807
808 let new_templates = raw_tasks
809 .into_iter()
810 .filter_map(|raw_template| {
811 serde_json::from_value::<DebugScenario>(raw_template).log_err()
812 })
813 .collect::<Vec<_>>();
814
815 let parsed_scenarios = &mut self.scenarios_from_settings;
816 let mut new_definitions: HashMap<_, _> = new_templates
817 .iter()
818 .map(|template| (template.label.clone(), template.clone()))
819 .collect();
820 let previously_existing_scenarios;
821
822 match location {
823 TaskSettingsLocation::Global(path) => {
824 previously_existing_scenarios = parsed_scenarios
825 .global_scenarios()
826 .map(|(_, scenario)| scenario.label)
827 .collect::<HashSet<_>>();
828 parsed_scenarios
829 .global
830 .entry(path.to_owned())
831 .insert_entry(new_templates);
832 }
833 TaskSettingsLocation::Worktree(location) => {
834 previously_existing_scenarios = parsed_scenarios
835 .worktree_scenarios(location.worktree_id)
836 .map(|(_, scenario)| scenario.label)
837 .collect::<HashSet<_>>();
838
839 if new_templates.is_empty() {
840 if let Some(worktree_tasks) =
841 parsed_scenarios.worktree.get_mut(&location.worktree_id)
842 {
843 worktree_tasks.remove(location.path);
844 }
845 } else {
846 parsed_scenarios
847 .worktree
848 .entry(location.worktree_id)
849 .or_default()
850 .insert(Arc::from(location.path), new_templates);
851 }
852 }
853 }
854 self.last_scheduled_scenarios.retain_mut(|(scenario, _)| {
855 if !previously_existing_scenarios.contains(&scenario.label) {
856 return true;
857 }
858 if let Some(new_definition) = new_definitions.remove(&scenario.label) {
859 *scenario = new_definition;
860 true
861 } else {
862 false
863 }
864 });
865
866 Ok(())
867 }
868}
869
870fn task_lru_comparator(
871 (kind_a, task_a, lru_score_a): &(TaskSourceKind, ResolvedTask, u32),
872 (kind_b, task_b, lru_score_b): &(TaskSourceKind, ResolvedTask, u32),
873) -> cmp::Ordering {
874 lru_score_a
875 // First, display recently used templates above all.
876 .cmp(lru_score_b)
877 // Then, ensure more specific sources are displayed first.
878 .then(task_source_kind_preference(kind_a).cmp(&task_source_kind_preference(kind_b)))
879 // After that, display first more specific tasks, using more template variables.
880 // Bonus points for tasks with symbol variables.
881 .then(task_variables_preference(task_a).cmp(&task_variables_preference(task_b)))
882 // Finally, sort by the resolved label, but a bit more specifically, to avoid mixing letters and digits.
883 .then({
884 NumericPrefixWithSuffix::from_numeric_prefixed_str(&task_a.resolved_label)
885 .cmp(&NumericPrefixWithSuffix::from_numeric_prefixed_str(
886 &task_b.resolved_label,
887 ))
888 .then(task_a.resolved_label.cmp(&task_b.resolved_label))
889 .then(kind_a.cmp(kind_b))
890 })
891}
892
893pub fn task_source_kind_preference(kind: &TaskSourceKind) -> u32 {
894 match kind {
895 TaskSourceKind::Lsp { .. } => 0,
896 TaskSourceKind::Language { .. } => 1,
897 TaskSourceKind::UserInput => 2,
898 TaskSourceKind::Worktree { .. } => 3,
899 TaskSourceKind::AbsPath { .. } => 4,
900 }
901}
902
903fn task_variables_preference(task: &ResolvedTask) -> Reverse<usize> {
904 let task_variables = task.substituted_variables();
905 Reverse(if task_variables.contains(&VariableName::Symbol) {
906 task_variables.len() + 1
907 } else {
908 task_variables.len()
909 })
910}
911
912/// A context provided that tries to provide values for all non-custom [`VariableName`] variants for a currently opened file.
913/// Applied as a base for every custom [`ContextProvider`] unless explicitly oped out.
914pub struct BasicContextProvider {
915 worktree_store: Entity<WorktreeStore>,
916}
917
918impl BasicContextProvider {
919 pub fn new(worktree_store: Entity<WorktreeStore>) -> Self {
920 Self { worktree_store }
921 }
922}
923
924impl ContextProvider for BasicContextProvider {
925 fn build_context(
926 &self,
927 _: &TaskVariables,
928 location: ContextLocation<'_>,
929 _: Option<HashMap<String, String>>,
930 _: Arc<dyn LanguageToolchainStore>,
931 cx: &mut App,
932 ) -> Task<Result<TaskVariables>> {
933 let location = location.file_location;
934 let buffer = location.buffer.read(cx);
935 let buffer_snapshot = buffer.snapshot();
936 let symbols = buffer_snapshot.symbols_containing(location.range.start, None);
937 let symbol = symbols.last().map(|symbol| {
938 let range = symbol
939 .name_ranges
940 .last()
941 .cloned()
942 .unwrap_or(0..symbol.text.len());
943 symbol.text[range].to_string()
944 });
945
946 let current_file = buffer.file().and_then(|file| file.as_local());
947 let Point { row, column } = location.range.start.to_point(&buffer_snapshot);
948 let row = row + 1;
949 let column = column + 1;
950 let selected_text = buffer
951 .chars_for_range(location.range.clone())
952 .collect::<String>();
953
954 let mut task_variables = TaskVariables::from_iter([
955 (VariableName::Row, row.to_string()),
956 (VariableName::Column, column.to_string()),
957 ]);
958
959 if let Some(symbol) = symbol {
960 task_variables.insert(VariableName::Symbol, symbol);
961 }
962 if !selected_text.trim().is_empty() {
963 task_variables.insert(VariableName::SelectedText, selected_text);
964 }
965 let worktree = buffer
966 .file()
967 .map(|file| file.worktree_id(cx))
968 .and_then(|worktree_id| {
969 self.worktree_store
970 .read(cx)
971 .worktree_for_id(worktree_id, cx)
972 });
973
974 if let Some(worktree) = worktree {
975 let worktree = worktree.read(cx);
976 let path_style = worktree.path_style();
977 task_variables.insert(
978 VariableName::WorktreeRoot,
979 worktree.abs_path().to_string_lossy().into_owned(),
980 );
981 if let Some(current_file) = current_file.as_ref() {
982 let relative_path = current_file.path();
983 task_variables.insert(
984 VariableName::RelativeFile,
985 relative_path.display(path_style).to_string(),
986 );
987 if let Some(relative_dir) = relative_path.parent() {
988 task_variables.insert(
989 VariableName::RelativeDir,
990 if relative_dir.is_empty() {
991 String::from(".")
992 } else {
993 relative_dir.display(path_style).to_string()
994 },
995 );
996 }
997 }
998 }
999
1000 if let Some(current_file) = current_file {
1001 let path = current_file.abs_path(cx);
1002 if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
1003 task_variables.insert(VariableName::Filename, String::from(filename));
1004 }
1005
1006 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
1007 task_variables.insert(VariableName::Stem, stem.into());
1008 }
1009
1010 if let Some(dirname) = path.parent().and_then(|s| s.to_str()) {
1011 task_variables.insert(VariableName::Dirname, dirname.into());
1012 }
1013
1014 task_variables.insert(VariableName::File, path.to_string_lossy().into_owned());
1015 }
1016
1017 if let Some(language) = buffer.language() {
1018 task_variables.insert(VariableName::Language, language.name().to_string());
1019 }
1020
1021 Task::ready(Ok(task_variables))
1022 }
1023}
1024
1025/// A ContextProvider that doesn't provide any task variables on it's own, though it has some associated tasks.
1026pub struct ContextProviderWithTasks {
1027 templates: TaskTemplates,
1028}
1029
1030impl ContextProviderWithTasks {
1031 pub fn new(definitions: TaskTemplates) -> Self {
1032 Self {
1033 templates: definitions,
1034 }
1035 }
1036}
1037
1038impl ContextProvider for ContextProviderWithTasks {
1039 fn associated_tasks(&self, _: Option<Arc<dyn File>>, _: &App) -> Task<Option<TaskTemplates>> {
1040 Task::ready(Some(self.templates.clone()))
1041 }
1042}