settings.rs

  1use anyhow::Result;
  2use gpui::font_cache::{FamilyId, FontCache};
  3use schemars::{
  4    gen::{SchemaGenerator, SchemaSettings},
  5    JsonSchema,
  6};
  7use serde::Deserialize;
  8use std::{collections::HashMap, fmt::Display, sync::Arc};
  9use theme::{Theme, ThemeRegistry};
 10use util::ResultExt as _;
 11
 12#[derive(Clone)]
 13pub struct Settings {
 14    pub buffer_font_family: FamilyId,
 15    pub buffer_font_size: f32,
 16    pub vim_mode: bool,
 17    pub tab_size: u32,
 18    pub soft_wrap: SoftWrap,
 19    pub preferred_line_length: u32,
 20    pub language_overrides: HashMap<Arc<str>, LanguageOverride>,
 21    pub theme: Arc<Theme>,
 22}
 23
 24#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
 25pub struct LanguageOverride {
 26    pub tab_size: Option<u32>,
 27    pub soft_wrap: Option<SoftWrap>,
 28    pub preferred_line_length: Option<u32>,
 29}
 30
 31#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
 32#[serde(rename_all = "snake_case")]
 33pub enum SoftWrap {
 34    None,
 35    EditorWidth,
 36    PreferredLineLength,
 37}
 38
 39#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
 40pub enum ThemeNames {
 41    Dark,
 42    Light,
 43}
 44
 45impl Display for ThemeNames {
 46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 47        match self {
 48            ThemeNames::Dark => write!(f, "Dark"),
 49            ThemeNames::Light => write!(f, "Light"),
 50        }
 51    }
 52}
 53
 54#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
 55pub struct LanguageOverrides {
 56    C: Option<LanguageOverride>,
 57    JSON: Option<LanguageOverride>,
 58    Markdown: Option<LanguageOverride>,
 59    PlainText: Option<LanguageOverride>,
 60    Rust: Option<LanguageOverride>,
 61    TSX: Option<LanguageOverride>,
 62    TypeScript: Option<LanguageOverride>,
 63}
 64
 65impl IntoIterator for LanguageOverrides {
 66    type Item = (String, LanguageOverride);
 67    type IntoIter = std::vec::IntoIter<Self::Item>;
 68
 69    fn into_iter(self) -> Self::IntoIter {
 70        vec![
 71            ("C", self.C),
 72            ("JSON", self.JSON),
 73            ("Markdown", self.Markdown),
 74            ("PlainText", self.PlainText),
 75            ("Rust", self.Rust),
 76            ("TSX", self.TSX),
 77            ("TypeScript", self.TypeScript),
 78        ]
 79        .into_iter()
 80        .filter_map(|(name, language_override)| language_override.map(|lo| (name.to_owned(), lo)))
 81        .collect::<Vec<_>>()
 82        .into_iter()
 83    }
 84}
 85
 86#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
 87pub struct SettingsFileContent {
 88    #[serde(default)]
 89    pub buffer_font_family: Option<String>,
 90    #[serde(default)]
 91    pub buffer_font_size: Option<f32>,
 92    #[serde(default)]
 93    pub vim_mode: Option<bool>,
 94    #[serde(flatten)]
 95    pub editor: LanguageOverride,
 96    #[serde(default)]
 97    pub language_overrides: LanguageOverrides,
 98    #[serde(default)]
 99    pub theme: Option<ThemeNames>,
100}
101
102impl Settings {
103    pub fn new(
104        buffer_font_family: &str,
105        font_cache: &FontCache,
106        theme: Arc<Theme>,
107    ) -> Result<Self> {
108        Ok(Self {
109            buffer_font_family: font_cache.load_family(&[buffer_font_family])?,
110            buffer_font_size: 15.,
111            vim_mode: false,
112            tab_size: 4,
113            soft_wrap: SoftWrap::None,
114            preferred_line_length: 80,
115            language_overrides: Default::default(),
116            theme,
117        })
118    }
119
120    pub fn file_json_schema() -> serde_json::Value {
121        let settings = SchemaSettings::draft07().with(|settings| {
122            settings.option_nullable = true;
123            settings.option_add_null_type = false;
124        });
125        let generator = SchemaGenerator::new(settings);
126        serde_json::to_value(generator.into_root_schema_for::<SettingsFileContent>()).unwrap()
127    }
128
129    pub fn with_overrides(
130        mut self,
131        language_name: impl Into<Arc<str>>,
132        overrides: LanguageOverride,
133    ) -> Self {
134        self.language_overrides
135            .insert(language_name.into(), overrides);
136        self
137    }
138
139    pub fn tab_size(&self, language: Option<&str>) -> u32 {
140        language
141            .and_then(|language| self.language_overrides.get(language))
142            .and_then(|settings| settings.tab_size)
143            .unwrap_or(self.tab_size)
144    }
145
146    pub fn soft_wrap(&self, language: Option<&str>) -> SoftWrap {
147        language
148            .and_then(|language| self.language_overrides.get(language))
149            .and_then(|settings| settings.soft_wrap)
150            .unwrap_or(self.soft_wrap)
151    }
152
153    pub fn preferred_line_length(&self, language: Option<&str>) -> u32 {
154        language
155            .and_then(|language| self.language_overrides.get(language))
156            .and_then(|settings| settings.preferred_line_length)
157            .unwrap_or(self.preferred_line_length)
158    }
159
160    #[cfg(any(test, feature = "test-support"))]
161    pub fn test(cx: &gpui::AppContext) -> Settings {
162        Settings {
163            buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
164            buffer_font_size: 14.,
165            vim_mode: false,
166            tab_size: 4,
167            soft_wrap: SoftWrap::None,
168            preferred_line_length: 80,
169            language_overrides: Default::default(),
170            theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), || Default::default()),
171        }
172    }
173
174    pub fn merge(
175        &mut self,
176        data: &SettingsFileContent,
177        theme_registry: &ThemeRegistry,
178        font_cache: &FontCache,
179    ) {
180        if let Some(value) = &data.buffer_font_family {
181            if let Some(id) = font_cache.load_family(&[value]).log_err() {
182                self.buffer_font_family = id;
183            }
184        }
185        if let Some(value) = &data.theme {
186            if let Some(theme) = theme_registry.get(&value.to_string()).log_err() {
187                self.theme = theme;
188            }
189        }
190
191        merge(&mut self.buffer_font_size, data.buffer_font_size);
192        merge(&mut self.vim_mode, data.vim_mode);
193        merge(&mut self.soft_wrap, data.editor.soft_wrap);
194        merge(&mut self.tab_size, data.editor.tab_size);
195        merge(
196            &mut self.preferred_line_length,
197            data.editor.preferred_line_length,
198        );
199
200        for (language_name, settings) in data.language_overrides.clone().into_iter() {
201            let target = self
202                .language_overrides
203                .entry(language_name.into())
204                .or_default();
205
206            merge_option(&mut target.tab_size, settings.tab_size);
207            merge_option(&mut target.soft_wrap, settings.soft_wrap);
208            merge_option(
209                &mut target.preferred_line_length,
210                settings.preferred_line_length,
211            );
212        }
213    }
214}
215
216fn merge<T: Copy>(target: &mut T, value: Option<T>) {
217    if let Some(value) = value {
218        *target = value;
219    }
220}
221
222fn merge_option<T: Copy>(target: &mut Option<T>, value: Option<T>) {
223    if value.is_some() {
224        *target = value;
225    }
226}