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