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