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