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