1//! A source of tasks, based on a static configuration, deserialized from the tasks config file, and related infrastructure for tracking changes to the file.
2
3use futures::StreamExt;
4use gpui::{AppContext, Context, Model, ModelContext, Subscription};
5use serde::Deserialize;
6use util::ResultExt;
7
8use crate::{TaskSource, TaskTemplates};
9use futures::channel::mpsc::UnboundedReceiver;
10
11/// The source of tasks defined in a tasks config file.
12pub struct StaticSource {
13 tasks: TaskTemplates,
14 _templates: Model<TrackedFile<TaskTemplates>>,
15 _subscription: Subscription,
16}
17
18/// A Wrapper around deserializable T that keeps track of its contents
19/// via a provided channel. Once T value changes, the observers of [`TrackedFile`] are
20/// notified.
21pub struct TrackedFile<T> {
22 parsed_contents: T,
23}
24
25impl<T: PartialEq + 'static> TrackedFile<T> {
26 /// Initializes new [`TrackedFile`] with a type that's deserializable.
27 pub fn new(mut tracker: UnboundedReceiver<String>, cx: &mut AppContext) -> Model<Self>
28 where
29 T: for<'a> Deserialize<'a> + Default,
30 {
31 cx.new_model(move |cx| {
32 cx.spawn(|tracked_file, mut cx| async move {
33 while let Some(new_contents) = tracker.next().await {
34 if !new_contents.trim().is_empty() {
35 // String -> T (ZedTaskFormat)
36 // String -> U (VsCodeFormat) -> Into::into T
37 let Some(new_contents) =
38 serde_json_lenient::from_str(&new_contents).log_err()
39 else {
40 continue;
41 };
42 tracked_file.update(&mut cx, |tracked_file: &mut TrackedFile<T>, cx| {
43 if tracked_file.parsed_contents != new_contents {
44 tracked_file.parsed_contents = new_contents;
45 cx.notify();
46 };
47 })?;
48 }
49 }
50 anyhow::Ok(())
51 })
52 .detach_and_log_err(cx);
53 Self {
54 parsed_contents: Default::default(),
55 }
56 })
57 }
58
59 /// Initializes new [`TrackedFile`] with a type that's convertible from another deserializable type.
60 pub fn new_convertible<U: for<'a> Deserialize<'a> + TryInto<T, Error = anyhow::Error>>(
61 mut tracker: UnboundedReceiver<String>,
62 cx: &mut AppContext,
63 ) -> Model<Self>
64 where
65 T: Default,
66 {
67 cx.new_model(move |cx| {
68 cx.spawn(|tracked_file, mut cx| async move {
69 while let Some(new_contents) = tracker.next().await {
70 if !new_contents.trim().is_empty() {
71 let Some(new_contents) =
72 serde_json_lenient::from_str::<U>(&new_contents).log_err()
73 else {
74 continue;
75 };
76 let Some(new_contents) = new_contents.try_into().log_err() else {
77 continue;
78 };
79 tracked_file.update(&mut cx, |tracked_file: &mut TrackedFile<T>, cx| {
80 if tracked_file.parsed_contents != new_contents {
81 tracked_file.parsed_contents = new_contents;
82 cx.notify();
83 };
84 })?;
85 }
86 }
87 anyhow::Ok(())
88 })
89 .detach_and_log_err(cx);
90 Self {
91 parsed_contents: Default::default(),
92 }
93 })
94 }
95
96 fn get(&self) -> &T {
97 &self.parsed_contents
98 }
99}
100
101impl StaticSource {
102 /// Initializes the static source, reacting on tasks config changes.
103 pub fn new(
104 templates: Model<TrackedFile<TaskTemplates>>,
105 cx: &mut AppContext,
106 ) -> Model<Box<dyn TaskSource>> {
107 cx.new_model(|cx| {
108 let _subscription = cx.observe(
109 &templates,
110 move |source: &mut Box<(dyn TaskSource + 'static)>, new_templates, cx| {
111 if let Some(static_source) = source.as_any().downcast_mut::<Self>() {
112 static_source.tasks = new_templates.read(cx).get().clone();
113 cx.notify();
114 }
115 },
116 );
117 Box::new(Self {
118 tasks: TaskTemplates::default(),
119 _templates: templates,
120 _subscription,
121 })
122 })
123 }
124}
125
126impl TaskSource for StaticSource {
127 fn tasks_to_schedule(&mut self, _: &mut ModelContext<Box<dyn TaskSource>>) -> TaskTemplates {
128 self.tasks.clone()
129 }
130
131 fn as_any(&mut self) -> &mut dyn std::any::Any {
132 self
133 }
134}