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, TaskHook, 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::{git_store::GitStore, 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 /// Returns all task templates (worktree and global) that have at least one
648 /// hook in the provided set.
649 pub fn templates_with_hooks(
650 &self,
651 hooks: &HashSet<TaskHook>,
652 worktree: WorktreeId,
653 ) -> Vec<(TaskSourceKind, TaskTemplate)> {
654 self.worktree_templates_from_settings(worktree)
655 .chain(self.global_templates_from_settings())
656 .filter(|(_, template)| !template.hooks.is_disjoint(hooks))
657 .collect()
658 }
659
660 fn global_templates_from_settings(
661 &self,
662 ) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
663 self.templates_from_settings.global_scenarios()
664 }
665
666 fn global_debug_scenarios_from_settings(
667 &self,
668 ) -> impl '_ + Iterator<Item = (TaskSourceKind, DebugScenario)> {
669 self.scenarios_from_settings.global_scenarios()
670 }
671
672 fn worktree_scenarios_from_settings(
673 &self,
674 worktree: WorktreeId,
675 ) -> impl '_ + Iterator<Item = (TaskSourceKind, DebugScenario)> {
676 self.scenarios_from_settings.worktree_scenarios(worktree)
677 }
678
679 fn worktree_templates_from_settings(
680 &self,
681 worktree: WorktreeId,
682 ) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
683 self.templates_from_settings.worktree_scenarios(worktree)
684 }
685
686 /// Updates in-memory task metadata from the JSON string given.
687 /// Will fail if the JSON is not a valid array of objects, but will continue if any object will not parse into a [`TaskTemplate`].
688 ///
689 /// Global tasks are updated for no worktree provided, otherwise the worktree metadata for a given path will be updated.
690 pub fn update_file_based_tasks(
691 &mut self,
692 location: TaskSettingsLocation<'_>,
693 raw_tasks_json: Option<&str>,
694 ) -> Result<(), InvalidSettingsError> {
695 let raw_tasks = match parse_json_with_comments::<Vec<serde_json::Value>>(
696 raw_tasks_json.unwrap_or("[]"),
697 ) {
698 Ok(tasks) => tasks,
699 Err(e) => {
700 return Err(InvalidSettingsError::Tasks {
701 path: match location {
702 TaskSettingsLocation::Global(path) => path.to_owned(),
703 TaskSettingsLocation::Worktree(settings_location) => {
704 settings_location.path.as_std_path().join(task_file_name())
705 }
706 },
707 message: format!("Failed to parse tasks file content as a JSON array: {e}"),
708 });
709 }
710 };
711
712 let mut validation_errors = Vec::new();
713 let new_templates = raw_tasks.into_iter().filter_map(|raw_template| {
714 let template = serde_json::from_value::<TaskTemplate>(raw_template).log_err()?;
715
716 // Validate the variable names used in the `TaskTemplate`.
717 let unknown_variables = template.unknown_variables();
718 if !unknown_variables.is_empty() {
719 let variables_list = unknown_variables
720 .iter()
721 .map(|variable| format!("${variable}"))
722 .collect::<Vec<_>>()
723 .join(", ");
724
725 validation_errors.push(format!(
726 "Task '{}' uses unknown variables: {}",
727 template.label, variables_list
728 ));
729
730 // Skip this template, since it uses unknown variable names, but
731 // continue processing others.
732 return None;
733 }
734
735 Some(template)
736 });
737
738 let parsed_templates = &mut self.templates_from_settings;
739 match location {
740 TaskSettingsLocation::Global(path) => {
741 parsed_templates
742 .global
743 .entry(path.to_owned())
744 .insert_entry(new_templates.collect());
745 self.last_scheduled_tasks.retain(|(kind, _)| {
746 if let TaskSourceKind::AbsPath { abs_path, .. } = kind {
747 abs_path != path
748 } else {
749 true
750 }
751 });
752 }
753 TaskSettingsLocation::Worktree(location) => {
754 let new_templates = new_templates.collect::<Vec<_>>();
755 if new_templates.is_empty() {
756 if let Some(worktree_tasks) =
757 parsed_templates.worktree.get_mut(&location.worktree_id)
758 {
759 worktree_tasks.remove(location.path);
760 }
761 } else {
762 parsed_templates
763 .worktree
764 .entry(location.worktree_id)
765 .or_default()
766 .insert(Arc::from(location.path), new_templates);
767 }
768 self.last_scheduled_tasks.retain(|(kind, _)| {
769 if let TaskSourceKind::Worktree {
770 directory_in_worktree,
771 id,
772 ..
773 } = kind
774 {
775 *id != location.worktree_id
776 || directory_in_worktree.as_ref() != location.path
777 } else {
778 true
779 }
780 });
781 }
782 }
783
784 if !validation_errors.is_empty() {
785 return Err(InvalidSettingsError::Tasks {
786 path: match &location {
787 TaskSettingsLocation::Global(path) => path.to_path_buf(),
788 TaskSettingsLocation::Worktree(location) => {
789 location.path.as_std_path().join(task_file_name())
790 }
791 },
792 message: validation_errors.join("\n"),
793 });
794 }
795
796 Ok(())
797 }
798
799 /// Updates in-memory task metadata from the JSON string given.
800 /// Will fail if the JSON is not a valid array of objects, but will continue if any object will not parse into a [`TaskTemplate`].
801 ///
802 /// Global tasks are updated for no worktree provided, otherwise the worktree metadata for a given path will be updated.
803 pub fn update_file_based_scenarios(
804 &mut self,
805 location: TaskSettingsLocation<'_>,
806 raw_tasks_json: Option<&str>,
807 ) -> Result<(), InvalidSettingsError> {
808 let raw_tasks = match parse_json_with_comments::<Vec<serde_json::Value>>(
809 raw_tasks_json.unwrap_or("[]"),
810 ) {
811 Ok(tasks) => tasks,
812 Err(e) => {
813 return Err(InvalidSettingsError::Debug {
814 path: match location {
815 TaskSettingsLocation::Global(path) => path.to_owned(),
816 TaskSettingsLocation::Worktree(settings_location) => settings_location
817 .path
818 .as_std_path()
819 .join(debug_task_file_name()),
820 },
821 message: format!("Failed to parse tasks file content as a JSON array: {e}"),
822 });
823 }
824 };
825
826 let new_templates = raw_tasks
827 .into_iter()
828 .filter_map(|raw_template| {
829 serde_json::from_value::<DebugScenario>(raw_template).log_err()
830 })
831 .collect::<Vec<_>>();
832
833 let parsed_scenarios = &mut self.scenarios_from_settings;
834 let mut new_definitions: HashMap<_, _> = new_templates
835 .iter()
836 .map(|template| (template.label.clone(), template.clone()))
837 .collect();
838 let previously_existing_scenarios;
839
840 match location {
841 TaskSettingsLocation::Global(path) => {
842 previously_existing_scenarios = parsed_scenarios
843 .global_scenarios()
844 .map(|(_, scenario)| scenario.label)
845 .collect::<HashSet<_>>();
846 parsed_scenarios
847 .global
848 .entry(path.to_owned())
849 .insert_entry(new_templates);
850 }
851 TaskSettingsLocation::Worktree(location) => {
852 previously_existing_scenarios = parsed_scenarios
853 .worktree_scenarios(location.worktree_id)
854 .map(|(_, scenario)| scenario.label)
855 .collect::<HashSet<_>>();
856
857 if new_templates.is_empty() {
858 if let Some(worktree_tasks) =
859 parsed_scenarios.worktree.get_mut(&location.worktree_id)
860 {
861 worktree_tasks.remove(location.path);
862 }
863 } else {
864 parsed_scenarios
865 .worktree
866 .entry(location.worktree_id)
867 .or_default()
868 .insert(Arc::from(location.path), new_templates);
869 }
870 }
871 }
872 self.last_scheduled_scenarios.retain_mut(|(scenario, _)| {
873 if !previously_existing_scenarios.contains(&scenario.label) {
874 return true;
875 }
876 if let Some(new_definition) = new_definitions.remove(&scenario.label) {
877 *scenario = new_definition;
878 true
879 } else {
880 false
881 }
882 });
883
884 Ok(())
885 }
886}
887
888fn task_lru_comparator(
889 (kind_a, task_a, lru_score_a): &(TaskSourceKind, ResolvedTask, u32),
890 (kind_b, task_b, lru_score_b): &(TaskSourceKind, ResolvedTask, u32),
891) -> cmp::Ordering {
892 lru_score_a
893 // First, display recently used templates above all.
894 .cmp(lru_score_b)
895 // Then, ensure more specific sources are displayed first.
896 .then(task_source_kind_preference(kind_a).cmp(&task_source_kind_preference(kind_b)))
897 // After that, display first more specific tasks, using more template variables.
898 // Bonus points for tasks with symbol variables.
899 .then(task_variables_preference(task_a).cmp(&task_variables_preference(task_b)))
900 // Finally, sort by the resolved label, but a bit more specifically, to avoid mixing letters and digits.
901 .then({
902 NumericPrefixWithSuffix::from_numeric_prefixed_str(&task_a.resolved_label)
903 .cmp(&NumericPrefixWithSuffix::from_numeric_prefixed_str(
904 &task_b.resolved_label,
905 ))
906 .then(task_a.resolved_label.cmp(&task_b.resolved_label))
907 .then(kind_a.cmp(kind_b))
908 })
909}
910
911pub fn task_source_kind_preference(kind: &TaskSourceKind) -> u32 {
912 match kind {
913 TaskSourceKind::Lsp { .. } => 0,
914 TaskSourceKind::Language { .. } => 1,
915 TaskSourceKind::UserInput => 2,
916 TaskSourceKind::Worktree { .. } => 3,
917 TaskSourceKind::AbsPath { .. } => 4,
918 }
919}
920
921fn task_variables_preference(task: &ResolvedTask) -> Reverse<usize> {
922 let task_variables = task.substituted_variables();
923 Reverse(if task_variables.contains(&VariableName::Symbol) {
924 task_variables.len() + 1
925 } else {
926 task_variables.len()
927 })
928}
929
930/// A context provided that tries to provide values for all non-custom [`VariableName`] variants for a currently opened file.
931/// Applied as a base for every custom [`ContextProvider`] unless explicitly oped out.
932pub struct BasicContextProvider {
933 worktree_store: Entity<WorktreeStore>,
934 git_store: Entity<GitStore>,
935}
936
937impl BasicContextProvider {
938 pub fn new(worktree_store: Entity<WorktreeStore>, git_store: Entity<GitStore>) -> Self {
939 Self {
940 worktree_store,
941 git_store,
942 }
943 }
944}
945
946impl ContextProvider for BasicContextProvider {
947 fn build_context(
948 &self,
949 _: &TaskVariables,
950 location: ContextLocation<'_>,
951 _: Option<HashMap<String, String>>,
952 _: Arc<dyn LanguageToolchainStore>,
953 cx: &mut App,
954 ) -> Task<Result<TaskVariables>> {
955 let location = location.file_location;
956 let buffer = location.buffer.read(cx);
957 let buffer_snapshot = buffer.snapshot();
958 let symbols = buffer_snapshot.symbols_containing(location.range.start, None);
959 let symbol = symbols.last().map(|symbol| {
960 let range = symbol
961 .name_ranges
962 .last()
963 .cloned()
964 .unwrap_or(0..symbol.text.len());
965 symbol.text[range].to_string()
966 });
967
968 let current_file = buffer.file().and_then(|file| file.as_local());
969 let Point { row, column } = location.range.start.to_point(&buffer_snapshot);
970 let row = row + 1;
971 let column = column + 1;
972 let selected_text = buffer
973 .chars_for_range(location.range.clone())
974 .collect::<String>();
975
976 let mut task_variables = TaskVariables::from_iter([
977 (VariableName::Row, row.to_string()),
978 (VariableName::Column, column.to_string()),
979 ]);
980
981 if let Some(symbol) = symbol {
982 task_variables.insert(VariableName::Symbol, symbol);
983 }
984 if !selected_text.trim().is_empty() {
985 task_variables.insert(VariableName::SelectedText, selected_text);
986 }
987 let worktree = buffer
988 .file()
989 .map(|file| file.worktree_id(cx))
990 .and_then(|worktree_id| {
991 self.worktree_store
992 .read(cx)
993 .worktree_for_id(worktree_id, cx)
994 });
995
996 if let Some(worktree) = worktree {
997 let worktree = worktree.read(cx);
998 let path_style = worktree.path_style();
999 task_variables.insert(
1000 VariableName::WorktreeRoot,
1001 worktree.abs_path().to_string_lossy().into_owned(),
1002 );
1003 if let Some(current_file) = current_file.as_ref() {
1004 let relative_path = current_file.path();
1005 task_variables.insert(
1006 VariableName::RelativeFile,
1007 relative_path.display(path_style).to_string(),
1008 );
1009 if let Some(relative_dir) = relative_path.parent() {
1010 task_variables.insert(
1011 VariableName::RelativeDir,
1012 if relative_dir.is_empty() {
1013 String::from(".")
1014 } else {
1015 relative_dir.display(path_style).to_string()
1016 },
1017 );
1018 }
1019 }
1020 }
1021
1022 if let Some(worktree_id) = location.buffer.read(cx).file().map(|f| f.worktree_id(cx)) {
1023 if let Some(path) = self
1024 .git_store
1025 .read(cx)
1026 .original_repo_path_for_worktree(worktree_id, cx)
1027 {
1028 task_variables.insert(
1029 VariableName::MainGitWorktree,
1030 path.to_string_lossy().into_owned(),
1031 );
1032 }
1033 }
1034
1035 if let Some(current_file) = current_file {
1036 let path = current_file.abs_path(cx);
1037 if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
1038 task_variables.insert(VariableName::Filename, String::from(filename));
1039 }
1040
1041 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
1042 task_variables.insert(VariableName::Stem, stem.into());
1043 }
1044
1045 if let Some(dirname) = path.parent().and_then(|s| s.to_str()) {
1046 task_variables.insert(VariableName::Dirname, dirname.into());
1047 }
1048
1049 task_variables.insert(VariableName::File, path.to_string_lossy().into_owned());
1050 }
1051
1052 if let Some(language) = buffer.language() {
1053 task_variables.insert(VariableName::Language, language.name().to_string());
1054 }
1055
1056 Task::ready(Ok(task_variables))
1057 }
1058}
1059
1060/// A ContextProvider that doesn't provide any task variables on it's own, though it has some associated tasks.
1061pub struct ContextProviderWithTasks {
1062 templates: TaskTemplates,
1063}
1064
1065impl ContextProviderWithTasks {
1066 pub fn new(definitions: TaskTemplates) -> Self {
1067 Self {
1068 templates: definitions,
1069 }
1070 }
1071}
1072
1073impl ContextProvider for ContextProviderWithTasks {
1074 fn associated_tasks(&self, _: Option<Entity<Buffer>>, _: &App) -> Task<Option<TaskTemplates>> {
1075 Task::ready(Some(self.templates.clone()))
1076 }
1077}