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