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