settings.rs

  1use anyhow::Result;
  2use futures::{stream, SinkExt, StreamExt as _};
  3use gpui::{
  4    executor,
  5    font_cache::{FamilyId, FontCache},
  6};
  7use language::Language;
  8use parking_lot::Mutex;
  9use postage::{prelude::Stream, watch};
 10use project::Fs;
 11use serde::Deserialize;
 12use std::{collections::HashMap, path::Path, sync::Arc, time::Duration};
 13use theme::{Theme, ThemeRegistry};
 14use util::ResultExt;
 15
 16#[derive(Clone)]
 17pub struct Settings {
 18    pub buffer_font_family: FamilyId,
 19    pub buffer_font_size: f32,
 20    pub tab_size: usize,
 21    pub soft_wrap: SoftWrap,
 22    pub preferred_line_length: u32,
 23    pub language_overrides: HashMap<Arc<str>, LanguageOverride>,
 24    pub theme: Arc<Theme>,
 25}
 26
 27#[derive(Clone, Debug, Default, Deserialize)]
 28pub struct LanguageOverride {
 29    pub tab_size: Option<usize>,
 30    pub soft_wrap: Option<SoftWrap>,
 31    pub preferred_line_length: Option<u32>,
 32}
 33
 34#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq)]
 35#[serde(rename_all = "snake_case")]
 36pub enum SoftWrap {
 37    None,
 38    EditorWidth,
 39    PreferredLineLength,
 40}
 41
 42#[derive(Clone)]
 43pub struct SettingsFile(watch::Receiver<SettingsFileContent>);
 44
 45#[derive(Clone, Debug, Default, Deserialize)]
 46struct SettingsFileContent {
 47    #[serde(default)]
 48    buffer_font_family: Option<String>,
 49    #[serde(default)]
 50    buffer_font_size: Option<f32>,
 51    #[serde(flatten)]
 52    editor: LanguageOverride,
 53    #[serde(default)]
 54    language_overrides: HashMap<Arc<str>, LanguageOverride>,
 55    #[serde(default)]
 56    theme: Option<String>,
 57}
 58
 59impl SettingsFile {
 60    pub async fn new(
 61        fs: Arc<dyn Fs>,
 62        executor: &executor::Background,
 63        path: impl Into<Arc<Path>>,
 64    ) -> Self {
 65        let path = path.into();
 66        let settings = Self::load(fs.clone(), &path).await.unwrap_or_default();
 67        let mut events = fs.watch(&path, Duration::from_millis(500)).await;
 68        let (mut tx, mut rx) = watch::channel_with(settings);
 69        rx.recv().await;
 70        executor
 71            .spawn(async move {
 72                while events.next().await.is_some() {
 73                    if let Some(settings) = Self::load(fs.clone(), &path).await {
 74                        if tx.send(settings).await.is_err() {
 75                            break;
 76                        }
 77                    }
 78                }
 79            })
 80            .detach();
 81        Self(rx)
 82    }
 83
 84    async fn load(fs: Arc<dyn Fs>, path: &Path) -> Option<SettingsFileContent> {
 85        if fs.is_file(&path).await {
 86            fs.load(&path)
 87                .await
 88                .log_err()
 89                .and_then(|data| serde_json::from_str(&data).log_err())
 90        } else {
 91            Some(SettingsFileContent::default())
 92        }
 93    }
 94}
 95
 96impl Settings {
 97    pub fn from_files(
 98        defaults: Self,
 99        sources: Vec<SettingsFile>,
100        executor: Arc<executor::Background>,
101        theme_registry: Arc<ThemeRegistry>,
102        font_cache: Arc<FontCache>,
103    ) -> (Arc<Mutex<watch::Sender<Self>>>, watch::Receiver<Self>) {
104        let (tx, mut rx) = watch::channel_with(defaults.clone());
105        let tx = Arc::new(Mutex::new(tx));
106        executor
107            .spawn({
108                let tx = tx.clone();
109                async move {
110                    let mut stream =
111                        stream::select_all(sources.iter().map(|source| source.0.clone()));
112                    while stream.next().await.is_some() {
113                        let mut settings = defaults.clone();
114                        for source in &sources {
115                            settings.merge(&*source.0.borrow(), &theme_registry, &font_cache);
116                        }
117                        *tx.lock().borrow_mut() = settings;
118                    }
119                }
120            })
121            .detach();
122        rx.try_recv().ok();
123        (tx, rx)
124    }
125
126    pub fn new(
127        buffer_font_family: &str,
128        font_cache: &FontCache,
129        theme: Arc<Theme>,
130    ) -> Result<Self> {
131        Ok(Self {
132            buffer_font_family: font_cache.load_family(&[buffer_font_family])?,
133            buffer_font_size: 15.,
134            tab_size: 4,
135            soft_wrap: SoftWrap::None,
136            preferred_line_length: 80,
137            language_overrides: Default::default(),
138            theme,
139        })
140    }
141
142    pub fn with_overrides(
143        mut self,
144        language_name: impl Into<Arc<str>>,
145        overrides: LanguageOverride,
146    ) -> Self {
147        self.language_overrides
148            .insert(language_name.into(), overrides);
149        self
150    }
151
152    pub fn tab_size(&self, language: Option<&Arc<Language>>) -> usize {
153        language
154            .and_then(|language| self.language_overrides.get(language.name().as_ref()))
155            .and_then(|settings| settings.tab_size)
156            .unwrap_or(self.tab_size)
157    }
158
159    pub fn soft_wrap(&self, language: Option<&Arc<Language>>) -> SoftWrap {
160        language
161            .and_then(|language| self.language_overrides.get(language.name().as_ref()))
162            .and_then(|settings| settings.soft_wrap)
163            .unwrap_or(self.soft_wrap)
164    }
165
166    pub fn preferred_line_length(&self, language: Option<&Arc<Language>>) -> u32 {
167        language
168            .and_then(|language| self.language_overrides.get(language.name().as_ref()))
169            .and_then(|settings| settings.preferred_line_length)
170            .unwrap_or(self.preferred_line_length)
171    }
172
173    #[cfg(any(test, feature = "test-support"))]
174    pub fn test(cx: &gpui::AppContext) -> Settings {
175        Settings {
176            buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
177            buffer_font_size: 14.,
178            tab_size: 4,
179            soft_wrap: SoftWrap::None,
180            preferred_line_length: 80,
181            language_overrides: Default::default(),
182            theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), || Default::default()),
183        }
184    }
185
186    fn merge(
187        &mut self,
188        data: &SettingsFileContent,
189        theme_registry: &ThemeRegistry,
190        font_cache: &FontCache,
191    ) {
192        if let Some(value) = &data.buffer_font_family {
193            if let Some(id) = font_cache.load_family(&[value]).log_err() {
194                self.buffer_font_family = id;
195            }
196        }
197        if let Some(value) = &data.theme {
198            if let Some(theme) = theme_registry.get(value).log_err() {
199                self.theme = theme;
200            }
201        }
202
203        merge(&mut self.buffer_font_size, data.buffer_font_size);
204        merge(&mut self.soft_wrap, data.editor.soft_wrap);
205        merge(&mut self.tab_size, data.editor.tab_size);
206        merge(
207            &mut self.preferred_line_length,
208            data.editor.preferred_line_length,
209        );
210
211        for (language_name, settings) in &data.language_overrides {
212            let target = self
213                .language_overrides
214                .entry(language_name.clone())
215                .or_default();
216
217            merge_option(&mut target.tab_size, settings.tab_size);
218            merge_option(&mut target.soft_wrap, settings.soft_wrap);
219            merge_option(
220                &mut target.preferred_line_length,
221                settings.preferred_line_length,
222            );
223        }
224    }
225}
226
227fn merge<T: Copy>(target: &mut T, value: Option<T>) {
228    if let Some(value) = value {
229        *target = value;
230    }
231}
232
233fn merge_option<T: Copy>(target: &mut Option<T>, value: Option<T>) {
234    if value.is_some() {
235        *target = value;
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242    use postage::prelude::Stream;
243    use project::FakeFs;
244
245    #[gpui::test]
246    async fn test_settings_from_files(cx: &mut gpui::TestAppContext) {
247        let executor = cx.background();
248        let fs = FakeFs::new(executor.clone());
249
250        fs.save(
251            "/settings1.json".as_ref(),
252            &r#"
253            {
254                "buffer_font_size": 24,
255                "soft_wrap": "editor_width",
256                "language_overrides": {
257                    "Markdown": {
258                        "preferred_line_length": 100,
259                        "soft_wrap": "preferred_line_length"
260                    }
261                }
262            }
263            "#
264            .into(),
265        )
266        .await
267        .unwrap();
268
269        let source1 = SettingsFile::new(fs.clone(), &executor, "/settings1.json".as_ref()).await;
270        let source2 = SettingsFile::new(fs.clone(), &executor, "/settings2.json".as_ref()).await;
271        let source3 = SettingsFile::new(fs.clone(), &executor, "/settings3.json".as_ref()).await;
272
273        let (_, mut settings_rx) = Settings::from_files(
274            cx.read(Settings::test),
275            vec![source1, source2, source3],
276            cx.background(),
277            ThemeRegistry::new((), cx.font_cache()),
278            cx.font_cache(),
279        );
280
281        let settings = settings_rx.recv().await.unwrap();
282        let md_settings = settings.language_overrides.get("Markdown").unwrap();
283        assert_eq!(settings.soft_wrap, SoftWrap::EditorWidth);
284        assert_eq!(settings.buffer_font_size, 24.0);
285        assert_eq!(settings.tab_size, 4);
286        assert_eq!(md_settings.soft_wrap, Some(SoftWrap::PreferredLineLength));
287        assert_eq!(md_settings.preferred_line_length, Some(100));
288
289        fs.save(
290            "/settings2.json".as_ref(),
291            &r#"
292            {
293                "tab_size": 2,
294                "soft_wrap": "none",
295                "language_overrides": {
296                    "Markdown": {
297                        "preferred_line_length": 120
298                    }
299                }
300            }
301            "#
302            .into(),
303        )
304        .await
305        .unwrap();
306
307        let settings = settings_rx.recv().await.unwrap();
308        let md_settings = settings.language_overrides.get("Markdown").unwrap();
309        assert_eq!(settings.soft_wrap, SoftWrap::None);
310        assert_eq!(settings.buffer_font_size, 24.0);
311        assert_eq!(settings.tab_size, 2);
312        assert_eq!(md_settings.soft_wrap, Some(SoftWrap::PreferredLineLength));
313        assert_eq!(md_settings.preferred_line_length, Some(120));
314
315        fs.remove_file("/settings2.json".as_ref(), Default::default())
316            .await
317            .unwrap();
318
319        let settings = settings_rx.recv().await.unwrap();
320        assert_eq!(settings.tab_size, 4);
321    }
322}