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