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