1use anyhow::{Context as _, Result};
2use collections::{BTreeMap, HashMap, btree_map, hash_map};
3use ec4rs::{ConfigParser, PropertiesSource, Section};
4use fs::Fs;
5use futures::{
6 FutureExt, StreamExt,
7 channel::{mpsc, oneshot},
8 future::LocalBoxFuture,
9};
10use gpui::{App, AsyncApp, BorrowAppContext, Global, SharedString, Task, UpdateGlobal};
11
12use paths::{EDITORCONFIG_NAME, local_settings_file_relative_path, task_file_name};
13use schemars::{JsonSchema, json_schema};
14use serde_json::Value;
15use smallvec::SmallVec;
16use std::{
17 any::{Any, TypeId, type_name},
18 fmt::Debug,
19 ops::Range,
20 path::{Path, PathBuf},
21 rc::Rc,
22 str::{self, FromStr},
23 sync::Arc,
24};
25use util::{
26 ResultExt as _,
27 schemars::{DefaultDenyUnknownFields, replace_subschema},
28};
29
30pub type EditorconfigProperties = ec4rs::Properties;
31
32use crate::{
33 ActiveSettingsProfileName, FontFamilyName, IconThemeName, LanguageSettingsContent,
34 LanguageToSettingsMap, SettingsJsonSchemaParams, ThemeName, VsCodeSettings, WorktreeId,
35 merge_from::MergeFrom,
36 parse_json_with_comments, replace_value_in_json_text,
37 settings_content::{
38 ExtensionsSettingsContent, ProjectSettingsContent, SettingsContent, UserSettingsContent,
39 },
40 update_value_in_json_text,
41};
42
43pub trait SettingsKey: 'static + Send + Sync {
44 /// The name of a key within the JSON file from which this setting should
45 /// be deserialized. If this is `None`, then the setting will be deserialized
46 /// from the root object.
47 const KEY: Option<&'static str>;
48
49 const FALLBACK_KEY: Option<&'static str> = None;
50}
51
52/// A value that can be defined as a user setting.
53///
54/// Settings can be loaded from a combination of multiple JSON files.
55pub trait Settings: 'static + Send + Sync + Sized {
56 /// The name of the keys in the [`FileContent`](Self::FileContent) that should
57 /// always be written to a settings file, even if their value matches the default
58 /// value.
59 ///
60 /// This is useful for tagged [`FileContent`](Self::FileContent)s where the tag
61 /// is a "version" field that should always be persisted, even if the current
62 /// user settings match the current version of the settings.
63 const PRESERVED_KEYS: Option<&'static [&'static str]> = None;
64
65 /// Read the value from default.json.
66 ///
67 /// This function *should* panic if default values are missing,
68 /// and you should add a default to default.json for documentation.
69 fn from_settings(content: &SettingsContent, cx: &mut App) -> Self;
70
71 fn missing_default() -> anyhow::Error {
72 anyhow::anyhow!("missing default for: {}", std::any::type_name::<Self>())
73 }
74
75 /// Use [the helpers in the vscode_import module](crate::vscode_import) to apply known
76 /// equivalent settings from a vscode config to our config
77 fn import_from_vscode(_vscode: &VsCodeSettings, _current: &mut SettingsContent) {}
78
79 #[track_caller]
80 fn register(cx: &mut App)
81 where
82 Self: Sized,
83 {
84 SettingsStore::update_global(cx, |store, cx| {
85 store.register_setting::<Self>(cx);
86 });
87 }
88
89 #[track_caller]
90 fn get<'a>(path: Option<SettingsLocation>, cx: &'a App) -> &'a Self
91 where
92 Self: Sized,
93 {
94 cx.global::<SettingsStore>().get(path)
95 }
96
97 #[track_caller]
98 fn get_global(cx: &App) -> &Self
99 where
100 Self: Sized,
101 {
102 cx.global::<SettingsStore>().get(None)
103 }
104
105 #[track_caller]
106 fn try_get(cx: &App) -> Option<&Self>
107 where
108 Self: Sized,
109 {
110 if cx.has_global::<SettingsStore>() {
111 cx.global::<SettingsStore>().try_get(None)
112 } else {
113 None
114 }
115 }
116
117 #[track_caller]
118 fn try_read_global<R>(cx: &AsyncApp, f: impl FnOnce(&Self) -> R) -> Option<R>
119 where
120 Self: Sized,
121 {
122 cx.try_read_global(|s: &SettingsStore, _| f(s.get(None)))
123 }
124
125 #[track_caller]
126 fn override_global(settings: Self, cx: &mut App)
127 where
128 Self: Sized,
129 {
130 cx.global_mut::<SettingsStore>().override_global(settings)
131 }
132}
133
134#[derive(Clone, Copy, Debug)]
135pub struct SettingsLocation<'a> {
136 pub worktree_id: WorktreeId,
137 pub path: &'a Path,
138}
139
140pub struct SettingsStore {
141 setting_values: HashMap<TypeId, Box<dyn AnySettingValue>>,
142 default_settings: Rc<SettingsContent>,
143 user_settings: Option<UserSettingsContent>,
144 global_settings: Option<Box<SettingsContent>>,
145
146 extension_settings: Option<Box<SettingsContent>>,
147 server_settings: Option<Box<SettingsContent>>,
148
149 merged_settings: Rc<SettingsContent>,
150
151 local_settings: BTreeMap<(WorktreeId, Arc<Path>), SettingsContent>,
152 raw_editorconfig_settings: BTreeMap<(WorktreeId, Arc<Path>), (String, Option<Editorconfig>)>,
153
154 _setting_file_updates: Task<()>,
155 setting_file_updates_tx:
156 mpsc::UnboundedSender<Box<dyn FnOnce(AsyncApp) -> LocalBoxFuture<'static, Result<()>>>>,
157}
158
159#[derive(Clone)]
160pub struct Editorconfig {
161 pub is_root: bool,
162 pub sections: SmallVec<[Section; 5]>,
163}
164
165impl FromStr for Editorconfig {
166 type Err = anyhow::Error;
167
168 fn from_str(contents: &str) -> Result<Self, Self::Err> {
169 let parser = ConfigParser::new_buffered(contents.as_bytes())
170 .context("creating editorconfig parser")?;
171 let is_root = parser.is_root;
172 let sections = parser
173 .collect::<Result<SmallVec<_>, _>>()
174 .context("parsing editorconfig sections")?;
175 Ok(Self { is_root, sections })
176 }
177}
178
179#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
180pub enum LocalSettingsKind {
181 Settings,
182 Tasks,
183 Editorconfig,
184 Debug,
185}
186
187impl Global for SettingsStore {}
188
189#[derive(Debug)]
190struct SettingValue<T> {
191 global_value: Option<T>,
192 local_values: Vec<(WorktreeId, Arc<Path>, T)>,
193}
194
195trait AnySettingValue: 'static + Send + Sync {
196 fn setting_type_name(&self) -> &'static str;
197
198 fn from_settings(&self, s: &SettingsContent, cx: &mut App) -> Box<dyn Any>;
199
200 fn value_for_path(&self, path: Option<SettingsLocation>) -> &dyn Any;
201 fn all_local_values(&self) -> Vec<(WorktreeId, Arc<Path>, &dyn Any)>;
202 fn set_global_value(&mut self, value: Box<dyn Any>);
203 fn set_local_value(&mut self, root_id: WorktreeId, path: Arc<Path>, value: Box<dyn Any>);
204 fn import_from_vscode(
205 &self,
206 vscode_settings: &VsCodeSettings,
207 settings_content: &mut SettingsContent,
208 );
209}
210
211impl SettingsStore {
212 pub fn new(cx: &App, default_settings: &str) -> Self {
213 let (setting_file_updates_tx, mut setting_file_updates_rx) = mpsc::unbounded();
214 let default_settings: Rc<SettingsContent> =
215 parse_json_with_comments(default_settings).unwrap();
216 Self {
217 setting_values: Default::default(),
218 default_settings: default_settings.clone(),
219 global_settings: None,
220 server_settings: None,
221 user_settings: None,
222 extension_settings: None,
223
224 merged_settings: default_settings,
225 local_settings: BTreeMap::default(),
226 raw_editorconfig_settings: BTreeMap::default(),
227 setting_file_updates_tx,
228 _setting_file_updates: cx.spawn(async move |cx| {
229 while let Some(setting_file_update) = setting_file_updates_rx.next().await {
230 (setting_file_update)(cx.clone()).await.log_err();
231 }
232 }),
233 }
234 }
235
236 pub fn observe_active_settings_profile_name(cx: &mut App) -> gpui::Subscription {
237 cx.observe_global::<ActiveSettingsProfileName>(|cx| {
238 Self::update_global(cx, |store, cx| {
239 store.recompute_values(None, cx).log_err();
240 });
241 })
242 }
243
244 pub fn update<C, R>(cx: &mut C, f: impl FnOnce(&mut Self, &mut C) -> R) -> R
245 where
246 C: BorrowAppContext,
247 {
248 cx.update_global(f)
249 }
250
251 /// Add a new type of setting to the store.
252 pub fn register_setting<T: Settings>(&mut self, cx: &mut App) {
253 let setting_type_id = TypeId::of::<T>();
254 let entry = self.setting_values.entry(setting_type_id);
255
256 if matches!(entry, hash_map::Entry::Occupied(_)) {
257 return;
258 }
259
260 let setting_value = entry.or_insert(Box::new(SettingValue::<T> {
261 global_value: None,
262 local_values: Vec::new(),
263 }));
264 let value = T::from_settings(&self.merged_settings, cx);
265 setting_value.set_global_value(Box::new(value));
266 }
267
268 /// Get the value of a setting.
269 ///
270 /// Panics if the given setting type has not been registered, or if there is no
271 /// value for this setting.
272 pub fn get<T: Settings>(&self, path: Option<SettingsLocation>) -> &T {
273 self.setting_values
274 .get(&TypeId::of::<T>())
275 .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
276 .value_for_path(path)
277 .downcast_ref::<T>()
278 .expect("no default value for setting type")
279 }
280
281 /// Get the value of a setting.
282 ///
283 /// Does not panic
284 pub fn try_get<T: Settings>(&self, path: Option<SettingsLocation>) -> Option<&T> {
285 self.setting_values
286 .get(&TypeId::of::<T>())
287 .map(|value| value.value_for_path(path))
288 .and_then(|value| value.downcast_ref::<T>())
289 }
290
291 /// Get all values from project specific settings
292 pub fn get_all_locals<T: Settings>(&self) -> Vec<(WorktreeId, Arc<Path>, &T)> {
293 self.setting_values
294 .get(&TypeId::of::<T>())
295 .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
296 .all_local_values()
297 .into_iter()
298 .map(|(id, path, any)| {
299 (
300 id,
301 path,
302 any.downcast_ref::<T>()
303 .expect("wrong value type for setting"),
304 )
305 })
306 .collect()
307 }
308
309 /// Override the global value for a setting.
310 ///
311 /// The given value will be overwritten if the user settings file changes.
312 pub fn override_global<T: Settings>(&mut self, value: T) {
313 self.setting_values
314 .get_mut(&TypeId::of::<T>())
315 .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
316 .set_global_value(Box::new(value))
317 }
318
319 /// Get the user's settings content.
320 ///
321 /// For user-facing functionality use the typed setting interface.
322 /// (e.g. ProjectSettings::get_global(cx))
323 pub fn raw_user_settings(&self) -> Option<&UserSettingsContent> {
324 self.user_settings.as_ref()
325 }
326
327 /// Get the default settings content as a raw JSON value.
328 pub fn raw_default_settings(&self) -> &SettingsContent {
329 &self.default_settings
330 }
331
332 /// Get the configured settings profile names.
333 pub fn configured_settings_profiles(&self) -> impl Iterator<Item = &str> {
334 self.user_settings
335 .iter()
336 .flat_map(|settings| settings.profiles.keys().map(|k| k.as_str()))
337 }
338
339 #[cfg(any(test, feature = "test-support"))]
340 pub fn test(cx: &mut App) -> Self {
341 Self::new(cx, &crate::test_settings())
342 }
343
344 /// Updates the value of a setting in the user's global configuration.
345 ///
346 /// This is only for tests. Normally, settings are only loaded from
347 /// JSON files.
348 #[cfg(any(test, feature = "test-support"))]
349 pub fn update_user_settings(
350 &mut self,
351 cx: &mut App,
352 update: impl FnOnce(&mut SettingsContent),
353 ) {
354 let mut content = self.user_settings.clone().unwrap_or_default().content;
355 update(&mut content);
356 let new_text = serde_json::to_string(&UserSettingsContent {
357 content,
358 ..Default::default()
359 })
360 .unwrap();
361 self.set_user_settings(&new_text, cx).unwrap();
362 }
363
364 pub async fn load_settings(fs: &Arc<dyn Fs>) -> Result<String> {
365 match fs.load(paths::settings_file()).await {
366 result @ Ok(_) => result,
367 Err(err) => {
368 if let Some(e) = err.downcast_ref::<std::io::Error>()
369 && e.kind() == std::io::ErrorKind::NotFound
370 {
371 return Ok(crate::initial_user_settings_content().to_string());
372 }
373 Err(err)
374 }
375 }
376 }
377
378 fn update_settings_file_inner(
379 &self,
380 fs: Arc<dyn Fs>,
381 update: impl 'static + Send + FnOnce(String, AsyncApp) -> Result<String>,
382 ) -> oneshot::Receiver<Result<()>> {
383 let (tx, rx) = oneshot::channel::<Result<()>>();
384 self.setting_file_updates_tx
385 .unbounded_send(Box::new(move |cx: AsyncApp| {
386 async move {
387 let res = async move {
388 let old_text = Self::load_settings(&fs).await?;
389 let new_text = update(old_text, cx)?;
390 let settings_path = paths::settings_file().as_path();
391 if fs.is_file(settings_path).await {
392 let resolved_path =
393 fs.canonicalize(settings_path).await.with_context(|| {
394 format!(
395 "Failed to canonicalize settings path {:?}",
396 settings_path
397 )
398 })?;
399
400 fs.atomic_write(resolved_path.clone(), new_text)
401 .await
402 .with_context(|| {
403 format!("Failed to write settings to file {:?}", resolved_path)
404 })?;
405 } else {
406 fs.atomic_write(settings_path.to_path_buf(), new_text)
407 .await
408 .with_context(|| {
409 format!("Failed to write settings to file {:?}", settings_path)
410 })?;
411 }
412 anyhow::Ok(())
413 }
414 .await;
415
416 let new_res = match &res {
417 Ok(_) => anyhow::Ok(()),
418 Err(e) => Err(anyhow::anyhow!("Failed to write settings to file {:?}", e)),
419 };
420
421 _ = tx.send(new_res);
422 res
423 }
424 .boxed_local()
425 }))
426 .map_err(|err| anyhow::format_err!("Failed to update settings file: {}", err))
427 .log_with_level(log::Level::Warn);
428 return rx;
429 }
430
431 pub fn update_settings_file_at_path(
432 &self,
433 fs: Arc<dyn Fs>,
434 path: &[impl AsRef<str>],
435 new_value: serde_json::Value,
436 ) -> oneshot::Receiver<Result<()>> {
437 let key_path = path
438 .into_iter()
439 .map(AsRef::as_ref)
440 .map(SharedString::new)
441 .collect::<Vec<_>>();
442 let update = move |mut old_text: String, cx: AsyncApp| {
443 cx.read_global(|store: &SettingsStore, _cx| {
444 // todo(settings_ui) use `update_value_in_json_text` for merging new and old objects with comment preservation, needs old value though...
445 let (range, replacement) = replace_value_in_json_text(
446 &old_text,
447 key_path.as_slice(),
448 store.json_tab_size(),
449 Some(&new_value),
450 None,
451 );
452 old_text.replace_range(range, &replacement);
453 old_text
454 })
455 };
456 self.update_settings_file_inner(fs, update)
457 }
458
459 pub fn update_settings_file(
460 &self,
461 fs: Arc<dyn Fs>,
462 update: impl 'static + Send + FnOnce(&mut SettingsContent, &App),
463 ) {
464 _ = self.update_settings_file_inner(fs, move |old_text: String, cx: AsyncApp| {
465 cx.read_global(|store: &SettingsStore, cx| {
466 store.new_text_for_update(old_text, |content| update(content, cx))
467 })
468 });
469 }
470
471 pub fn import_vscode_settings(
472 &self,
473 fs: Arc<dyn Fs>,
474 vscode_settings: VsCodeSettings,
475 ) -> oneshot::Receiver<Result<()>> {
476 self.update_settings_file_inner(fs, move |old_text: String, cx: AsyncApp| {
477 cx.read_global(|store: &SettingsStore, _cx| {
478 store.get_vscode_edits(old_text, &vscode_settings)
479 })
480 })
481 }
482}
483
484impl SettingsStore {
485 /// Updates the value of a setting in a JSON file, returning the new text
486 /// for that JSON file.
487 pub fn new_text_for_update(
488 &self,
489 old_text: String,
490 update: impl FnOnce(&mut SettingsContent),
491 ) -> String {
492 let edits = self.edits_for_update(&old_text, update);
493 let mut new_text = old_text;
494 for (range, replacement) in edits.into_iter() {
495 new_text.replace_range(range, &replacement);
496 }
497 new_text
498 }
499
500 pub fn get_vscode_edits(&self, old_text: String, vscode: &VsCodeSettings) -> String {
501 self.new_text_for_update(old_text, |settings_content| {
502 for v in self.setting_values.values() {
503 v.import_from_vscode(vscode, settings_content)
504 }
505 })
506 }
507
508 /// Updates the value of a setting in a JSON file, returning a list
509 /// of edits to apply to the JSON file.
510 pub fn edits_for_update(
511 &self,
512 text: &str,
513 update: impl FnOnce(&mut SettingsContent),
514 ) -> Vec<(Range<usize>, String)> {
515 let old_content: UserSettingsContent =
516 parse_json_with_comments(text).log_err().unwrap_or_default();
517 let mut new_content = old_content.clone();
518 update(&mut new_content.content);
519
520 let old_value = serde_json::to_value(&old_content).unwrap();
521 let new_value = serde_json::to_value(new_content).unwrap();
522
523 let mut key_path = Vec::new();
524 let mut edits = Vec::new();
525 let tab_size = self.json_tab_size();
526 let mut text = text.to_string();
527 update_value_in_json_text(
528 &mut text,
529 &mut key_path,
530 tab_size,
531 &old_value,
532 &new_value,
533 &mut edits,
534 );
535 edits
536 }
537
538 pub fn json_tab_size(&self) -> usize {
539 2
540 }
541
542 /// Sets the default settings via a JSON string.
543 ///
544 /// The string should contain a JSON object with a default value for every setting.
545 pub fn set_default_settings(
546 &mut self,
547 default_settings_content: &str,
548 cx: &mut App,
549 ) -> Result<()> {
550 self.default_settings = parse_json_with_comments(default_settings_content)?;
551 self.recompute_values(None, cx)?;
552 Ok(())
553 }
554
555 /// Sets the user settings via a JSON string.
556 pub fn set_user_settings(&mut self, user_settings_content: &str, cx: &mut App) -> Result<()> {
557 let settings: UserSettingsContent = if user_settings_content.is_empty() {
558 parse_json_with_comments("{}")?
559 } else {
560 parse_json_with_comments(user_settings_content)?
561 };
562
563 self.user_settings = Some(settings);
564 self.recompute_values(None, cx)?;
565 Ok(())
566 }
567
568 /// Sets the global settings via a JSON string.
569 pub fn set_global_settings(
570 &mut self,
571 global_settings_content: &str,
572 cx: &mut App,
573 ) -> Result<()> {
574 let settings: SettingsContent = if global_settings_content.is_empty() {
575 parse_json_with_comments("{}")?
576 } else {
577 parse_json_with_comments(global_settings_content)?
578 };
579
580 self.global_settings = Some(Box::new(settings));
581 self.recompute_values(None, cx)?;
582 Ok(())
583 }
584
585 pub fn set_server_settings(
586 &mut self,
587 server_settings_content: &str,
588 cx: &mut App,
589 ) -> Result<()> {
590 let settings: Option<SettingsContent> = if server_settings_content.is_empty() {
591 None
592 } else {
593 parse_json_with_comments(server_settings_content)?
594 };
595
596 // Rewrite the server settings into a content type
597 self.server_settings = settings.map(|settings| Box::new(settings));
598
599 self.recompute_values(None, cx)?;
600 Ok(())
601 }
602
603 /// Add or remove a set of local settings via a JSON string.
604 pub fn set_local_settings(
605 &mut self,
606 root_id: WorktreeId,
607 directory_path: Arc<Path>,
608 kind: LocalSettingsKind,
609 settings_content: Option<&str>,
610 cx: &mut App,
611 ) -> std::result::Result<(), InvalidSettingsError> {
612 let mut zed_settings_changed = false;
613 match (
614 kind,
615 settings_content
616 .map(|content| content.trim())
617 .filter(|content| !content.is_empty()),
618 ) {
619 (LocalSettingsKind::Tasks, _) => {
620 return Err(InvalidSettingsError::Tasks {
621 message: "Attempted to submit tasks into the settings store".to_string(),
622 path: directory_path.join(task_file_name()),
623 });
624 }
625 (LocalSettingsKind::Debug, _) => {
626 return Err(InvalidSettingsError::Debug {
627 message: "Attempted to submit debugger config into the settings store"
628 .to_string(),
629 path: directory_path.join(task_file_name()),
630 });
631 }
632 (LocalSettingsKind::Settings, None) => {
633 zed_settings_changed = self
634 .local_settings
635 .remove(&(root_id, directory_path.clone()))
636 .is_some()
637 }
638 (LocalSettingsKind::Editorconfig, None) => {
639 self.raw_editorconfig_settings
640 .remove(&(root_id, directory_path.clone()));
641 }
642 (LocalSettingsKind::Settings, Some(settings_contents)) => {
643 let new_settings = parse_json_with_comments::<ProjectSettingsContent>(
644 settings_contents,
645 )
646 .map_err(|e| InvalidSettingsError::LocalSettings {
647 path: directory_path.join(local_settings_file_relative_path()),
648 message: e.to_string(),
649 })?;
650 match self.local_settings.entry((root_id, directory_path.clone())) {
651 btree_map::Entry::Vacant(v) => {
652 v.insert(SettingsContent {
653 project: new_settings,
654 ..Default::default()
655 });
656 zed_settings_changed = true;
657 }
658 btree_map::Entry::Occupied(mut o) => {
659 if &o.get().project != &new_settings {
660 o.insert(SettingsContent {
661 project: new_settings,
662 ..Default::default()
663 });
664 zed_settings_changed = true;
665 }
666 }
667 }
668 }
669 (LocalSettingsKind::Editorconfig, Some(editorconfig_contents)) => {
670 match self
671 .raw_editorconfig_settings
672 .entry((root_id, directory_path.clone()))
673 {
674 btree_map::Entry::Vacant(v) => match editorconfig_contents.parse() {
675 Ok(new_contents) => {
676 v.insert((editorconfig_contents.to_owned(), Some(new_contents)));
677 }
678 Err(e) => {
679 v.insert((editorconfig_contents.to_owned(), None));
680 return Err(InvalidSettingsError::Editorconfig {
681 message: e.to_string(),
682 path: directory_path.join(EDITORCONFIG_NAME),
683 });
684 }
685 },
686 btree_map::Entry::Occupied(mut o) => {
687 if o.get().0 != editorconfig_contents {
688 match editorconfig_contents.parse() {
689 Ok(new_contents) => {
690 o.insert((
691 editorconfig_contents.to_owned(),
692 Some(new_contents),
693 ));
694 }
695 Err(e) => {
696 o.insert((editorconfig_contents.to_owned(), None));
697 return Err(InvalidSettingsError::Editorconfig {
698 message: e.to_string(),
699 path: directory_path.join(EDITORCONFIG_NAME),
700 });
701 }
702 }
703 }
704 }
705 }
706 }
707 };
708
709 if zed_settings_changed {
710 self.recompute_values(Some((root_id, &directory_path)), cx)?;
711 }
712 Ok(())
713 }
714
715 pub fn set_extension_settings(
716 &mut self,
717 content: ExtensionsSettingsContent,
718 cx: &mut App,
719 ) -> Result<()> {
720 self.extension_settings = Some(Box::new(SettingsContent {
721 project: ProjectSettingsContent {
722 all_languages: content.all_languages,
723 ..Default::default()
724 },
725 ..Default::default()
726 }));
727 self.recompute_values(None, cx)?;
728 Ok(())
729 }
730
731 /// Add or remove a set of local settings via a JSON string.
732 pub fn clear_local_settings(&mut self, root_id: WorktreeId, cx: &mut App) -> Result<()> {
733 self.local_settings
734 .retain(|(worktree_id, _), _| worktree_id != &root_id);
735 self.recompute_values(Some((root_id, "".as_ref())), cx)?;
736 Ok(())
737 }
738
739 pub fn local_settings(
740 &self,
741 root_id: WorktreeId,
742 ) -> impl '_ + Iterator<Item = (Arc<Path>, &ProjectSettingsContent)> {
743 self.local_settings
744 .range(
745 (root_id, Path::new("").into())
746 ..(
747 WorktreeId::from_usize(root_id.to_usize() + 1),
748 Path::new("").into(),
749 ),
750 )
751 .map(|((_, path), content)| (path.clone(), &content.project))
752 }
753
754 pub fn local_editorconfig_settings(
755 &self,
756 root_id: WorktreeId,
757 ) -> impl '_ + Iterator<Item = (Arc<Path>, String, Option<Editorconfig>)> {
758 self.raw_editorconfig_settings
759 .range(
760 (root_id, Path::new("").into())
761 ..(
762 WorktreeId::from_usize(root_id.to_usize() + 1),
763 Path::new("").into(),
764 ),
765 )
766 .map(|((_, path), (content, parsed_content))| {
767 (path.clone(), content.clone(), parsed_content.clone())
768 })
769 }
770
771 pub fn json_schema(&self, params: &SettingsJsonSchemaParams) -> Value {
772 let mut generator = schemars::generate::SchemaSettings::draft2019_09()
773 .with_transform(DefaultDenyUnknownFields)
774 .into_generator();
775
776 UserSettingsContent::json_schema(&mut generator);
777
778 let language_settings_content_ref = generator
779 .subschema_for::<LanguageSettingsContent>()
780 .to_value();
781 replace_subschema::<LanguageToSettingsMap>(&mut generator, || {
782 json_schema!({
783 "type": "object",
784 "properties": params
785 .language_names
786 .iter()
787 .map(|name| {
788 (
789 name.clone(),
790 language_settings_content_ref.clone(),
791 )
792 })
793 .collect::<serde_json::Map<_, _>>()
794 })
795 });
796
797 replace_subschema::<FontFamilyName>(&mut generator, || {
798 json_schema!({
799 "type": "string",
800 "enum": params.font_names,
801 })
802 });
803
804 replace_subschema::<ThemeName>(&mut generator, || {
805 json_schema!({
806 "type": "string",
807 "enum": params.theme_names,
808 })
809 });
810
811 replace_subschema::<IconThemeName>(&mut generator, || {
812 json_schema!({
813 "type": "string",
814 "enum": params.icon_theme_names,
815 })
816 });
817
818 generator
819 .root_schema_for::<UserSettingsContent>()
820 .to_value()
821 }
822
823 fn recompute_values(
824 &mut self,
825 changed_local_path: Option<(WorktreeId, &Path)>,
826 cx: &mut App,
827 ) -> std::result::Result<(), InvalidSettingsError> {
828 // Reload the global and local values for every setting.
829 let mut project_settings_stack = Vec::<SettingsContent>::new();
830 let mut paths_stack = Vec::<Option<(WorktreeId, &Path)>>::new();
831
832 if changed_local_path.is_none() {
833 let mut merged = self.default_settings.as_ref().clone();
834 merged.merge_from_option(self.extension_settings.as_deref());
835 merged.merge_from_option(self.global_settings.as_deref());
836 if let Some(user_settings) = self.user_settings.as_ref() {
837 merged.merge_from(&user_settings.content);
838 merged.merge_from_option(user_settings.for_release_channel());
839 merged.merge_from_option(user_settings.for_os());
840 merged.merge_from_option(user_settings.for_profile(cx));
841 }
842 merged.merge_from_option(self.server_settings.as_deref());
843 self.merged_settings = Rc::new(merged);
844
845 for setting_value in self.setting_values.values_mut() {
846 let value = setting_value.from_settings(&self.merged_settings, cx);
847 setting_value.set_global_value(value);
848 }
849 }
850
851 for ((root_id, directory_path), local_settings) in &self.local_settings {
852 // Build a stack of all of the local values for that setting.
853 while let Some(prev_entry) = paths_stack.last() {
854 if let Some((prev_root_id, prev_path)) = prev_entry
855 && (root_id != prev_root_id || !directory_path.starts_with(prev_path))
856 {
857 paths_stack.pop();
858 project_settings_stack.pop();
859 continue;
860 }
861 break;
862 }
863
864 paths_stack.push(Some((*root_id, directory_path.as_ref())));
865 let mut merged_local_settings = if let Some(deepest) = project_settings_stack.last() {
866 (*deepest).clone()
867 } else {
868 self.merged_settings.as_ref().clone()
869 };
870 merged_local_settings.merge_from(local_settings);
871
872 project_settings_stack.push(merged_local_settings);
873
874 // If a local settings file changed, then avoid recomputing local
875 // settings for any path outside of that directory.
876 if changed_local_path.is_some_and(|(changed_root_id, changed_local_path)| {
877 *root_id != changed_root_id || !directory_path.starts_with(changed_local_path)
878 }) {
879 continue;
880 }
881
882 for setting_value in self.setting_values.values_mut() {
883 let value =
884 setting_value.from_settings(&project_settings_stack.last().unwrap(), cx);
885 setting_value.set_local_value(*root_id, directory_path.clone(), value);
886 }
887 }
888 Ok(())
889 }
890
891 pub fn editorconfig_properties(
892 &self,
893 for_worktree: WorktreeId,
894 for_path: &Path,
895 ) -> Option<EditorconfigProperties> {
896 let mut properties = EditorconfigProperties::new();
897
898 for (directory_with_config, _, parsed_editorconfig) in
899 self.local_editorconfig_settings(for_worktree)
900 {
901 if !for_path.starts_with(&directory_with_config) {
902 properties.use_fallbacks();
903 return Some(properties);
904 }
905 let parsed_editorconfig = parsed_editorconfig?;
906 if parsed_editorconfig.is_root {
907 properties = EditorconfigProperties::new();
908 }
909 for section in parsed_editorconfig.sections {
910 section.apply_to(&mut properties, for_path).log_err()?;
911 }
912 }
913
914 properties.use_fallbacks();
915 Some(properties)
916 }
917}
918
919#[derive(Debug, Clone, PartialEq)]
920pub enum InvalidSettingsError {
921 LocalSettings { path: PathBuf, message: String },
922 UserSettings { message: String },
923 ServerSettings { message: String },
924 DefaultSettings { message: String },
925 Editorconfig { path: PathBuf, message: String },
926 Tasks { path: PathBuf, message: String },
927 Debug { path: PathBuf, message: String },
928}
929
930impl std::fmt::Display for InvalidSettingsError {
931 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
932 match self {
933 InvalidSettingsError::LocalSettings { message, .. }
934 | InvalidSettingsError::UserSettings { message }
935 | InvalidSettingsError::ServerSettings { message }
936 | InvalidSettingsError::DefaultSettings { message }
937 | InvalidSettingsError::Tasks { message, .. }
938 | InvalidSettingsError::Editorconfig { message, .. }
939 | InvalidSettingsError::Debug { message, .. } => {
940 write!(f, "{message}")
941 }
942 }
943 }
944}
945impl std::error::Error for InvalidSettingsError {}
946
947impl Debug for SettingsStore {
948 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
949 f.debug_struct("SettingsStore")
950 .field(
951 "types",
952 &self
953 .setting_values
954 .values()
955 .map(|value| value.setting_type_name())
956 .collect::<Vec<_>>(),
957 )
958 .field("default_settings", &self.default_settings)
959 .field("user_settings", &self.user_settings)
960 .field("local_settings", &self.local_settings)
961 .finish_non_exhaustive()
962 }
963}
964
965impl<T: Settings> AnySettingValue for SettingValue<T> {
966 fn from_settings(&self, s: &SettingsContent, cx: &mut App) -> Box<dyn Any> {
967 Box::new(T::from_settings(s, cx)) as _
968 }
969
970 fn setting_type_name(&self) -> &'static str {
971 type_name::<T>()
972 }
973
974 fn all_local_values(&self) -> Vec<(WorktreeId, Arc<Path>, &dyn Any)> {
975 self.local_values
976 .iter()
977 .map(|(id, path, value)| (*id, path.clone(), value as _))
978 .collect()
979 }
980
981 fn value_for_path(&self, path: Option<SettingsLocation>) -> &dyn Any {
982 if let Some(SettingsLocation { worktree_id, path }) = path {
983 for (settings_root_id, settings_path, value) in self.local_values.iter().rev() {
984 if worktree_id == *settings_root_id && path.starts_with(settings_path) {
985 return value;
986 }
987 }
988 }
989
990 self.global_value
991 .as_ref()
992 .unwrap_or_else(|| panic!("no default value for setting {}", self.setting_type_name()))
993 }
994
995 fn set_global_value(&mut self, value: Box<dyn Any>) {
996 self.global_value = Some(*value.downcast().unwrap());
997 }
998
999 fn set_local_value(&mut self, root_id: WorktreeId, path: Arc<Path>, value: Box<dyn Any>) {
1000 let value = *value.downcast().unwrap();
1001 match self
1002 .local_values
1003 .binary_search_by_key(&(root_id, &path), |e| (e.0, &e.1))
1004 {
1005 Ok(ix) => self.local_values[ix].2 = value,
1006 Err(ix) => self.local_values.insert(ix, (root_id, path, value)),
1007 }
1008 }
1009
1010 fn import_from_vscode(
1011 &self,
1012 vscode_settings: &VsCodeSettings,
1013 settings_content: &mut SettingsContent,
1014 ) {
1015 T::import_from_vscode(vscode_settings, settings_content);
1016 }
1017}
1018
1019#[cfg(test)]
1020mod tests {
1021 use std::num::NonZeroU32;
1022
1023 use crate::{
1024 ClosePosition, ItemSettingsContent, VsCodeSettingsSource, default_settings,
1025 settings_content::LanguageSettingsContent, test_settings,
1026 };
1027
1028 use super::*;
1029 use unindent::Unindent;
1030
1031 #[derive(Debug, PartialEq)]
1032 struct AutoUpdateSetting {
1033 auto_update: bool,
1034 }
1035
1036 impl Settings for AutoUpdateSetting {
1037 fn from_settings(content: &SettingsContent, _: &mut App) -> Self {
1038 AutoUpdateSetting {
1039 auto_update: content.auto_update.unwrap(),
1040 }
1041 }
1042 }
1043
1044 #[derive(Debug, PartialEq)]
1045 struct ItemSettings {
1046 close_position: ClosePosition,
1047 git_status: bool,
1048 }
1049
1050 impl Settings for ItemSettings {
1051 fn from_settings(content: &SettingsContent, _: &mut App) -> Self {
1052 let content = content.tabs.clone().unwrap();
1053 ItemSettings {
1054 close_position: content.close_position.unwrap(),
1055 git_status: content.git_status.unwrap(),
1056 }
1057 }
1058
1059 fn import_from_vscode(vscode: &VsCodeSettings, content: &mut SettingsContent) {
1060 let mut show = None;
1061
1062 vscode.bool_setting("workbench.editor.decorations.colors", &mut show);
1063 if let Some(show) = show {
1064 content
1065 .tabs
1066 .get_or_insert_default()
1067 .git_status
1068 .replace(show);
1069 }
1070 }
1071 }
1072
1073 #[derive(Debug, PartialEq)]
1074 struct DefaultLanguageSettings {
1075 tab_size: NonZeroU32,
1076 preferred_line_length: u32,
1077 }
1078
1079 impl Settings for DefaultLanguageSettings {
1080 fn from_settings(content: &SettingsContent, _: &mut App) -> Self {
1081 let content = &content.project.all_languages.defaults;
1082 DefaultLanguageSettings {
1083 tab_size: content.tab_size.unwrap(),
1084 preferred_line_length: content.preferred_line_length.unwrap(),
1085 }
1086 }
1087
1088 fn import_from_vscode(vscode: &VsCodeSettings, content: &mut SettingsContent) {
1089 let content = &mut content.project.all_languages.defaults;
1090
1091 if let Some(size) = vscode
1092 .read_value("editor.tabSize")
1093 .and_then(|v| v.as_u64())
1094 .and_then(|n| NonZeroU32::new(n as u32))
1095 {
1096 content.tab_size = Some(size);
1097 }
1098 }
1099 }
1100
1101 #[gpui::test]
1102 fn test_settings_store_basic(cx: &mut App) {
1103 let mut store = SettingsStore::new(cx, &default_settings());
1104 store.register_setting::<AutoUpdateSetting>(cx);
1105 store.register_setting::<ItemSettings>(cx);
1106 store.register_setting::<DefaultLanguageSettings>(cx);
1107
1108 assert_eq!(
1109 store.get::<AutoUpdateSetting>(None),
1110 &AutoUpdateSetting { auto_update: true }
1111 );
1112 assert_eq!(
1113 store.get::<ItemSettings>(None).close_position,
1114 ClosePosition::Right
1115 );
1116
1117 store
1118 .set_user_settings(
1119 r#"{
1120 "auto_update": false,
1121 "tabs": {
1122 "close_position": "left"
1123 }
1124 }"#,
1125 cx,
1126 )
1127 .unwrap();
1128
1129 assert_eq!(
1130 store.get::<AutoUpdateSetting>(None),
1131 &AutoUpdateSetting { auto_update: false }
1132 );
1133 assert_eq!(
1134 store.get::<ItemSettings>(None).close_position,
1135 ClosePosition::Left
1136 );
1137
1138 store
1139 .set_local_settings(
1140 WorktreeId::from_usize(1),
1141 Path::new("/root1").into(),
1142 LocalSettingsKind::Settings,
1143 Some(r#"{ "tab_size": 5 }"#),
1144 cx,
1145 )
1146 .unwrap();
1147 store
1148 .set_local_settings(
1149 WorktreeId::from_usize(1),
1150 Path::new("/root1/subdir").into(),
1151 LocalSettingsKind::Settings,
1152 Some(r#"{ "preferred_line_length": 50 }"#),
1153 cx,
1154 )
1155 .unwrap();
1156
1157 store
1158 .set_local_settings(
1159 WorktreeId::from_usize(1),
1160 Path::new("/root2").into(),
1161 LocalSettingsKind::Settings,
1162 Some(r#"{ "tab_size": 9, "auto_update": true}"#),
1163 cx,
1164 )
1165 .unwrap();
1166
1167 assert_eq!(
1168 store.get::<DefaultLanguageSettings>(Some(SettingsLocation {
1169 worktree_id: WorktreeId::from_usize(1),
1170 path: Path::new("/root1/something"),
1171 })),
1172 &DefaultLanguageSettings {
1173 preferred_line_length: 80,
1174 tab_size: 5.try_into().unwrap(),
1175 }
1176 );
1177 assert_eq!(
1178 store.get::<DefaultLanguageSettings>(Some(SettingsLocation {
1179 worktree_id: WorktreeId::from_usize(1),
1180 path: Path::new("/root1/subdir/something")
1181 })),
1182 &DefaultLanguageSettings {
1183 preferred_line_length: 50,
1184 tab_size: 5.try_into().unwrap(),
1185 }
1186 );
1187 assert_eq!(
1188 store.get::<DefaultLanguageSettings>(Some(SettingsLocation {
1189 worktree_id: WorktreeId::from_usize(1),
1190 path: Path::new("/root2/something")
1191 })),
1192 &DefaultLanguageSettings {
1193 preferred_line_length: 80,
1194 tab_size: 9.try_into().unwrap(),
1195 }
1196 );
1197 assert_eq!(
1198 store.get::<AutoUpdateSetting>(Some(SettingsLocation {
1199 worktree_id: WorktreeId::from_usize(1),
1200 path: Path::new("/root2/something")
1201 })),
1202 &AutoUpdateSetting { auto_update: false }
1203 );
1204 }
1205
1206 #[gpui::test]
1207 fn test_setting_store_assign_json_before_register(cx: &mut App) {
1208 let mut store = SettingsStore::new(cx, &test_settings());
1209 store
1210 .set_user_settings(r#"{ "auto_update": false }"#, cx)
1211 .unwrap();
1212 store.register_setting::<AutoUpdateSetting>(cx);
1213
1214 assert_eq!(
1215 store.get::<AutoUpdateSetting>(None),
1216 &AutoUpdateSetting { auto_update: false }
1217 );
1218 }
1219
1220 #[track_caller]
1221 fn check_settings_update(
1222 store: &mut SettingsStore,
1223 old_json: String,
1224 update: fn(&mut SettingsContent),
1225 expected_new_json: String,
1226 cx: &mut App,
1227 ) {
1228 store.set_user_settings(&old_json, cx).ok();
1229 let edits = store.edits_for_update(&old_json, update);
1230 let mut new_json = old_json;
1231 for (range, replacement) in edits.into_iter() {
1232 new_json.replace_range(range, &replacement);
1233 }
1234 pretty_assertions::assert_eq!(new_json, expected_new_json);
1235 }
1236
1237 #[gpui::test]
1238 fn test_setting_store_update(cx: &mut App) {
1239 let mut store = SettingsStore::new(cx, &test_settings());
1240
1241 // entries added and updated
1242 check_settings_update(
1243 &mut store,
1244 r#"{
1245 "languages": {
1246 "JSON": {
1247 "auto_indent": true
1248 }
1249 }
1250 }"#
1251 .unindent(),
1252 |settings| {
1253 settings
1254 .languages_mut()
1255 .get_mut("JSON")
1256 .unwrap()
1257 .auto_indent = Some(false);
1258
1259 settings.languages_mut().insert(
1260 "Rust".into(),
1261 LanguageSettingsContent {
1262 auto_indent: Some(true),
1263 ..Default::default()
1264 },
1265 );
1266 },
1267 r#"{
1268 "languages": {
1269 "Rust": {
1270 "auto_indent": true
1271 },
1272 "JSON": {
1273 "auto_indent": false
1274 }
1275 }
1276 }"#
1277 .unindent(),
1278 cx,
1279 );
1280
1281 // entries removed
1282 check_settings_update(
1283 &mut store,
1284 r#"{
1285 "languages": {
1286 "Rust": {
1287 "language_setting_2": true
1288 },
1289 "JSON": {
1290 "language_setting_1": false
1291 }
1292 }
1293 }"#
1294 .unindent(),
1295 |settings| {
1296 settings.languages_mut().remove("JSON").unwrap();
1297 },
1298 r#"{
1299 "languages": {
1300 "Rust": {
1301 "language_setting_2": true
1302 }
1303 }
1304 }"#
1305 .unindent(),
1306 cx,
1307 );
1308
1309 check_settings_update(
1310 &mut store,
1311 r#"{
1312 "languages": {
1313 "Rust": {
1314 "language_setting_2": true
1315 },
1316 "JSON": {
1317 "language_setting_1": false
1318 }
1319 }
1320 }"#
1321 .unindent(),
1322 |settings| {
1323 settings.languages_mut().remove("Rust").unwrap();
1324 },
1325 r#"{
1326 "languages": {
1327 "JSON": {
1328 "language_setting_1": false
1329 }
1330 }
1331 }"#
1332 .unindent(),
1333 cx,
1334 );
1335
1336 // weird formatting
1337 check_settings_update(
1338 &mut store,
1339 r#"{
1340 "tabs": { "close_position": "left", "name": "Max" }
1341 }"#
1342 .unindent(),
1343 |settings| {
1344 settings.tabs.as_mut().unwrap().close_position = Some(ClosePosition::Left);
1345 },
1346 r#"{
1347 "tabs": { "close_position": "left", "name": "Max" }
1348 }"#
1349 .unindent(),
1350 cx,
1351 );
1352
1353 // single-line formatting, other keys
1354 check_settings_update(
1355 &mut store,
1356 r#"{ "one": 1, "two": 2 }"#.to_owned(),
1357 |settings| settings.auto_update = Some(true),
1358 r#"{ "auto_update": true, "one": 1, "two": 2 }"#.to_owned(),
1359 cx,
1360 );
1361
1362 // empty object
1363 check_settings_update(
1364 &mut store,
1365 r#"{
1366 "tabs": {}
1367 }"#
1368 .unindent(),
1369 |settings| settings.tabs.as_mut().unwrap().close_position = Some(ClosePosition::Left),
1370 r#"{
1371 "tabs": {
1372 "close_position": "left"
1373 }
1374 }"#
1375 .unindent(),
1376 cx,
1377 );
1378
1379 // no content
1380 check_settings_update(
1381 &mut store,
1382 r#""#.unindent(),
1383 |settings| {
1384 settings.tabs = Some(ItemSettingsContent {
1385 git_status: Some(true),
1386 ..Default::default()
1387 })
1388 },
1389 r#"{
1390 "tabs": {
1391 "git_status": true
1392 }
1393 }
1394 "#
1395 .unindent(),
1396 cx,
1397 );
1398
1399 check_settings_update(
1400 &mut store,
1401 r#"{
1402 }
1403 "#
1404 .unindent(),
1405 |settings| settings.title_bar.get_or_insert_default().show_branch_name = Some(true),
1406 r#"{
1407 "title_bar": {
1408 "show_branch_name": true
1409 }
1410 }
1411 "#
1412 .unindent(),
1413 cx,
1414 );
1415 }
1416
1417 #[gpui::test]
1418 fn test_vscode_import(cx: &mut App) {
1419 let mut store = SettingsStore::new(cx, &test_settings());
1420 store.register_setting::<DefaultLanguageSettings>(cx);
1421 store.register_setting::<ItemSettings>(cx);
1422 store.register_setting::<AutoUpdateSetting>(cx);
1423
1424 // create settings that werent present
1425 check_vscode_import(
1426 &mut store,
1427 r#"{
1428 }
1429 "#
1430 .unindent(),
1431 r#" { "editor.tabSize": 37 } "#.to_owned(),
1432 r#"{
1433 "tab_size": 37
1434 }
1435 "#
1436 .unindent(),
1437 cx,
1438 );
1439
1440 // persist settings that were present
1441 check_vscode_import(
1442 &mut store,
1443 r#"{
1444 "preferred_line_length": 99,
1445 }
1446 "#
1447 .unindent(),
1448 r#"{ "editor.tabSize": 42 }"#.to_owned(),
1449 r#"{
1450 "tab_size": 42,
1451 "preferred_line_length": 99,
1452 }
1453 "#
1454 .unindent(),
1455 cx,
1456 );
1457
1458 // don't clobber settings that aren't present in vscode
1459 check_vscode_import(
1460 &mut store,
1461 r#"{
1462 "preferred_line_length": 99,
1463 "tab_size": 42
1464 }
1465 "#
1466 .unindent(),
1467 r#"{}"#.to_owned(),
1468 r#"{
1469 "preferred_line_length": 99,
1470 "tab_size": 42
1471 }
1472 "#
1473 .unindent(),
1474 cx,
1475 );
1476
1477 // custom enum
1478 check_vscode_import(
1479 &mut store,
1480 r#"{
1481 }
1482 "#
1483 .unindent(),
1484 r#"{ "workbench.editor.decorations.colors": true }"#.to_owned(),
1485 r#"{
1486 "tabs": {
1487 "git_status": true
1488 }
1489 }
1490 "#
1491 .unindent(),
1492 cx,
1493 );
1494 }
1495
1496 #[track_caller]
1497 fn check_vscode_import(
1498 store: &mut SettingsStore,
1499 old: String,
1500 vscode: String,
1501 expected: String,
1502 cx: &mut App,
1503 ) {
1504 store.set_user_settings(&old, cx).ok();
1505 let new = store.get_vscode_edits(
1506 old,
1507 &VsCodeSettings::from_str(&vscode, VsCodeSettingsSource::VsCode).unwrap(),
1508 );
1509 pretty_assertions::assert_eq!(new, expected);
1510 }
1511
1512 #[gpui::test]
1513 fn test_update_git_settings(cx: &mut App) {
1514 let store = SettingsStore::new(cx, &test_settings());
1515
1516 let actual = store.new_text_for_update("{}".to_string(), |current| {
1517 current
1518 .git
1519 .get_or_insert_default()
1520 .inline_blame
1521 .get_or_insert_default()
1522 .enabled = Some(true);
1523 });
1524 assert_eq!(
1525 actual,
1526 r#"{
1527 "git": {
1528 "inline_blame": {
1529 "enabled": true
1530 }
1531 }
1532 }
1533 "#
1534 .unindent()
1535 );
1536 }
1537
1538 #[gpui::test]
1539 fn test_global_settings(cx: &mut App) {
1540 let mut store = SettingsStore::new(cx, &test_settings());
1541 store.register_setting::<ItemSettings>(cx);
1542
1543 // Set global settings - these should override defaults but not user settings
1544 store
1545 .set_global_settings(
1546 r#"{
1547 "tabs": {
1548 "close_position": "right",
1549 "git_status": true,
1550 }
1551 }"#,
1552 cx,
1553 )
1554 .unwrap();
1555
1556 // Before user settings, global settings should apply
1557 assert_eq!(
1558 store.get::<ItemSettings>(None),
1559 &ItemSettings {
1560 close_position: ClosePosition::Right,
1561 git_status: true,
1562 }
1563 );
1564
1565 // Set user settings - these should override both defaults and global
1566 store
1567 .set_user_settings(
1568 r#"{
1569 "tabs": {
1570 "close_position": "left"
1571 }
1572 }"#,
1573 cx,
1574 )
1575 .unwrap();
1576
1577 // User settings should override global settings
1578 assert_eq!(
1579 store.get::<ItemSettings>(None),
1580 &ItemSettings {
1581 close_position: ClosePosition::Left,
1582 git_status: true, // Staff from global settings
1583 }
1584 );
1585 }
1586}