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