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