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