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