1mod font_size;
2mod keymap_file;
3mod settings_file;
4mod settings_store;
5
6use anyhow::{bail, Result};
7use gpui::{
8 font_cache::{FamilyId, FontCache},
9 fonts, AppContext, AssetSource,
10};
11use schemars::{
12 gen::SchemaGenerator,
13 schema::{InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec},
14 JsonSchema,
15};
16use serde::{Deserialize, Serialize};
17use serde_json::Value;
18use sqlez::{
19 bindable::{Bind, Column, StaticColumnCount},
20 statement::Statement,
21};
22use std::{borrow::Cow, str, sync::Arc};
23use theme::{Theme, ThemeRegistry};
24use util::ResultExt as _;
25
26pub use font_size::{adjust_font_size_delta, font_size_for_setting};
27pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
28pub use settings_file::*;
29pub use settings_store::{Setting, SettingsJsonSchemaParams, SettingsStore};
30
31pub const DEFAULT_SETTINGS_ASSET_PATH: &str = "settings/default.json";
32pub const INITIAL_USER_SETTINGS_ASSET_PATH: &str = "settings/initial_user_settings.json";
33
34#[derive(Clone)]
35pub struct Settings {
36 pub buffer_font_family_name: String,
37 pub buffer_font_features: fonts::Features,
38 pub buffer_font_family: FamilyId,
39 pub buffer_font_size: f32,
40 pub active_pane_magnification: f32,
41 pub confirm_quit: bool,
42 pub show_call_status_icon: bool,
43 pub autosave: Autosave,
44 pub default_dock_anchor: DockAnchor,
45 pub git: GitSettings,
46 pub git_overrides: GitSettings,
47 pub theme: Arc<Theme>,
48 pub base_keymap: BaseKeymap,
49}
50
51impl Setting for Settings {
52 const KEY: Option<&'static str> = None;
53
54 type FileContent = SettingsFileContent;
55
56 fn load(
57 defaults: &Self::FileContent,
58 user_values: &[&Self::FileContent],
59 cx: &AppContext,
60 ) -> Result<Self> {
61 let buffer_font_features = defaults.buffer_font_features.clone().unwrap();
62 let themes = cx.global::<Arc<ThemeRegistry>>();
63
64 let mut this = Self {
65 buffer_font_family: cx
66 .font_cache()
67 .load_family(
68 &[defaults.buffer_font_family.as_ref().unwrap()],
69 &buffer_font_features,
70 )
71 .unwrap(),
72 buffer_font_family_name: defaults.buffer_font_family.clone().unwrap(),
73 buffer_font_features,
74 buffer_font_size: defaults.buffer_font_size.unwrap(),
75 active_pane_magnification: defaults.active_pane_magnification.unwrap(),
76 confirm_quit: defaults.confirm_quit.unwrap(),
77 show_call_status_icon: defaults.show_call_status_icon.unwrap(),
78 autosave: defaults.autosave.unwrap(),
79 default_dock_anchor: defaults.default_dock_anchor.unwrap(),
80 git: defaults.git.unwrap(),
81 git_overrides: Default::default(),
82 theme: themes.get(defaults.theme.as_ref().unwrap()).unwrap(),
83 base_keymap: Default::default(),
84 };
85
86 for value in user_values.into_iter().copied().cloned() {
87 this.set_user_settings(value, themes.as_ref(), cx.font_cache());
88 }
89
90 Ok(this)
91 }
92
93 fn json_schema(
94 generator: &mut SchemaGenerator,
95 params: &SettingsJsonSchemaParams,
96 ) -> schemars::schema::RootSchema {
97 let mut root_schema = generator.root_schema_for::<SettingsFileContent>();
98
99 // Create a schema for a theme name.
100 let theme_name_schema = SchemaObject {
101 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
102 enum_values: Some(
103 params
104 .theme_names
105 .iter()
106 .cloned()
107 .map(Value::String)
108 .collect(),
109 ),
110 ..Default::default()
111 };
112
113 // Create a schema for a 'languages overrides' object, associating editor
114 // settings with specific langauges.
115 assert!(root_schema.definitions.contains_key("EditorSettings"));
116
117 let languages_object_schema = SchemaObject {
118 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))),
119 object: Some(Box::new(ObjectValidation {
120 properties: params
121 .language_names
122 .iter()
123 .map(|name| {
124 (
125 name.clone(),
126 Schema::new_ref("#/definitions/EditorSettings".into()),
127 )
128 })
129 .collect(),
130 ..Default::default()
131 })),
132 ..Default::default()
133 };
134
135 // Add these new schemas as definitions, and modify properties of the root
136 // schema to reference them.
137 root_schema.definitions.extend([
138 ("ThemeName".into(), theme_name_schema.into()),
139 ("Languages".into(), languages_object_schema.into()),
140 ]);
141 let root_schema_object = &mut root_schema.schema.object.as_mut().unwrap();
142
143 root_schema_object.properties.extend([
144 (
145 "theme".to_owned(),
146 Schema::new_ref("#/definitions/ThemeName".into()),
147 ),
148 (
149 "languages".to_owned(),
150 Schema::new_ref("#/definitions/Languages".into()),
151 ),
152 // For backward compatibility
153 (
154 "language_overrides".to_owned(),
155 Schema::new_ref("#/definitions/Languages".into()),
156 ),
157 ]);
158
159 root_schema
160 }
161}
162
163#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
164pub enum BaseKeymap {
165 #[default]
166 VSCode,
167 JetBrains,
168 SublimeText,
169 Atom,
170 TextMate,
171}
172
173impl BaseKeymap {
174 pub const OPTIONS: [(&'static str, Self); 5] = [
175 ("VSCode (Default)", Self::VSCode),
176 ("Atom", Self::Atom),
177 ("JetBrains", Self::JetBrains),
178 ("Sublime Text", Self::SublimeText),
179 ("TextMate", Self::TextMate),
180 ];
181
182 pub fn asset_path(&self) -> Option<&'static str> {
183 match self {
184 BaseKeymap::JetBrains => Some("keymaps/jetbrains.json"),
185 BaseKeymap::SublimeText => Some("keymaps/sublime_text.json"),
186 BaseKeymap::Atom => Some("keymaps/atom.json"),
187 BaseKeymap::TextMate => Some("keymaps/textmate.json"),
188 BaseKeymap::VSCode => None,
189 }
190 }
191
192 pub fn names() -> impl Iterator<Item = &'static str> {
193 Self::OPTIONS.iter().map(|(name, _)| *name)
194 }
195
196 pub fn from_names(option: &str) -> BaseKeymap {
197 Self::OPTIONS
198 .iter()
199 .copied()
200 .find_map(|(name, value)| (name == option).then(|| value))
201 .unwrap_or_default()
202 }
203}
204#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
205pub struct GitSettings {
206 pub git_gutter: Option<GitGutter>,
207 pub gutter_debounce: Option<u64>,
208}
209
210#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
211#[serde(rename_all = "snake_case")]
212pub enum GitGutter {
213 #[default]
214 TrackedFiles,
215 Hide,
216}
217
218#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
219#[serde(rename_all = "snake_case")]
220pub enum Autosave {
221 Off,
222 AfterDelay { milliseconds: u64 },
223 OnFocusChange,
224 OnWindowChange,
225}
226
227#[derive(PartialEq, Eq, Debug, Default, Copy, Clone, Hash, Serialize, Deserialize, JsonSchema)]
228#[serde(rename_all = "snake_case")]
229pub enum DockAnchor {
230 #[default]
231 Bottom,
232 Right,
233 Expanded,
234}
235
236impl StaticColumnCount for DockAnchor {}
237impl Bind for DockAnchor {
238 fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
239 match self {
240 DockAnchor::Bottom => "Bottom",
241 DockAnchor::Right => "Right",
242 DockAnchor::Expanded => "Expanded",
243 }
244 .bind(statement, start_index)
245 }
246}
247
248impl Column for DockAnchor {
249 fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> {
250 String::column(statement, start_index).and_then(|(anchor_text, next_index)| {
251 Ok((
252 match anchor_text.as_ref() {
253 "Bottom" => DockAnchor::Bottom,
254 "Right" => DockAnchor::Right,
255 "Expanded" => DockAnchor::Expanded,
256 _ => bail!("Stored dock anchor is incorrect"),
257 },
258 next_index,
259 ))
260 })
261 }
262}
263
264#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
265pub struct SettingsFileContent {
266 #[serde(default)]
267 pub buffer_font_family: Option<String>,
268 #[serde(default)]
269 pub buffer_font_size: Option<f32>,
270 #[serde(default)]
271 pub buffer_font_features: Option<fonts::Features>,
272 #[serde(default)]
273 pub active_pane_magnification: Option<f32>,
274 #[serde(default)]
275 pub cursor_blink: Option<bool>,
276 #[serde(default)]
277 pub confirm_quit: Option<bool>,
278 #[serde(default)]
279 pub hover_popover_enabled: Option<bool>,
280 #[serde(default)]
281 pub show_completions_on_input: Option<bool>,
282 #[serde(default)]
283 pub show_call_status_icon: Option<bool>,
284 #[serde(default)]
285 pub autosave: Option<Autosave>,
286 #[serde(default)]
287 pub default_dock_anchor: Option<DockAnchor>,
288 #[serde(default)]
289 pub git: Option<GitSettings>,
290 #[serde(default)]
291 pub theme: Option<String>,
292 #[serde(default)]
293 pub base_keymap: Option<BaseKeymap>,
294}
295
296impl Settings {
297 pub fn initial_user_settings_content(assets: &'static impl AssetSource) -> Cow<'static, str> {
298 match assets.load(INITIAL_USER_SETTINGS_ASSET_PATH).unwrap() {
299 Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
300 Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
301 }
302 }
303
304 /// Fill out the settings corresponding to the default.json file, overrides will be set later
305 pub fn defaults(
306 assets: impl AssetSource,
307 font_cache: &FontCache,
308 themes: &ThemeRegistry,
309 ) -> Self {
310 let defaults: SettingsFileContent = settings_store::parse_json_with_comments(
311 str::from_utf8(assets.load(DEFAULT_SETTINGS_ASSET_PATH).unwrap().as_ref()).unwrap(),
312 )
313 .unwrap();
314
315 let buffer_font_features = defaults.buffer_font_features.unwrap();
316 Self {
317 buffer_font_family: font_cache
318 .load_family(
319 &[defaults.buffer_font_family.as_ref().unwrap()],
320 &buffer_font_features,
321 )
322 .unwrap(),
323 buffer_font_family_name: defaults.buffer_font_family.unwrap(),
324 buffer_font_features,
325 buffer_font_size: defaults.buffer_font_size.unwrap(),
326 active_pane_magnification: defaults.active_pane_magnification.unwrap(),
327 confirm_quit: defaults.confirm_quit.unwrap(),
328 show_call_status_icon: defaults.show_call_status_icon.unwrap(),
329 autosave: defaults.autosave.unwrap(),
330 default_dock_anchor: defaults.default_dock_anchor.unwrap(),
331 git: defaults.git.unwrap(),
332 git_overrides: Default::default(),
333 theme: themes.get(&defaults.theme.unwrap()).unwrap(),
334 base_keymap: Default::default(),
335 }
336 }
337
338 // Fill out the overrride and etc. settings from the user's settings.json
339 fn set_user_settings(
340 &mut self,
341 data: SettingsFileContent,
342 theme_registry: &ThemeRegistry,
343 font_cache: &FontCache,
344 ) {
345 let mut family_changed = false;
346 if let Some(value) = data.buffer_font_family {
347 self.buffer_font_family_name = value;
348 family_changed = true;
349 }
350 if let Some(value) = data.buffer_font_features {
351 self.buffer_font_features = value;
352 family_changed = true;
353 }
354 if family_changed {
355 if let Some(id) = font_cache
356 .load_family(&[&self.buffer_font_family_name], &self.buffer_font_features)
357 .log_err()
358 {
359 self.buffer_font_family = id;
360 }
361 }
362
363 if let Some(value) = &data.theme {
364 if let Some(theme) = theme_registry.get(value).log_err() {
365 self.theme = theme;
366 }
367 }
368
369 merge(&mut self.buffer_font_size, data.buffer_font_size);
370 merge(
371 &mut self.active_pane_magnification,
372 data.active_pane_magnification,
373 );
374 merge(&mut self.confirm_quit, data.confirm_quit);
375 merge(&mut self.autosave, data.autosave);
376 merge(&mut self.default_dock_anchor, data.default_dock_anchor);
377 merge(&mut self.base_keymap, data.base_keymap);
378
379 self.git_overrides = data.git.unwrap_or_default();
380 }
381
382 pub fn git_gutter(&self) -> GitGutter {
383 self.git_overrides.git_gutter.unwrap_or_else(|| {
384 self.git
385 .git_gutter
386 .expect("git_gutter should be some by setting setup")
387 })
388 }
389
390 #[cfg(any(test, feature = "test-support"))]
391 pub fn test(cx: &gpui::AppContext) -> Settings {
392 Settings {
393 buffer_font_family_name: "Monaco".to_string(),
394 buffer_font_features: Default::default(),
395 buffer_font_family: cx
396 .font_cache()
397 .load_family(&["Monaco"], &Default::default())
398 .unwrap(),
399 buffer_font_size: 14.,
400 active_pane_magnification: 1.,
401 confirm_quit: false,
402 show_call_status_icon: true,
403 autosave: Autosave::Off,
404 default_dock_anchor: DockAnchor::Bottom,
405 git: Default::default(),
406 git_overrides: Default::default(),
407 theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), Default::default),
408 base_keymap: Default::default(),
409 }
410 }
411
412 #[cfg(any(test, feature = "test-support"))]
413 pub fn test_async(cx: &mut gpui::TestAppContext) {
414 cx.update(|cx| {
415 let settings = Self::test(cx);
416 cx.set_global(settings);
417 });
418 }
419}
420
421fn merge<T: Copy>(target: &mut T, value: Option<T>) {
422 if let Some(value) = value {
423 *target = value;
424 }
425}