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 std::{
  4    borrow::Cow,
  5    path::{Path, PathBuf},
  6    sync::Arc,
  7};
  8
  9use collections::HashMap;
 10use futures::StreamExt;
 11use gpui::{AppContext, Context, Model, ModelContext, Subscription};
 12use schemars::{gen::SchemaSettings, JsonSchema};
 13use serde::{Deserialize, Serialize};
 14use util::ResultExt;
 15
 16use crate::{SpawnInTerminal, Task, TaskId, TaskSource};
 17use futures::channel::mpsc::UnboundedReceiver;
 18
 19/// A single config file entry with the deserialized task definition.
 20#[derive(Clone, Debug, PartialEq)]
 21struct StaticTask {
 22    id: TaskId,
 23    definition: Definition,
 24}
 25
 26impl Task for StaticTask {
 27    fn exec(&self, cwd: Option<PathBuf>) -> Option<SpawnInTerminal> {
 28        Some(SpawnInTerminal {
 29            id: self.id.clone(),
 30            cwd,
 31            use_new_terminal: self.definition.use_new_terminal,
 32            allow_concurrent_runs: self.definition.allow_concurrent_runs,
 33            label: self.definition.label.clone(),
 34            command: self.definition.command.clone(),
 35            args: self.definition.args.clone(),
 36            env: self.definition.env.clone(),
 37            separate_shell: false,
 38        })
 39    }
 40
 41    fn name(&self) -> &str {
 42        &self.definition.label
 43    }
 44
 45    fn id(&self) -> &TaskId {
 46        &self.id
 47    }
 48
 49    fn cwd(&self) -> Option<&Path> {
 50        self.definition.cwd.as_deref()
 51    }
 52}
 53
 54/// The source of tasks defined in a tasks config file.
 55pub struct StaticSource {
 56    tasks: Vec<StaticTask>,
 57    _definitions: Model<TrackedFile<DefinitionProvider>>,
 58    _subscription: Subscription,
 59}
 60
 61/// Static task definition from the tasks config file.
 62#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
 63pub(crate) struct Definition {
 64    /// Human readable name of the task to display in the UI.
 65    pub label: String,
 66    /// Executable command to spawn.
 67    pub command: String,
 68    /// Arguments to the command.
 69    #[serde(default)]
 70    pub args: Vec<String>,
 71    /// Env overrides for the command, will be appended to the terminal's environment from the settings.
 72    #[serde(default)]
 73    pub env: HashMap<String, String>,
 74    /// Current working directory to spawn the command into, defaults to current project root.
 75    #[serde(default)]
 76    pub cwd: Option<PathBuf>,
 77    /// Whether to use a new terminal tab or reuse the existing one to spawn the process.
 78    #[serde(default)]
 79    pub use_new_terminal: bool,
 80    /// Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish.
 81    #[serde(default)]
 82    pub allow_concurrent_runs: bool,
 83}
 84
 85/// A group of Tasks defined in a JSON file.
 86#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
 87pub struct DefinitionProvider(Vec<Definition>);
 88
 89impl DefinitionProvider {
 90    /// Generates JSON schema of Tasks JSON definition format.
 91    pub fn generate_json_schema() -> serde_json_lenient::Value {
 92        let schema = SchemaSettings::draft07()
 93            .with(|settings| settings.option_add_null_type = false)
 94            .into_generator()
 95            .into_root_schema_for::<Self>();
 96
 97        serde_json_lenient::to_value(schema).unwrap()
 98    }
 99}
100/// A Wrapper around deserializable T that keeps track of its contents
101/// via a provided channel. Once T value changes, the observers of [`TrackedFile`] are
102/// notified.
103struct TrackedFile<T> {
104    parsed_contents: T,
105}
106
107impl<T: for<'a> Deserialize<'a> + PartialEq + 'static> TrackedFile<T> {
108    fn new(
109        parsed_contents: T,
110        mut tracker: UnboundedReceiver<String>,
111        cx: &mut AppContext,
112    ) -> Model<Self> {
113        cx.new_model(move |cx| {
114            cx.spawn(|tracked_file, mut cx| async move {
115                while let Some(new_contents) = tracker.next().await {
116                    if !new_contents.trim().is_empty() {
117                        let Some(new_contents) =
118                            serde_json_lenient::from_str(&new_contents).log_err()
119                        else {
120                            continue;
121                        };
122                        tracked_file.update(&mut cx, |tracked_file: &mut TrackedFile<T>, cx| {
123                            if tracked_file.parsed_contents != new_contents {
124                                tracked_file.parsed_contents = new_contents;
125                                cx.notify();
126                            };
127                        })?;
128                    }
129                }
130                anyhow::Ok(())
131            })
132            .detach_and_log_err(cx);
133            Self { parsed_contents }
134        })
135    }
136
137    fn get(&self) -> &T {
138        &self.parsed_contents
139    }
140}
141
142impl StaticSource {
143    /// Initializes the static source, reacting on tasks config changes.
144    pub fn new(
145        id_base: impl Into<Cow<'static, str>>,
146        tasks_file_tracker: UnboundedReceiver<String>,
147        cx: &mut AppContext,
148    ) -> Model<Box<dyn TaskSource>> {
149        let definitions = TrackedFile::new(DefinitionProvider::default(), tasks_file_tracker, cx);
150        cx.new_model(|cx| {
151            let id_base = id_base.into();
152            let _subscription = cx.observe(
153                &definitions,
154                move |source: &mut Box<(dyn TaskSource + 'static)>, new_definitions, cx| {
155                    if let Some(static_source) = source.as_any().downcast_mut::<Self>() {
156                        static_source.tasks = new_definitions
157                            .read(cx)
158                            .get()
159                            .0
160                            .clone()
161                            .into_iter()
162                            .enumerate()
163                            .map(|(i, definition)| StaticTask {
164                                id: TaskId(format!("static_{id_base}_{i}_{}", definition.label)),
165                                definition,
166                            })
167                            .collect();
168                        cx.notify();
169                    }
170                },
171            );
172            Box::new(Self {
173                tasks: Vec::new(),
174                _definitions: definitions,
175                _subscription,
176            })
177        })
178    }
179}
180
181impl TaskSource for StaticSource {
182    fn tasks_for_path(
183        &mut self,
184        _: Option<&Path>,
185        _: &mut ModelContext<Box<dyn TaskSource>>,
186    ) -> Vec<Arc<dyn Task>> {
187        self.tasks
188            .clone()
189            .into_iter()
190            .map(|task| Arc::new(task) as Arc<dyn Task>)
191            .collect()
192    }
193
194    fn as_any(&mut self) -> &mut dyn std::any::Any {
195        self
196    }
197}