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}