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