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