static_source.rs

  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}