1mod keymap_file;
2
3use anyhow::Result;
4use gpui::font_cache::{FamilyId, FontCache};
5use schemars::{
6 gen::{SchemaGenerator, SchemaSettings},
7 schema::{
8 InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec, SubschemaValidation,
9 },
10 JsonSchema,
11};
12use serde::{de::DeserializeOwned, Deserialize};
13use serde_json::Value;
14use std::{collections::HashMap, sync::Arc};
15use theme::{Theme, ThemeRegistry};
16use util::ResultExt as _;
17
18pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
19
20#[derive(Clone)]
21pub struct Settings {
22 pub projects_online_by_default: bool,
23 pub buffer_font_family: FamilyId,
24 pub buffer_font_size: f32,
25 pub default_buffer_font_size: f32,
26 pub vim_mode: bool,
27 pub tab_size: u32,
28 pub hard_tabs: bool,
29 pub soft_wrap: SoftWrap,
30 pub preferred_line_length: u32,
31 pub format_on_save: bool,
32 pub language_overrides: HashMap<Arc<str>, LanguageOverride>,
33 pub theme: Arc<Theme>,
34}
35
36#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
37pub struct LanguageOverride {
38 pub tab_size: Option<u32>,
39 pub hard_tabs: Option<bool>,
40 pub soft_wrap: Option<SoftWrap>,
41 pub preferred_line_length: Option<u32>,
42 pub format_on_save: Option<bool>,
43}
44
45#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
46#[serde(rename_all = "snake_case")]
47pub enum SoftWrap {
48 None,
49 EditorWidth,
50 PreferredLineLength,
51}
52
53#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
54pub struct SettingsFileContent {
55 #[serde(default)]
56 pub projects_online_by_default: Option<bool>,
57 #[serde(default)]
58 pub buffer_font_family: Option<String>,
59 #[serde(default)]
60 pub buffer_font_size: Option<f32>,
61 #[serde(default)]
62 pub vim_mode: Option<bool>,
63 #[serde(default)]
64 pub format_on_save: Option<bool>,
65 #[serde(flatten)]
66 pub editor: LanguageOverride,
67 #[serde(default)]
68 pub language_overrides: HashMap<Arc<str>, LanguageOverride>,
69 #[serde(default)]
70 pub theme: Option<String>,
71}
72
73impl Settings {
74 pub fn new(
75 buffer_font_family: &str,
76 font_cache: &FontCache,
77 theme: Arc<Theme>,
78 ) -> Result<Self> {
79 Ok(Self {
80 buffer_font_family: font_cache.load_family(&[buffer_font_family])?,
81 buffer_font_size: 15.,
82 default_buffer_font_size: 15.,
83 vim_mode: false,
84 tab_size: 4,
85 hard_tabs: false,
86 soft_wrap: SoftWrap::None,
87 preferred_line_length: 80,
88 language_overrides: Default::default(),
89 format_on_save: true,
90 projects_online_by_default: true,
91 theme,
92 })
93 }
94
95 pub fn with_overrides(
96 mut self,
97 language_name: impl Into<Arc<str>>,
98 overrides: LanguageOverride,
99 ) -> Self {
100 self.language_overrides
101 .insert(language_name.into(), overrides);
102 self
103 }
104
105 pub fn tab_size(&self, language: Option<&str>) -> u32 {
106 language
107 .and_then(|language| self.language_overrides.get(language))
108 .and_then(|settings| settings.tab_size)
109 .unwrap_or(self.tab_size)
110 }
111
112 pub fn hard_tabs(&self, language: Option<&str>) -> bool {
113 language
114 .and_then(|language| self.language_overrides.get(language))
115 .and_then(|settings| settings.hard_tabs)
116 .unwrap_or(self.hard_tabs)
117 }
118
119 pub fn soft_wrap(&self, language: Option<&str>) -> SoftWrap {
120 language
121 .and_then(|language| self.language_overrides.get(language))
122 .and_then(|settings| settings.soft_wrap)
123 .unwrap_or(self.soft_wrap)
124 }
125
126 pub fn preferred_line_length(&self, language: Option<&str>) -> u32 {
127 language
128 .and_then(|language| self.language_overrides.get(language))
129 .and_then(|settings| settings.preferred_line_length)
130 .unwrap_or(self.preferred_line_length)
131 }
132
133 pub fn format_on_save(&self, language: Option<&str>) -> bool {
134 language
135 .and_then(|language| self.language_overrides.get(language))
136 .and_then(|settings| settings.format_on_save)
137 .unwrap_or(self.format_on_save)
138 }
139
140 #[cfg(any(test, feature = "test-support"))]
141 pub fn test(cx: &gpui::AppContext) -> Settings {
142 Settings {
143 buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
144 buffer_font_size: 14.,
145 default_buffer_font_size: 14.,
146 vim_mode: false,
147 tab_size: 4,
148 hard_tabs: false,
149 soft_wrap: SoftWrap::None,
150 preferred_line_length: 80,
151 format_on_save: true,
152 language_overrides: Default::default(),
153 projects_online_by_default: true,
154 theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), || Default::default()),
155 }
156 }
157
158 #[cfg(any(test, feature = "test-support"))]
159 pub fn test_async(cx: &mut gpui::TestAppContext) {
160 cx.update(|cx| {
161 let settings = Self::test(cx);
162 cx.set_global(settings.clone());
163 });
164 }
165
166 pub fn merge(
167 &mut self,
168 data: &SettingsFileContent,
169 theme_registry: &ThemeRegistry,
170 font_cache: &FontCache,
171 ) {
172 if let Some(value) = &data.buffer_font_family {
173 if let Some(id) = font_cache.load_family(&[value]).log_err() {
174 self.buffer_font_family = id;
175 }
176 }
177 if let Some(value) = &data.theme {
178 if let Some(theme) = theme_registry.get(&value.to_string()).log_err() {
179 self.theme = theme;
180 }
181 }
182
183 merge(
184 &mut self.projects_online_by_default,
185 data.projects_online_by_default,
186 );
187 merge(&mut self.buffer_font_size, data.buffer_font_size);
188 merge(&mut self.default_buffer_font_size, data.buffer_font_size);
189 merge(&mut self.vim_mode, data.vim_mode);
190 merge(&mut self.format_on_save, data.format_on_save);
191 merge(&mut self.soft_wrap, data.editor.soft_wrap);
192 merge(&mut self.tab_size, data.editor.tab_size);
193 merge(
194 &mut self.preferred_line_length,
195 data.editor.preferred_line_length,
196 );
197
198 for (language_name, settings) in data.language_overrides.clone().into_iter() {
199 let target = self
200 .language_overrides
201 .entry(language_name.into())
202 .or_default();
203
204 merge_option(&mut target.tab_size, settings.tab_size);
205 merge_option(&mut target.soft_wrap, settings.soft_wrap);
206 merge_option(&mut target.format_on_save, settings.format_on_save);
207 merge_option(
208 &mut target.preferred_line_length,
209 settings.preferred_line_length,
210 );
211 }
212 }
213}
214
215pub fn settings_file_json_schema(
216 theme_names: Vec<String>,
217 language_names: Vec<String>,
218) -> serde_json::Value {
219 let settings = SchemaSettings::draft07().with(|settings| {
220 settings.option_add_null_type = false;
221 });
222 let generator = SchemaGenerator::new(settings);
223 let mut root_schema = generator.into_root_schema_for::<SettingsFileContent>();
224
225 // Construct theme names reference type
226 let theme_names = theme_names
227 .into_iter()
228 .map(|name| Value::String(name))
229 .collect();
230 let theme_names_schema = Schema::Object(SchemaObject {
231 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
232 enum_values: Some(theme_names),
233 ..Default::default()
234 });
235 root_schema
236 .definitions
237 .insert("ThemeName".to_owned(), theme_names_schema);
238
239 // Construct language overrides reference type
240 let language_override_schema_reference = Schema::Object(SchemaObject {
241 reference: Some("#/definitions/LanguageOverride".to_owned()),
242 ..Default::default()
243 });
244 let language_overrides_properties = language_names
245 .into_iter()
246 .map(|name| {
247 (
248 name,
249 Schema::Object(SchemaObject {
250 subschemas: Some(Box::new(SubschemaValidation {
251 all_of: Some(vec![language_override_schema_reference.clone()]),
252 ..Default::default()
253 })),
254 ..Default::default()
255 }),
256 )
257 })
258 .collect();
259 let language_overrides_schema = Schema::Object(SchemaObject {
260 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))),
261 object: Some(Box::new(ObjectValidation {
262 properties: language_overrides_properties,
263 ..Default::default()
264 })),
265 ..Default::default()
266 });
267 root_schema
268 .definitions
269 .insert("LanguageOverrides".to_owned(), language_overrides_schema);
270
271 // Modify theme property to use new theme reference type
272 let settings_file_schema = root_schema.schema.object.as_mut().unwrap();
273 let language_overrides_schema_reference = Schema::Object(SchemaObject {
274 reference: Some("#/definitions/ThemeName".to_owned()),
275 ..Default::default()
276 });
277 settings_file_schema.properties.insert(
278 "theme".to_owned(),
279 Schema::Object(SchemaObject {
280 subschemas: Some(Box::new(SubschemaValidation {
281 all_of: Some(vec![language_overrides_schema_reference]),
282 ..Default::default()
283 })),
284 ..Default::default()
285 }),
286 );
287
288 // Modify language_overrides property to use LanguageOverrides reference
289 settings_file_schema.properties.insert(
290 "language_overrides".to_owned(),
291 Schema::Object(SchemaObject {
292 reference: Some("#/definitions/LanguageOverrides".to_owned()),
293 ..Default::default()
294 }),
295 );
296 serde_json::to_value(root_schema).unwrap()
297}
298
299fn merge<T: Copy>(target: &mut T, value: Option<T>) {
300 if let Some(value) = value {
301 *target = value;
302 }
303}
304
305fn merge_option<T: Copy>(target: &mut Option<T>, value: Option<T>) {
306 if value.is_some() {
307 *target = value;
308 }
309}
310
311pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
312 Ok(serde_json::from_reader(
313 json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()),
314 )?)
315}