Files
zed/crates/settings/src/settings_store.rs
T
Kirill Bulatov eae9741e2a Fix default.json profile and os settings not applying on startup (#54635)
When started working on "disable the profiler for all but Nightly and
Dev" users, I've tried to do that using the default OS settings and
discovered that those are ignored.

Release Notes:

- Fixed default.json profile and os settings not applying on startup
2026-04-24 15:05:52 +00:00

3014 lines
101 KiB
Rust

use anyhow::{Context as _, Result};
use collections::{BTreeMap, HashMap, btree_map, hash_map};
use fs::Fs;
use futures::{
FutureExt, StreamExt,
channel::{mpsc, oneshot},
future::LocalBoxFuture,
};
use gpui::{
App, AppContext, AsyncApp, BorrowAppContext, Entity, Global, SharedString, Task, UpdateGlobal,
};
use paths::{local_settings_file_relative_path, task_file_name};
use schemars::{JsonSchema, json_schema};
use serde_json::Value;
use settings_content::{ActionName, ParseStatus};
use std::{
any::{Any, TypeId, type_name},
fmt::Debug,
ops::Range,
path::{Path, PathBuf},
rc::Rc,
str,
sync::Arc,
};
use util::{
ResultExt as _,
rel_path::RelPath,
schemars::{AllowTrailingCommas, DefaultDenyUnknownFields, replace_subschema},
};
use crate::editorconfig_store::EditorconfigStore;
use crate::{
ActiveSettingsProfileName, FontFamilyName, IconThemeName, LanguageSettingsContent,
LanguageToSettingsMap, LspSettings, LspSettingsMap, SemanticTokenRules, ThemeName,
UserSettingsContentExt, VsCodeSettings, WorktreeId,
settings_content::{
ExtensionsSettingsContent, ProfileBase, ProjectSettingsContent, RootUserSettings,
SettingsContent, UserSettingsContent, merge_from::MergeFrom,
},
};
use settings_json::{infer_json_indent_size, update_value_in_json_text};
pub const LSP_SETTINGS_SCHEMA_URL_PREFIX: &str = "zed://schemas/settings/lsp/";
pub trait SettingsKey: 'static + Send + Sync {
/// The name of a key within the JSON file from which this setting should
/// be deserialized. If this is `None`, then the setting will be deserialized
/// from the root object.
const KEY: Option<&'static str>;
const FALLBACK_KEY: Option<&'static str> = None;
}
/// A value that can be defined as a user setting.
///
/// Settings can be loaded from a combination of multiple JSON files.
pub trait Settings: 'static + Send + Sync + Sized {
/// The name of the keys in the [`FileContent`](Self::FileContent) that should
/// always be written to a settings file, even if their value matches the default
/// value.
///
/// This is useful for tagged [`FileContent`](Self::FileContent)s where the tag
/// is a "version" field that should always be persisted, even if the current
/// user settings match the current version of the settings.
const PRESERVED_KEYS: Option<&'static [&'static str]> = None;
/// Read the value from default.json.
///
/// This function *should* panic if default values are missing,
/// and you should add a default to default.json for documentation.
fn from_settings(content: &SettingsContent) -> Self;
#[track_caller]
fn register(cx: &mut App)
where
Self: Sized,
{
SettingsStore::update_global(cx, |store, _| {
store.register_setting::<Self>();
});
}
#[track_caller]
fn get<'a>(path: Option<SettingsLocation>, cx: &'a App) -> &'a Self
where
Self: Sized,
{
cx.global::<SettingsStore>().get(path)
}
#[track_caller]
fn get_global(cx: &App) -> &Self
where
Self: Sized,
{
cx.global::<SettingsStore>().get(None)
}
#[track_caller]
fn try_get(cx: &App) -> Option<&Self>
where
Self: Sized,
{
if cx.has_global::<SettingsStore>() {
cx.global::<SettingsStore>().try_get(None)
} else {
None
}
}
#[track_caller]
fn try_read_global<R>(cx: &AsyncApp, f: impl FnOnce(&Self) -> R) -> Option<R>
where
Self: Sized,
{
cx.try_read_global(|s: &SettingsStore, _| f(s.get(None)))
}
#[track_caller]
fn override_global(settings: Self, cx: &mut App)
where
Self: Sized,
{
cx.global_mut::<SettingsStore>().override_global(settings)
}
}
pub struct RegisteredSetting {
pub settings_value: fn() -> Box<dyn AnySettingValue>,
pub from_settings: fn(&SettingsContent) -> Box<dyn Any>,
pub id: fn() -> TypeId,
}
inventory::collect!(RegisteredSetting);
#[derive(Clone, Copy, Debug)]
pub struct SettingsLocation<'a> {
pub worktree_id: WorktreeId,
pub path: &'a RelPath,
}
pub struct SettingsStore {
setting_values: HashMap<TypeId, Box<dyn AnySettingValue>>,
default_settings: Rc<SettingsContent>,
user_settings: Option<UserSettingsContent>,
global_settings: Option<Box<SettingsContent>>,
extension_settings: Option<Box<SettingsContent>>,
server_settings: Option<Box<SettingsContent>>,
language_semantic_token_rules: HashMap<SharedString, SemanticTokenRules>,
merged_settings: Rc<SettingsContent>,
last_user_settings_content: Option<String>,
last_global_settings_content: Option<String>,
local_settings: BTreeMap<(WorktreeId, Arc<RelPath>), SettingsContent>,
pub editorconfig_store: Entity<EditorconfigStore>,
_settings_files_watcher: Option<Task<()>>,
_setting_file_updates: Task<()>,
setting_file_updates_tx:
mpsc::UnboundedSender<Box<dyn FnOnce(AsyncApp) -> LocalBoxFuture<'static, Result<()>>>>,
file_errors: BTreeMap<SettingsFile, SettingsParseResult>,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum SettingsFile {
Default,
Global,
User,
Server,
/// Represents project settings in ssh projects as well as local projects
Project((WorktreeId, Arc<RelPath>)),
}
impl PartialOrd for SettingsFile {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
/// Sorted in order of precedence
impl Ord for SettingsFile {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
use SettingsFile::*;
use std::cmp::Ordering;
match (self, other) {
(User, User) => Ordering::Equal,
(Server, Server) => Ordering::Equal,
(Default, Default) => Ordering::Equal,
(Project((id1, rel_path1)), Project((id2, rel_path2))) => id1
.cmp(id2)
.then_with(|| rel_path1.cmp(rel_path2).reverse()),
(Project(_), _) => Ordering::Less,
(_, Project(_)) => Ordering::Greater,
(Server, _) => Ordering::Less,
(_, Server) => Ordering::Greater,
(User, _) => Ordering::Less,
(_, User) => Ordering::Greater,
(Global, _) => Ordering::Less,
(_, Global) => Ordering::Greater,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum LocalSettingsKind {
Settings,
Tasks,
Editorconfig,
Debug,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum LocalSettingsPath {
InWorktree(Arc<RelPath>),
OutsideWorktree(Arc<Path>),
}
impl LocalSettingsPath {
pub fn is_outside_worktree(&self) -> bool {
matches!(self, Self::OutsideWorktree(_))
}
pub fn to_proto(&self) -> String {
match self {
Self::InWorktree(path) => path.to_proto(),
Self::OutsideWorktree(path) => path.to_string_lossy().to_string(),
}
}
pub fn from_proto(path: &str, is_outside_worktree: bool) -> anyhow::Result<Self> {
if is_outside_worktree {
Ok(Self::OutsideWorktree(PathBuf::from(path).into()))
} else {
Ok(Self::InWorktree(RelPath::from_proto(path)?))
}
}
}
impl Global for SettingsStore {}
#[derive(Default)]
pub struct DefaultSemanticTokenRules(pub SemanticTokenRules);
impl gpui::Global for DefaultSemanticTokenRules {}
#[doc(hidden)]
#[derive(Debug)]
pub struct SettingValue<T> {
#[doc(hidden)]
pub global_value: Option<T>,
#[doc(hidden)]
pub local_values: Vec<(WorktreeId, Arc<RelPath>, T)>,
}
#[doc(hidden)]
pub trait AnySettingValue: 'static + Send + Sync {
fn setting_type_name(&self) -> &'static str;
fn from_settings(&self, s: &SettingsContent) -> Box<dyn Any>;
fn value_for_path(&self, path: Option<SettingsLocation>) -> &dyn Any;
fn all_local_values(&self) -> Vec<(WorktreeId, Arc<RelPath>, &dyn Any)>;
fn set_global_value(&mut self, value: Box<dyn Any>);
fn set_local_value(&mut self, root_id: WorktreeId, path: Arc<RelPath>, value: Box<dyn Any>);
fn clear_local_values(&mut self, root_id: WorktreeId);
}
/// Parameters that are used when generating some JSON schemas at runtime.
pub struct SettingsJsonSchemaParams<'a> {
pub language_names: &'a [String],
pub font_names: &'a [String],
pub theme_names: &'a [SharedString],
pub icon_theme_names: &'a [SharedString],
pub lsp_adapter_names: &'a [String],
pub action_names: &'a [&'a str],
pub action_documentation: &'a HashMap<&'a str, &'a str>,
pub deprecations: &'a HashMap<&'a str, &'a str>,
pub deprecation_messages: &'a HashMap<&'a str, &'a str>,
}
impl SettingsStore {
pub fn new(cx: &mut App, default_settings: &str) -> Self {
let (setting_file_updates_tx, mut setting_file_updates_rx) = mpsc::unbounded();
let default_settings = Self::parse_default_settings(default_settings).unwrap();
if !cx.has_global::<DefaultSemanticTokenRules>() {
cx.set_global::<DefaultSemanticTokenRules>(
crate::parse_json_with_comments::<SemanticTokenRules>(
&crate::default_semantic_token_rules(),
)
.map(DefaultSemanticTokenRules)
.unwrap_or_default(),
);
}
let default_settings: Rc<SettingsContent> = default_settings.into();
let mut this = Self {
setting_values: Default::default(),
default_settings: default_settings.clone(),
global_settings: None,
server_settings: None,
user_settings: None,
extension_settings: None,
language_semantic_token_rules: HashMap::default(),
merged_settings: default_settings,
last_user_settings_content: None,
last_global_settings_content: None,
local_settings: BTreeMap::default(),
editorconfig_store: cx.new(|_| EditorconfigStore::default()),
_settings_files_watcher: None,
setting_file_updates_tx,
_setting_file_updates: cx.spawn(async move |cx| {
while let Some(setting_file_update) = setting_file_updates_rx.next().await {
(setting_file_update)(cx.clone()).await.log_err();
}
}),
file_errors: BTreeMap::default(),
};
this.load_settings_types();
this
}
pub fn observe_active_settings_profile_name(cx: &mut App) -> gpui::Subscription {
cx.observe_global::<ActiveSettingsProfileName>(|cx| {
Self::update_global(cx, |store, cx| {
store.recompute_values(None, cx);
});
})
}
pub fn update<C, R>(cx: &mut C, f: impl FnOnce(&mut Self, &mut C) -> R) -> R
where
C: BorrowAppContext,
{
cx.update_global(f)
}
pub fn watch_settings_files(
&mut self,
fs: Arc<dyn Fs>,
cx: &mut App,
settings_changed: impl 'static + Fn(SettingsFile, SettingsParseResult, &mut App),
) {
let (mut user_settings_file_rx, user_settings_watcher) = crate::watch_config_file(
cx.background_executor(),
fs.clone(),
paths::settings_file().clone(),
);
let (mut global_settings_file_rx, global_settings_watcher) = crate::watch_config_file(
cx.background_executor(),
fs,
paths::global_settings_file().clone(),
);
let global_content = cx
.foreground_executor()
.block_on(global_settings_file_rx.next())
.unwrap();
let user_content = cx
.foreground_executor()
.block_on(user_settings_file_rx.next())
.unwrap();
let result = self.set_user_settings(&user_content, cx);
settings_changed(SettingsFile::User, result, cx);
let result = self.set_global_settings(&global_content, cx);
settings_changed(SettingsFile::Global, result, cx);
self._settings_files_watcher = Some(cx.spawn(async move |cx| {
let _user_settings_watcher = user_settings_watcher;
let _global_settings_watcher = global_settings_watcher;
let mut settings_streams = futures::stream::select(
global_settings_file_rx.map(|content| (SettingsFile::Global, content)),
user_settings_file_rx.map(|content| (SettingsFile::User, content)),
);
while let Some((settings_file, content)) = settings_streams.next().await {
cx.update_global(|store: &mut SettingsStore, cx| {
let result = match settings_file {
SettingsFile::User => store.set_user_settings(&content, cx),
SettingsFile::Global => store.set_global_settings(&content, cx),
_ => return,
};
settings_changed(settings_file, result, cx);
cx.refresh_windows();
});
}
}));
}
/// Add a new type of setting to the store.
pub fn register_setting<T: Settings>(&mut self) {
self.register_setting_internal(&RegisteredSetting {
settings_value: || {
Box::new(SettingValue::<T> {
global_value: None,
local_values: Vec::new(),
})
},
from_settings: |content| Box::new(T::from_settings(content)),
id: || TypeId::of::<T>(),
});
}
fn load_settings_types(&mut self) {
for registered_setting in inventory::iter::<RegisteredSetting>() {
self.register_setting_internal(registered_setting);
}
}
fn register_setting_internal(&mut self, registered_setting: &RegisteredSetting) {
let entry = self.setting_values.entry((registered_setting.id)());
if matches!(entry, hash_map::Entry::Occupied(_)) {
return;
}
let setting_value = entry.or_insert((registered_setting.settings_value)());
let value = (registered_setting.from_settings)(&self.merged_settings);
setting_value.set_global_value(value);
}
pub fn merged_settings(&self) -> &SettingsContent {
&self.merged_settings
}
/// Get the value of a setting.
///
/// Panics if the given setting type has not been registered, or if there is no
/// value for this setting.
pub fn get<T: Settings>(&self, path: Option<SettingsLocation>) -> &T {
self.setting_values
.get(&TypeId::of::<T>())
.unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
.value_for_path(path)
.downcast_ref::<T>()
.expect("no default value for setting type")
}
/// Get the value of a setting.
///
/// Does not panic
pub fn try_get<T: Settings>(&self, path: Option<SettingsLocation>) -> Option<&T> {
self.setting_values
.get(&TypeId::of::<T>())
.map(|value| value.value_for_path(path))
.and_then(|value| value.downcast_ref::<T>())
}
/// Get all values from project specific settings
pub fn get_all_locals<T: Settings>(&self) -> Vec<(WorktreeId, Arc<RelPath>, &T)> {
self.setting_values
.get(&TypeId::of::<T>())
.unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
.all_local_values()
.into_iter()
.map(|(id, path, any)| {
(
id,
path,
any.downcast_ref::<T>()
.expect("wrong value type for setting"),
)
})
.collect()
}
/// Override the global value for a setting.
///
/// The given value will be overwritten if the user settings file changes.
pub fn override_global<T: Settings>(&mut self, value: T) {
self.setting_values
.get_mut(&TypeId::of::<T>())
.unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
.set_global_value(Box::new(value))
}
/// Get the user's settings content.
///
/// For user-facing functionality use the typed setting interface.
/// (e.g. ProjectSettings::get_global(cx))
pub fn raw_user_settings(&self) -> Option<&UserSettingsContent> {
self.user_settings.as_ref()
}
/// Get the default settings content as a raw JSON value.
pub fn raw_default_settings(&self) -> &SettingsContent {
&self.default_settings
}
/// Get the configured settings profile names.
pub fn configured_settings_profiles(&self) -> impl Iterator<Item = &str> {
self.user_settings
.iter()
.flat_map(|settings| settings.profiles.keys().map(|k| k.as_str()))
}
#[cfg(any(test, feature = "test-support"))]
pub fn test(cx: &mut App) -> Self {
Self::new(cx, &crate::test_settings())
}
/// Updates the value of a setting in the user's global configuration.
///
/// This is only for tests. Normally, settings are only loaded from
/// JSON files.
#[cfg(any(test, feature = "test-support"))]
pub fn update_user_settings(
&mut self,
cx: &mut App,
update: impl FnOnce(&mut SettingsContent),
) {
let mut content = self.user_settings.clone().unwrap_or_default().content;
update(&mut content);
fn trail(this: &mut SettingsStore, content: Box<SettingsContent>, cx: &mut App) {
let new_text = serde_json::to_string(&UserSettingsContent {
content,
..Default::default()
})
.unwrap();
_ = this.set_user_settings(&new_text, cx);
}
trail(self, content, cx);
}
pub async fn load_settings(fs: &Arc<dyn Fs>) -> Result<String> {
match fs.load(paths::settings_file()).await {
result @ Ok(_) => result,
Err(err) => {
if let Some(e) = err.downcast_ref::<std::io::Error>()
&& e.kind() == std::io::ErrorKind::NotFound
{
return Ok(crate::initial_user_settings_content().to_string());
}
Err(err)
}
}
}
fn update_settings_file_inner(
&self,
fs: Arc<dyn Fs>,
update: impl 'static + Send + FnOnce(String, AsyncApp) -> Result<String>,
) -> oneshot::Receiver<Result<()>> {
let (tx, rx) = oneshot::channel::<Result<()>>();
self.setting_file_updates_tx
.unbounded_send(Box::new(move |cx: AsyncApp| {
async move {
let res = async move {
let old_text = Self::load_settings(&fs).await?;
let new_text = update(old_text, cx.clone())?;
let settings_path = paths::settings_file().as_path();
if fs.is_file(settings_path).await {
let resolved_path =
fs.canonicalize(settings_path).await.with_context(|| {
format!(
"Failed to canonicalize settings path {:?}",
settings_path
)
})?;
fs.atomic_write(resolved_path.clone(), new_text.clone())
.await
.with_context(|| {
format!("Failed to write settings to file {:?}", resolved_path)
})?;
} else {
fs.atomic_write(settings_path.to_path_buf(), new_text.clone())
.await
.with_context(|| {
format!("Failed to write settings to file {:?}", settings_path)
})?;
}
cx.update_global(|store: &mut SettingsStore, cx| {
store.set_user_settings(&new_text, cx).result().map(|_| ())
})
}
.await;
let new_res = match &res {
Ok(_) => anyhow::Ok(()),
Err(e) => Err(anyhow::anyhow!("{:?}", e)),
};
_ = tx.send(new_res);
res
}
.boxed_local()
}))
.map_err(|err| anyhow::format_err!("Failed to update settings file: {}", err))
.log_with_level(log::Level::Warn);
return rx;
}
pub fn update_settings_file(
&self,
fs: Arc<dyn Fs>,
update: impl 'static + Send + FnOnce(&mut SettingsContent, &App),
) {
_ = self.update_settings_file_with_completion(fs, update);
}
pub fn update_settings_file_with_completion(
&self,
fs: Arc<dyn Fs>,
update: impl 'static + Send + FnOnce(&mut SettingsContent, &App),
) -> oneshot::Receiver<Result<()>> {
self.update_settings_file_inner(fs, move |old_text: String, cx: AsyncApp| {
cx.read_global(|store: &SettingsStore, cx| {
store.new_text_for_update(old_text, |content| update(content, cx))
})
})
}
pub fn import_vscode_settings(
&self,
fs: Arc<dyn Fs>,
vscode_settings: VsCodeSettings,
) -> oneshot::Receiver<Result<()>> {
self.update_settings_file_inner(fs, move |old_text: String, cx: AsyncApp| {
cx.read_global(|store: &SettingsStore, _cx| {
store.get_vscode_edits(old_text, &vscode_settings)
})
})
}
pub fn get_all_files(&self) -> Vec<SettingsFile> {
let mut files = Vec::from_iter(
self.local_settings
.keys()
// rev because these are sorted by path, so highest precedence is last
.rev()
.cloned()
.map(SettingsFile::Project),
);
if self.server_settings.is_some() {
files.push(SettingsFile::Server);
}
// ignoring profiles
// ignoring os profiles
// ignoring release channel profiles
// ignoring global
// ignoring extension
if self.user_settings.is_some() {
files.push(SettingsFile::User);
}
files.push(SettingsFile::Default);
files
}
pub fn get_content_for_file(&self, file: SettingsFile) -> Option<&SettingsContent> {
match file {
SettingsFile::User => self
.user_settings
.as_ref()
.map(|settings| settings.content.as_ref()),
SettingsFile::Default => Some(self.default_settings.as_ref()),
SettingsFile::Server => self.server_settings.as_deref(),
SettingsFile::Project(ref key) => self.local_settings.get(key),
SettingsFile::Global => self.global_settings.as_deref(),
}
}
pub fn get_overrides_for_field<T>(
&self,
target_file: SettingsFile,
get: fn(&SettingsContent) -> &Option<T>,
) -> Vec<SettingsFile> {
let all_files = self.get_all_files();
let mut found_file = false;
let mut overrides = Vec::new();
for file in all_files.into_iter().rev() {
if !found_file {
found_file = file == target_file;
continue;
}
if let SettingsFile::Project((wt_id, ref path)) = file
&& let SettingsFile::Project((target_wt_id, ref target_path)) = target_file
&& (wt_id != target_wt_id || !target_path.starts_with(path))
{
// if requesting value from a local file, don't return values from local files in different worktrees
continue;
}
let Some(content) = self.get_content_for_file(file.clone()) else {
continue;
};
if get(content).is_some() {
overrides.push(file);
}
}
overrides
}
/// Checks the given file, and files that the passed file overrides for the given field.
/// Returns the first file found that contains the value.
/// The value will only be None if no file contains the value.
/// I.e. if no file contains the value, returns `(File::Default, None)`
pub fn get_value_from_file<'a, T: 'a>(
&'a self,
target_file: SettingsFile,
pick: fn(&'a SettingsContent) -> Option<T>,
) -> (SettingsFile, Option<T>) {
self.get_value_from_file_inner(target_file, pick, true)
}
/// Same as `Self::get_value_from_file` except that it does not include the current file.
/// Therefore it returns the value that was potentially overloaded by the target file.
pub fn get_value_up_to_file<'a, T: 'a>(
&'a self,
target_file: SettingsFile,
pick: fn(&'a SettingsContent) -> Option<T>,
) -> (SettingsFile, Option<T>) {
self.get_value_from_file_inner(target_file, pick, false)
}
fn get_value_from_file_inner<'a, T: 'a>(
&'a self,
target_file: SettingsFile,
pick: fn(&'a SettingsContent) -> Option<T>,
include_target_file: bool,
) -> (SettingsFile, Option<T>) {
// todo(settings_ui): Add a metadata field for overriding the "overrides" tag, for contextually different settings
// e.g. disable AI isn't overridden, or a vec that gets extended instead or some such
// todo(settings_ui) cache all files
let all_files = self.get_all_files();
let mut found_file = false;
for file in all_files.into_iter() {
if !found_file && file != SettingsFile::Default {
if file != target_file {
continue;
}
found_file = true;
if !include_target_file {
continue;
}
}
if let SettingsFile::Project((worktree_id, ref path)) = file
&& let SettingsFile::Project((target_worktree_id, ref target_path)) = target_file
&& (worktree_id != target_worktree_id || !target_path.starts_with(&path))
{
// if requesting value from a local file, don't return values from local files in different worktrees
continue;
}
let Some(content) = self.get_content_for_file(file.clone()) else {
continue;
};
if let Some(value) = pick(content) {
return (file, Some(value));
}
}
(SettingsFile::Default, None)
}
#[inline(always)]
fn parse_and_migrate_zed_settings<SettingsContentType: RootUserSettings>(
&mut self,
user_settings_content: &str,
file: SettingsFile,
) -> (Option<SettingsContentType>, SettingsParseResult) {
let mut migration_status = MigrationStatus::NotNeeded;
let (settings, parse_status) = if user_settings_content.is_empty() {
SettingsContentType::parse_json("{}")
} else {
let migration_res = migrator::migrate_settings(user_settings_content);
migration_status = match &migration_res {
Ok(Some(_)) => MigrationStatus::Succeeded,
Ok(None) => MigrationStatus::NotNeeded,
Err(err) => MigrationStatus::Failed {
error: err.to_string(),
},
};
let content = match &migration_res {
Ok(Some(content)) => content,
Ok(None) => user_settings_content,
Err(_) => user_settings_content,
};
SettingsContentType::parse_json(content)
};
let result = SettingsParseResult {
parse_status,
migration_status,
};
self.file_errors.insert(file, result.clone());
return (settings, result);
}
pub fn error_for_file(&self, file: SettingsFile) -> Option<SettingsParseResult> {
self.file_errors
.get(&file)
.filter(|parse_result| parse_result.requires_user_action())
.cloned()
}
}
impl SettingsStore {
/// Updates the value of a setting in a JSON file, returning the new text
/// for that JSON file.
pub fn new_text_for_update(
&self,
old_text: String,
update: impl FnOnce(&mut SettingsContent),
) -> Result<String> {
let edits = self.edits_for_update(&old_text, update)?;
let mut new_text = old_text;
for (range, replacement) in edits.into_iter() {
new_text.replace_range(range, &replacement);
}
Ok(new_text)
}
pub fn get_vscode_edits(&self, old_text: String, vscode: &VsCodeSettings) -> Result<String> {
self.new_text_for_update(old_text, |content| {
content.merge_from(&vscode.settings_content())
})
}
/// Updates the value of a setting in a JSON file, returning a list
/// of edits to apply to the JSON file.
pub fn edits_for_update(
&self,
text: &str,
update: impl FnOnce(&mut SettingsContent),
) -> Result<Vec<(Range<usize>, String)>> {
let old_content = if text.trim().is_empty() {
UserSettingsContent::default()
} else {
let (old_content, parse_status) = UserSettingsContent::parse_json(text);
if let ParseStatus::Failed { error } = &parse_status {
log::error!("Failed to parse settings for update: {error}");
}
old_content
.context("Settings file could not be parsed. Fix syntax errors before updating.")?
};
let mut new_content = old_content.clone();
update(&mut new_content.content);
let old_value = serde_json::to_value(&old_content).unwrap();
let new_value = serde_json::to_value(new_content).unwrap();
let mut key_path = Vec::new();
let mut edits = Vec::new();
let tab_size = infer_json_indent_size(&text);
let mut text = text.to_string();
update_value_in_json_text(
&mut text,
&mut key_path,
tab_size,
&old_value,
&new_value,
&mut edits,
);
Ok(edits)
}
/// Mutates the default settings in place and recomputes all setting values.
pub fn update_default_settings(
&mut self,
cx: &mut App,
update: impl FnOnce(&mut SettingsContent),
) {
let default_settings = Rc::make_mut(&mut self.default_settings);
update(default_settings);
self.recompute_values(None, cx);
}
/// Sets the default settings via a JSON string.
///
/// The string should contain a JSON object with a default value for every setting.
pub fn set_default_settings(
&mut self,
default_settings_content: &str,
cx: &mut App,
) -> Result<()> {
self.default_settings = Self::parse_default_settings(default_settings_content)?.into();
self.recompute_values(None, cx);
Ok(())
}
/// Parses the default settings JSON and folds any `dev`/`nightly`/`preview`/`stable`
/// release-channel overrides and `macos`/`linux`/`windows` platform overrides into
/// the returned [`SettingsContent`].
///
/// Unlike user settings, default settings are used directly as the base for all
/// merges, so overrides must be resolved up front.
fn parse_default_settings(default_settings: &str) -> Result<SettingsContent> {
let parsed = UserSettingsContent::parse_json_with_comments(default_settings)?;
let mut merged = (*parsed.content).clone();
merged.merge_from_option(parsed.for_release_channel());
merged.merge_from_option(parsed.for_os());
Ok(merged)
}
/// Sets the user settings via a JSON string.
#[must_use]
pub fn set_user_settings(
&mut self,
user_settings_content: &str,
cx: &mut App,
) -> SettingsParseResult {
if self.last_user_settings_content.as_deref() == Some(user_settings_content) {
return SettingsParseResult {
parse_status: ParseStatus::Unchanged,
migration_status: MigrationStatus::NotNeeded,
};
}
self.last_user_settings_content = Some(user_settings_content.to_string());
let (settings, parse_result) = self.parse_and_migrate_zed_settings::<UserSettingsContent>(
user_settings_content,
SettingsFile::User,
);
if let Some(settings) = settings {
self.user_settings = Some(settings);
self.recompute_values(None, cx);
}
return parse_result;
}
/// Sets the global settings via a JSON string.
#[must_use]
pub fn set_global_settings(
&mut self,
global_settings_content: &str,
cx: &mut App,
) -> SettingsParseResult {
if self.last_global_settings_content.as_deref() == Some(global_settings_content) {
return SettingsParseResult {
parse_status: ParseStatus::Unchanged,
migration_status: MigrationStatus::NotNeeded,
};
}
self.last_global_settings_content = Some(global_settings_content.to_string());
let (settings, parse_result) = self.parse_and_migrate_zed_settings::<SettingsContent>(
global_settings_content,
SettingsFile::Global,
);
if let Some(settings) = settings {
self.global_settings = Some(Box::new(settings));
self.recompute_values(None, cx);
}
return parse_result;
}
pub fn set_server_settings(
&mut self,
server_settings_content: &str,
cx: &mut App,
) -> Result<()> {
let settings = if server_settings_content.is_empty() {
None
} else {
Option::<SettingsContent>::parse_json_with_comments(server_settings_content)?
};
// Rewrite the server settings into a content type
self.server_settings = settings.map(|settings| Box::new(settings));
self.recompute_values(None, cx);
Ok(())
}
/// Sets language-specific semantic token rules.
///
/// These rules are registered by language modules (e.g. the Rust language module)
/// or by third-party extensions (via `semantic_token_rules.json` in their language
/// directories). They are stored separately from the global rules and are only
/// applied to buffers of the matching language by the `SemanticTokenStylizer`.
///
/// This triggers a settings recomputation so that observers (e.g. `LspStore`)
/// are notified and can invalidate cached stylizers.
pub fn set_language_semantic_token_rules(
&mut self,
language: SharedString,
rules: SemanticTokenRules,
cx: &mut App,
) {
self.language_semantic_token_rules.insert(language, rules);
self.recompute_values(None, cx);
}
/// Removes language-specific semantic token rules for the given language.
///
/// This should be called when an extension that registered rules for a language
/// is unloaded. Triggers a settings recomputation so that observers (e.g.
/// `LspStore`) are notified and can invalidate cached stylizers.
pub fn remove_language_semantic_token_rules(&mut self, language: &str, cx: &mut App) {
self.language_semantic_token_rules.remove(language);
self.recompute_values(None, cx);
}
/// Returns the language-specific semantic token rules for the given language,
/// if any have been registered.
pub fn language_semantic_token_rules(&self, language: &str) -> Option<&SemanticTokenRules> {
self.language_semantic_token_rules.get(language)
}
/// Add or remove a set of local settings via a JSON string.
pub fn set_local_settings(
&mut self,
root_id: WorktreeId,
path: LocalSettingsPath,
kind: LocalSettingsKind,
settings_content: Option<&str>,
cx: &mut App,
) -> std::result::Result<(), InvalidSettingsError> {
let content = settings_content
.map(|content| content.trim())
.filter(|content| !content.is_empty());
let mut zed_settings_changed = false;
match (path.clone(), kind, content) {
(LocalSettingsPath::InWorktree(directory_path), LocalSettingsKind::Tasks, _) => {
return Err(InvalidSettingsError::Tasks {
message: "Attempted to submit tasks into the settings store".to_string(),
path: directory_path
.join(RelPath::unix(task_file_name()).unwrap())
.as_std_path()
.to_path_buf(),
});
}
(LocalSettingsPath::InWorktree(directory_path), LocalSettingsKind::Debug, _) => {
return Err(InvalidSettingsError::Debug {
message: "Attempted to submit debugger config into the settings store"
.to_string(),
path: directory_path
.join(RelPath::unix(task_file_name()).unwrap())
.as_std_path()
.to_path_buf(),
});
}
(LocalSettingsPath::InWorktree(directory_path), LocalSettingsKind::Settings, None) => {
zed_settings_changed = self
.local_settings
.remove(&(root_id, directory_path.clone()))
.is_some();
self.file_errors
.remove(&SettingsFile::Project((root_id, directory_path)));
}
(
LocalSettingsPath::InWorktree(directory_path),
LocalSettingsKind::Settings,
Some(settings_contents),
) => {
let (new_settings, parse_result) = self
.parse_and_migrate_zed_settings::<ProjectSettingsContent>(
settings_contents,
SettingsFile::Project((root_id, directory_path.clone())),
);
match parse_result.parse_status {
ParseStatus::Success => Ok(()),
ParseStatus::Unchanged => Ok(()),
ParseStatus::Failed { error } => Err(InvalidSettingsError::LocalSettings {
path: directory_path.join(local_settings_file_relative_path()),
message: error,
}),
}?;
if let Some(new_settings) = new_settings {
match self.local_settings.entry((root_id, directory_path)) {
btree_map::Entry::Vacant(v) => {
v.insert(SettingsContent {
project: new_settings,
..Default::default()
});
zed_settings_changed = true;
}
btree_map::Entry::Occupied(mut o) => {
if &o.get().project != &new_settings {
o.insert(SettingsContent {
project: new_settings,
..Default::default()
});
zed_settings_changed = true;
}
}
}
}
}
(directory_path, LocalSettingsKind::Editorconfig, editorconfig_contents) => {
self.editorconfig_store.update(cx, |store, _| {
store.set_configs(root_id, directory_path, editorconfig_contents)
})?;
}
(LocalSettingsPath::OutsideWorktree(path), kind, _) => {
log::error!(
"OutsideWorktree path {:?} with kind {:?} is only supported by editorconfig",
path,
kind
);
return Ok(());
}
}
if let LocalSettingsPath::InWorktree(directory_path) = &path {
if zed_settings_changed {
self.recompute_values(Some((root_id, &directory_path)), cx);
}
}
Ok(())
}
pub fn set_extension_settings(
&mut self,
content: ExtensionsSettingsContent,
cx: &mut App,
) -> Result<()> {
self.extension_settings = Some(Box::new(SettingsContent {
project: ProjectSettingsContent {
all_languages: content.all_languages,
..Default::default()
},
..Default::default()
}));
self.recompute_values(None, cx);
Ok(())
}
/// Add or remove a set of local settings via a JSON string.
pub fn clear_local_settings(&mut self, root_id: WorktreeId, cx: &mut App) -> Result<()> {
self.local_settings
.retain(|(worktree_id, _), _| worktree_id != &root_id);
self.editorconfig_store
.update(cx, |store, _cx| store.remove_for_worktree(root_id));
for setting_value in self.setting_values.values_mut() {
setting_value.clear_local_values(root_id);
}
self.recompute_values(Some((root_id, RelPath::empty())), cx);
Ok(())
}
pub fn local_settings(
&self,
root_id: WorktreeId,
) -> impl '_ + Iterator<Item = (Arc<RelPath>, &ProjectSettingsContent)> {
self.local_settings
.range(
(root_id, RelPath::empty().into())
..(
WorktreeId::from_usize(root_id.to_usize() + 1),
RelPath::empty().into(),
),
)
.map(|((_, path), content)| (path.clone(), &content.project))
}
/// Configures common schema replacements shared between user and project
/// settings schemas.
///
/// This sets up language-specific settings and LSP adapter settings that
/// are valid in both user and project settings.
fn configure_schema_generator(
generator: &mut schemars::SchemaGenerator,
params: &SettingsJsonSchemaParams,
) {
let language_settings_content_ref = generator
.subschema_for::<LanguageSettingsContent>()
.to_value();
if !params.language_names.is_empty() {
replace_subschema::<LanguageToSettingsMap>(generator, || {
json_schema!({
"type": "object",
"errorMessage": "No language with this name is installed.",
"properties": params.language_names.iter().map(|name| (name.clone(), language_settings_content_ref.clone())).collect::<serde_json::Map<_, _>>()
})
});
}
generator.subschema_for::<LspSettings>();
let lsp_settings_definition = generator
.definitions()
.get("LspSettings")
.expect("LspSettings should be defined")
.clone();
if !params.lsp_adapter_names.is_empty() {
replace_subschema::<LspSettingsMap>(generator, || {
let mut lsp_properties = serde_json::Map::new();
for adapter_name in params.lsp_adapter_names {
let mut base_lsp_settings = lsp_settings_definition
.as_object()
.expect("LspSettings should be an object")
.clone();
if let Some(properties) = base_lsp_settings.get_mut("properties") {
if let Some(properties_object) = properties.as_object_mut() {
properties_object.insert(
"initialization_options".to_string(),
serde_json::json!({
"$ref": format!("{LSP_SETTINGS_SCHEMA_URL_PREFIX}{adapter_name}/initialization_options")
}),
);
properties_object.insert(
"settings".to_string(),
serde_json::json!({
"$ref": format!("{LSP_SETTINGS_SCHEMA_URL_PREFIX}{adapter_name}/settings")
}),
);
}
}
lsp_properties.insert(
adapter_name.clone(),
serde_json::Value::Object(base_lsp_settings),
);
}
json_schema!({
"type": "object",
"properties": lsp_properties
})
});
}
}
pub fn json_schema(params: &SettingsJsonSchemaParams) -> Value {
let mut generator = schemars::generate::SchemaSettings::draft2019_09()
.with_transform(DefaultDenyUnknownFields)
.with_transform(AllowTrailingCommas)
.into_generator();
UserSettingsContent::json_schema(&mut generator);
Self::configure_schema_generator(&mut generator, params);
if !params.font_names.is_empty() {
replace_subschema::<FontFamilyName>(&mut generator, || {
json_schema!({
"type": "string",
"enum": params.font_names,
})
});
}
if !params.theme_names.is_empty() {
replace_subschema::<ThemeName>(&mut generator, || {
json_schema!({
"type": "string",
"enum": params.theme_names,
})
});
}
if !params.icon_theme_names.is_empty() {
replace_subschema::<IconThemeName>(&mut generator, || {
json_schema!({
"type": "string",
"enum": params.icon_theme_names,
})
});
}
if !params.action_names.is_empty() {
replace_subschema::<ActionName>(&mut generator, || {
ActionName::build_schema(
params.action_names.iter().copied(),
params.action_documentation,
params.deprecations,
params.deprecation_messages,
)
});
}
generator
.root_schema_for::<UserSettingsContent>()
.to_value()
}
/// Generate JSON schema for project settings, including only settings valid
/// for project-level configurations.
pub fn project_json_schema(params: &SettingsJsonSchemaParams) -> Value {
let mut generator = schemars::generate::SchemaSettings::draft2019_09()
.with_transform(DefaultDenyUnknownFields)
.with_transform(AllowTrailingCommas)
.into_generator();
ProjectSettingsContent::json_schema(&mut generator);
Self::configure_schema_generator(&mut generator, params);
generator
.root_schema_for::<ProjectSettingsContent>()
.to_value()
}
fn recompute_values(
&mut self,
changed_local_path: Option<(WorktreeId, &RelPath)>,
cx: &mut App,
) {
// Reload the global and local values for every setting.
let mut project_settings_stack = Vec::<SettingsContent>::new();
let mut paths_stack = Vec::<Option<(WorktreeId, &RelPath)>>::new();
if changed_local_path.is_none() {
let mut merged = self.default_settings.as_ref().clone();
merged.merge_from_option(self.extension_settings.as_deref());
merged.merge_from_option(self.global_settings.as_deref());
if let Some(user_settings) = self.user_settings.as_ref() {
let active_profile = user_settings.for_profile(cx);
let should_merge_user_settings =
active_profile.is_none_or(|profile| profile.base == ProfileBase::User);
if should_merge_user_settings {
merged.merge_from(&user_settings.content);
merged.merge_from_option(user_settings.for_release_channel());
merged.merge_from_option(user_settings.for_os());
}
if let Some(profile) = active_profile {
merged.merge_from(&profile.settings);
}
}
merged.merge_from_option(self.server_settings.as_deref());
// Merge `disable_ai` from all project/local settings into the global value.
// Since `SaturatingBool` uses OR logic, if any project has `disable_ai: true`,
// the global value will be true. This allows project-level `disable_ai` to
// affect the global setting used by UI elements without file context.
for local_settings in self.local_settings.values() {
merged
.project
.disable_ai
.merge_from(&local_settings.project.disable_ai);
}
self.merged_settings = Rc::new(merged);
for setting_value in self.setting_values.values_mut() {
let value = setting_value.from_settings(&self.merged_settings);
setting_value.set_global_value(value);
}
} else {
// When only a local path changed, we still need to recompute the global
// `disable_ai` value since it depends on all local settings.
let mut merged = (*self.merged_settings).clone();
// Reset disable_ai to compute fresh from base settings
merged.project.disable_ai = self.default_settings.project.disable_ai;
if let Some(global) = &self.global_settings {
merged
.project
.disable_ai
.merge_from(&global.project.disable_ai);
}
if let Some(user) = &self.user_settings {
merged
.project
.disable_ai
.merge_from(&user.content.project.disable_ai);
}
if let Some(server) = &self.server_settings {
merged
.project
.disable_ai
.merge_from(&server.project.disable_ai);
}
for local_settings in self.local_settings.values() {
merged
.project
.disable_ai
.merge_from(&local_settings.project.disable_ai);
}
self.merged_settings = Rc::new(merged);
for setting_value in self.setting_values.values_mut() {
let value = setting_value.from_settings(&self.merged_settings);
setting_value.set_global_value(value);
}
}
for ((root_id, directory_path), local_settings) in &self.local_settings {
// Build a stack of all of the local values for that setting.
while let Some(prev_entry) = paths_stack.last() {
if let Some((prev_root_id, prev_path)) = prev_entry
&& (root_id != prev_root_id || !directory_path.starts_with(prev_path))
{
paths_stack.pop();
project_settings_stack.pop();
continue;
}
break;
}
paths_stack.push(Some((*root_id, directory_path.as_ref())));
let mut merged_local_settings = if let Some(deepest) = project_settings_stack.last() {
(*deepest).clone()
} else {
self.merged_settings.as_ref().clone()
};
merged_local_settings.merge_from(local_settings);
project_settings_stack.push(merged_local_settings);
// If a local settings file changed, then avoid recomputing local
// settings for any path outside of that directory.
if changed_local_path.is_some_and(|(changed_root_id, changed_local_path)| {
*root_id != changed_root_id || !directory_path.starts_with(changed_local_path)
}) {
continue;
}
for setting_value in self.setting_values.values_mut() {
let value = setting_value.from_settings(&project_settings_stack.last().unwrap());
setting_value.set_local_value(*root_id, directory_path.clone(), value);
}
}
}
}
/// The result of parsing settings, including any migration attempts
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SettingsParseResult {
/// The result of parsing the settings file (possibly after migration)
pub parse_status: ParseStatus,
/// The result of attempting to migrate the settings file
pub migration_status: MigrationStatus,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MigrationStatus {
/// No migration was needed - settings are up to date
NotNeeded,
/// Settings were automatically migrated in memory, but the file needs to be updated
Succeeded,
/// Migration was attempted but failed. Original settings were parsed instead.
Failed { error: String },
}
impl Default for SettingsParseResult {
fn default() -> Self {
Self {
parse_status: ParseStatus::Success,
migration_status: MigrationStatus::NotNeeded,
}
}
}
impl SettingsParseResult {
pub fn unwrap(self) -> bool {
self.result().unwrap()
}
pub fn expect(self, message: &str) -> bool {
self.result().expect(message)
}
/// Formats the ParseResult as a Result type. This is a lossy conversion
pub fn result(self) -> Result<bool> {
let migration_result = match self.migration_status {
MigrationStatus::NotNeeded => Ok(false),
MigrationStatus::Succeeded => Ok(true),
MigrationStatus::Failed { error } => {
Err(anyhow::format_err!(error)).context("Failed to migrate settings")
}
};
let parse_result = match self.parse_status {
ParseStatus::Success | ParseStatus::Unchanged => Ok(()),
ParseStatus::Failed { error } => {
Err(anyhow::format_err!(error)).context("Failed to parse settings")
}
};
match (migration_result, parse_result) {
(migration_result @ Ok(_), Ok(())) => migration_result,
(Err(migration_err), Ok(())) => Err(migration_err),
(_, Err(parse_err)) => Err(parse_err),
}
}
/// Returns true if there were any errors migrating and parsing the settings content or if migration was required but there were no errors
pub fn requires_user_action(&self) -> bool {
matches!(self.parse_status, ParseStatus::Failed { .. })
|| matches!(
self.migration_status,
MigrationStatus::Succeeded | MigrationStatus::Failed { .. }
)
}
pub fn ok(self) -> Option<bool> {
self.result().ok()
}
pub fn parse_error(&self) -> Option<String> {
match &self.parse_status {
ParseStatus::Failed { error } => Some(error.clone()),
ParseStatus::Success | ParseStatus::Unchanged => None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum InvalidSettingsError {
LocalSettings {
path: Arc<RelPath>,
message: String,
},
UserSettings {
message: String,
},
ServerSettings {
message: String,
},
DefaultSettings {
message: String,
},
Editorconfig {
path: LocalSettingsPath,
message: String,
},
Tasks {
path: PathBuf,
message: String,
},
Debug {
path: PathBuf,
message: String,
},
}
impl std::fmt::Display for InvalidSettingsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
InvalidSettingsError::LocalSettings { message, .. }
| InvalidSettingsError::UserSettings { message }
| InvalidSettingsError::ServerSettings { message }
| InvalidSettingsError::DefaultSettings { message }
| InvalidSettingsError::Tasks { message, .. }
| InvalidSettingsError::Editorconfig { message, .. }
| InvalidSettingsError::Debug { message, .. } => write!(f, "{message}"),
}
}
}
impl std::error::Error for InvalidSettingsError {}
impl Debug for SettingsStore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SettingsStore")
.field(
"types",
&self
.setting_values
.values()
.map(|value| value.setting_type_name())
.collect::<Vec<_>>(),
)
.field("default_settings", &self.default_settings)
.field("user_settings", &self.user_settings)
.field("local_settings", &self.local_settings)
.finish_non_exhaustive()
}
}
impl<T: Settings> AnySettingValue for SettingValue<T> {
fn from_settings(&self, s: &SettingsContent) -> Box<dyn Any> {
Box::new(T::from_settings(s)) as _
}
fn setting_type_name(&self) -> &'static str {
type_name::<T>()
}
fn all_local_values(&self) -> Vec<(WorktreeId, Arc<RelPath>, &dyn Any)> {
self.local_values
.iter()
.map(|(id, path, value)| (*id, path.clone(), value as _))
.collect()
}
fn value_for_path(&self, path: Option<SettingsLocation>) -> &dyn Any {
if let Some(SettingsLocation { worktree_id, path }) = path {
for (settings_root_id, settings_path, value) in self.local_values.iter().rev() {
if worktree_id == *settings_root_id && path.starts_with(settings_path) {
return value;
}
}
}
self.global_value
.as_ref()
.unwrap_or_else(|| panic!("no default value for setting {}", self.setting_type_name()))
}
fn set_global_value(&mut self, value: Box<dyn Any>) {
self.global_value = Some(*value.downcast().unwrap());
}
fn set_local_value(&mut self, root_id: WorktreeId, path: Arc<RelPath>, value: Box<dyn Any>) {
let value = *value.downcast().unwrap();
match self
.local_values
.binary_search_by_key(&(root_id, &path), |e| (e.0, &e.1))
{
Ok(ix) => self.local_values[ix].2 = value,
Err(ix) => self.local_values.insert(ix, (root_id, path, value)),
}
}
fn clear_local_values(&mut self, root_id: WorktreeId) {
self.local_values
.retain(|(worktree_id, _, _)| *worktree_id != root_id);
}
}
#[cfg(test)]
mod tests {
use std::{cell::RefCell, num::NonZeroU32};
use crate::{
ClosePosition, ItemSettingsContent, VsCodeSettingsSource, default_settings,
settings_content::LanguageSettingsContent, test_settings,
};
use super::*;
use fs::FakeFs;
use unindent::Unindent;
use util::rel_path::rel_path;
#[derive(Debug, PartialEq)]
struct AutoUpdateSetting {
auto_update: bool,
}
impl Settings for AutoUpdateSetting {
fn from_settings(content: &SettingsContent) -> Self {
AutoUpdateSetting {
auto_update: content.auto_update.unwrap(),
}
}
}
#[derive(Debug, PartialEq)]
struct ItemSettings {
close_position: ClosePosition,
git_status: bool,
}
impl Settings for ItemSettings {
fn from_settings(content: &SettingsContent) -> Self {
let content = content.tabs.clone().unwrap();
ItemSettings {
close_position: content.close_position.unwrap(),
git_status: content.git_status.unwrap(),
}
}
}
#[derive(Debug, PartialEq)]
struct DefaultLanguageSettings {
tab_size: NonZeroU32,
preferred_line_length: u32,
}
impl Settings for DefaultLanguageSettings {
fn from_settings(content: &SettingsContent) -> Self {
let content = &content.project.all_languages.defaults;
DefaultLanguageSettings {
tab_size: content.tab_size.unwrap(),
preferred_line_length: content.preferred_line_length.unwrap(),
}
}
}
#[derive(Debug, PartialEq)]
struct ThemeSettings {
buffer_font_family: FontFamilyName,
buffer_font_fallbacks: Vec<FontFamilyName>,
}
impl Settings for ThemeSettings {
fn from_settings(content: &SettingsContent) -> Self {
let content = content.theme.clone();
ThemeSettings {
buffer_font_family: content.buffer_font_family.unwrap(),
buffer_font_fallbacks: content.buffer_font_fallbacks.unwrap(),
}
}
}
#[gpui::test]
async fn test_update_settings_file_updates_store_before_watcher(cx: &mut gpui::TestAppContext) {
let fs = FakeFs::new(cx.background_executor.clone());
fs.create_dir(paths::settings_file().parent().unwrap())
.await
.unwrap();
fs.insert_file(
paths::settings_file(),
r#"{ "tabs": { "close_position": "right" } }"#.as_bytes().to_vec(),
)
.await;
fs.pause_events();
cx.run_until_parked();
let success = SettingsParseResult {
parse_status: ParseStatus::Success,
migration_status: MigrationStatus::NotNeeded,
};
let parse_results = Rc::new(RefCell::new(Vec::new()));
cx.update(|cx| {
let mut store = SettingsStore::new(cx, &default_settings());
store.register_setting::<ItemSettings>();
store.watch_settings_files(fs.clone(), cx, {
let parse_results = parse_results.clone();
move |_, result, _| {
parse_results.borrow_mut().push(result);
}
});
cx.set_global(store);
});
// Calling watch_settings_files loads user and global settings.
assert_eq!(
parse_results.borrow().as_slice(),
&[success.clone(), success.clone()]
);
cx.update(|cx| {
assert_eq!(
cx.global::<SettingsStore>()
.get::<ItemSettings>(None)
.close_position,
ClosePosition::Right
);
});
// Updating the settings file returns a channel that resolves once the settings are loaded.
let rx = cx.update(|cx| {
cx.global::<SettingsStore>()
.update_settings_file_with_completion(fs.clone(), move |settings, _| {
settings.tabs.get_or_insert_default().close_position =
Some(ClosePosition::Left);
})
});
assert!(rx.await.unwrap().is_ok());
assert_eq!(
parse_results.borrow().as_slice(),
&[success.clone(), success.clone()]
);
cx.update(|cx| {
assert_eq!(
cx.global::<SettingsStore>()
.get::<ItemSettings>(None)
.close_position,
ClosePosition::Left
);
});
// When the FS event occurs, the settings are recognized as unchanged.
fs.flush_events(100);
cx.run_until_parked();
assert_eq!(
parse_results.borrow().as_slice(),
&[
success.clone(),
success.clone(),
SettingsParseResult {
parse_status: ParseStatus::Unchanged,
migration_status: MigrationStatus::NotNeeded
}
]
);
}
#[gpui::test]
fn test_default_settings_release_channel_overrides(cx: &mut App) {
assert_eq!(
*release_channel::RELEASE_CHANNEL,
release_channel::ReleaseChannel::Dev,
"tests expect the dev release channel",
);
let mut defaults: serde_json::Value =
crate::parse_json_with_comments(&default_settings()).unwrap();
let root = defaults
.as_object_mut()
.expect("default settings must be a JSON object");
root.insert("dev".into(), serde_json::json!({ "auto_update": false }));
root.insert("stable".into(), serde_json::json!({ "auto_update": true }));
let defaults_with_overrides = serde_json::to_string(&defaults).unwrap();
let mut store = SettingsStore::new(cx, &defaults_with_overrides);
store.register_setting::<AutoUpdateSetting>();
assert_eq!(
store.get::<AutoUpdateSetting>(None),
&AutoUpdateSetting { auto_update: false },
"dev override from default settings should apply",
);
}
#[gpui::test]
fn test_settings_store_basic(cx: &mut App) {
let mut store = SettingsStore::new(cx, &default_settings());
store.register_setting::<AutoUpdateSetting>();
store.register_setting::<ItemSettings>();
store.register_setting::<DefaultLanguageSettings>();
assert_eq!(
store.get::<AutoUpdateSetting>(None),
&AutoUpdateSetting { auto_update: true }
);
assert_eq!(
store.get::<ItemSettings>(None).close_position,
ClosePosition::Right
);
store
.set_user_settings(
r#"{
"auto_update": false,
"tabs": {
"close_position": "left"
}
}"#,
cx,
)
.unwrap();
assert_eq!(
store.get::<AutoUpdateSetting>(None),
&AutoUpdateSetting { auto_update: false }
);
assert_eq!(
store.get::<ItemSettings>(None).close_position,
ClosePosition::Left
);
store
.set_local_settings(
WorktreeId::from_usize(1),
LocalSettingsPath::InWorktree(rel_path("root1").into()),
LocalSettingsKind::Settings,
Some(r#"{ "tab_size": 5 }"#),
cx,
)
.unwrap();
store
.set_local_settings(
WorktreeId::from_usize(1),
LocalSettingsPath::InWorktree(rel_path("root1/subdir").into()),
LocalSettingsKind::Settings,
Some(r#"{ "preferred_line_length": 50 }"#),
cx,
)
.unwrap();
store
.set_local_settings(
WorktreeId::from_usize(1),
LocalSettingsPath::InWorktree(rel_path("root2").into()),
LocalSettingsKind::Settings,
Some(r#"{ "tab_size": 9, "auto_update": true}"#),
cx,
)
.unwrap();
assert_eq!(
store.get::<DefaultLanguageSettings>(Some(SettingsLocation {
worktree_id: WorktreeId::from_usize(1),
path: rel_path("root1/something"),
})),
&DefaultLanguageSettings {
preferred_line_length: 80,
tab_size: 5.try_into().unwrap(),
}
);
assert_eq!(
store.get::<DefaultLanguageSettings>(Some(SettingsLocation {
worktree_id: WorktreeId::from_usize(1),
path: rel_path("root1/subdir/something"),
})),
&DefaultLanguageSettings {
preferred_line_length: 50,
tab_size: 5.try_into().unwrap(),
}
);
assert_eq!(
store.get::<DefaultLanguageSettings>(Some(SettingsLocation {
worktree_id: WorktreeId::from_usize(1),
path: rel_path("root2/something"),
})),
&DefaultLanguageSettings {
preferred_line_length: 80,
tab_size: 9.try_into().unwrap(),
}
);
assert_eq!(
store.get::<AutoUpdateSetting>(Some(SettingsLocation {
worktree_id: WorktreeId::from_usize(1),
path: rel_path("root2/something")
})),
&AutoUpdateSetting { auto_update: false }
);
}
#[gpui::test]
fn test_setting_store_assign_json_before_register(cx: &mut App) {
let mut store = SettingsStore::new(cx, &test_settings());
store
.set_user_settings(r#"{ "auto_update": false }"#, cx)
.unwrap();
store.register_setting::<AutoUpdateSetting>();
assert_eq!(
store.get::<AutoUpdateSetting>(None),
&AutoUpdateSetting { auto_update: false }
);
}
#[track_caller]
fn check_settings_update(
store: &mut SettingsStore,
old_json: String,
update: fn(&mut SettingsContent),
expected_new_json: String,
cx: &mut App,
) {
store.set_user_settings(&old_json, cx).ok();
let edits = store.edits_for_update(&old_json, update).unwrap();
let mut new_json = old_json;
for (range, replacement) in edits.into_iter() {
new_json.replace_range(range, &replacement);
}
pretty_assertions::assert_eq!(new_json, expected_new_json);
}
#[gpui::test]
fn test_setting_store_update(cx: &mut App) {
let mut store = SettingsStore::new(cx, &test_settings());
// entries added and updated
check_settings_update(
&mut store,
r#"{
"languages": {
"JSON": {
"auto_indent": "syntax_aware"
}
}
}"#
.unindent(),
|settings| {
settings
.languages_mut()
.get_mut("JSON")
.unwrap()
.auto_indent = Some(crate::AutoIndentMode::None);
settings.languages_mut().insert(
"Rust".into(),
LanguageSettingsContent {
auto_indent: Some(crate::AutoIndentMode::SyntaxAware),
..Default::default()
},
);
},
r#"{
"languages": {
"Rust": {
"auto_indent": "syntax_aware"
},
"JSON": {
"auto_indent": "none"
}
}
}"#
.unindent(),
cx,
);
// entries removed
check_settings_update(
&mut store,
r#"{
"languages": {
"Rust": {
"language_setting_2": true
},
"JSON": {
"language_setting_1": false
}
}
}"#
.unindent(),
|settings| {
settings.languages_mut().remove("JSON").unwrap();
},
r#"{
"languages": {
"Rust": {
"language_setting_2": true
}
}
}"#
.unindent(),
cx,
);
check_settings_update(
&mut store,
r#"{
"languages": {
"Rust": {
"language_setting_2": true
},
"JSON": {
"language_setting_1": false
}
}
}"#
.unindent(),
|settings| {
settings.languages_mut().remove("Rust").unwrap();
},
r#"{
"languages": {
"JSON": {
"language_setting_1": false
}
}
}"#
.unindent(),
cx,
);
// weird formatting
check_settings_update(
&mut store,
r#"{
"tabs": { "close_position": "left", "name": "Max" }
}"#
.unindent(),
|settings| {
settings.tabs.as_mut().unwrap().close_position = Some(ClosePosition::Left);
},
r#"{
"tabs": { "close_position": "left", "name": "Max" }
}"#
.unindent(),
cx,
);
// single-line formatting, other keys
check_settings_update(
&mut store,
r#"{ "one": 1, "two": 2 }"#.to_owned(),
|settings| settings.auto_update = Some(true),
r#"{ "auto_update": true, "one": 1, "two": 2 }"#.to_owned(),
cx,
);
// empty object
check_settings_update(
&mut store,
r#"{
"tabs": {}
}"#
.unindent(),
|settings| settings.tabs.as_mut().unwrap().close_position = Some(ClosePosition::Left),
r#"{
"tabs": {
"close_position": "left"
}
}"#
.unindent(),
cx,
);
// no content
check_settings_update(
&mut store,
r#""#.unindent(),
|settings| {
settings.tabs = Some(ItemSettingsContent {
git_status: Some(true),
..Default::default()
})
},
r#"{
"tabs": {
"git_status": true
}
}
"#
.unindent(),
cx,
);
check_settings_update(
&mut store,
r#"{
}
"#
.unindent(),
|settings| settings.title_bar.get_or_insert_default().show_branch_name = Some(true),
r#"{
"title_bar": {
"show_branch_name": true
}
}
"#
.unindent(),
cx,
);
}
#[gpui::test]
fn test_edits_for_update_preserves_unknown_keys(cx: &mut App) {
let mut store = SettingsStore::new(cx, &test_settings());
store.register_setting::<AutoUpdateSetting>();
let old_json = r#"{
"some_unknown_key": "should_be_preserved",
"auto_update": false
}"#
.unindent();
check_settings_update(
&mut store,
old_json,
|settings| settings.auto_update = Some(true),
r#"{
"some_unknown_key": "should_be_preserved",
"auto_update": true
}"#
.unindent(),
cx,
);
}
#[gpui::test]
fn test_edits_for_update_returns_error_on_invalid_json(cx: &mut App) {
let store = SettingsStore::new(cx, &test_settings());
let invalid_json = r#"{ this is not valid json at all !!!"#;
let result = store.edits_for_update(invalid_json, |_| {});
assert!(result.is_err());
}
#[gpui::test]
fn test_vscode_import(cx: &mut App) {
let mut store = SettingsStore::new(cx, &test_settings());
store.register_setting::<DefaultLanguageSettings>();
store.register_setting::<ItemSettings>();
store.register_setting::<AutoUpdateSetting>();
store.register_setting::<ThemeSettings>();
// create settings that werent present
check_vscode_import(
&mut store,
r#"{
}
"#
.unindent(),
r#" { "editor.tabSize": 37 } "#.to_owned(),
r#"{
"base_keymap": "VSCode",
"tab_size": 37
}
"#
.unindent(),
cx,
);
// persist settings that were present
check_vscode_import(
&mut store,
r#"{
"preferred_line_length": 99,
}
"#
.unindent(),
r#"{ "editor.tabSize": 42 }"#.to_owned(),
r#"{
"base_keymap": "VSCode",
"tab_size": 42,
"preferred_line_length": 99,
}
"#
.unindent(),
cx,
);
// don't clobber settings that aren't present in vscode
check_vscode_import(
&mut store,
r#"{
"preferred_line_length": 99,
"tab_size": 42
}
"#
.unindent(),
r#"{}"#.to_owned(),
r#"{
"base_keymap": "VSCode",
"preferred_line_length": 99,
"tab_size": 42
}
"#
.unindent(),
cx,
);
// custom enum
check_vscode_import(
&mut store,
r#"{
}
"#
.unindent(),
r#"{ "git.decorations.enabled": true }"#.to_owned(),
r#"{
"project_panel": {
"git_status": true
},
"outline_panel": {
"git_status": true
},
"base_keymap": "VSCode",
"tabs": {
"git_status": true
}
}
"#
.unindent(),
cx,
);
// explorer sort settings
check_vscode_import(
&mut store,
r#"{
}
"#
.unindent(),
r#"{
"explorer.sortOrder": "mixed",
"explorer.sortOrderLexicographicOptions": "lower"
}"#
.unindent(),
r#"{
"project_panel": {
"sort_mode": "mixed",
"sort_order": "lower"
},
"base_keymap": "VSCode"
}
"#
.unindent(),
cx,
);
// font-family
check_vscode_import(
&mut store,
r#"{
}
"#
.unindent(),
r#"{ "editor.fontFamily": "Cascadia Code, 'Consolas', Courier New" }"#.to_owned(),
r#"{
"base_keymap": "VSCode",
"buffer_font_fallbacks": [
"Consolas",
"Courier New"
],
"buffer_font_family": "Cascadia Code"
}
"#
.unindent(),
cx,
);
// terminal bell settings - newer accessibility setting
check_vscode_import(
&mut store,
r#"{
}
"#
.unindent(),
r#"{ "accessibility.signals.terminalBell": { "sound": "on" } }"#.to_owned(),
r#"{
"terminal": {
"bell": "system"
},
"base_keymap": "VSCode"
}
"#
.unindent(),
cx,
);
// terminal bell settings - newer accessibility setting disabled
check_vscode_import(
&mut store,
r#"{
}
"#
.unindent(),
r#"{ "accessibility.signals.terminalBell": { "sound": "off" } }"#.to_owned(),
r#"{
"terminal": {
"bell": "off"
},
"base_keymap": "VSCode"
}
"#
.unindent(),
cx,
);
// terminal bell settings - older enableBell setting (true)
check_vscode_import(
&mut store,
r#"{
}
"#
.unindent(),
r#"{ "terminal.integrated.enableBell": true }"#.to_owned(),
r#"{
"terminal": {
"bell": "system"
},
"base_keymap": "VSCode"
}
"#
.unindent(),
cx,
);
// terminal bell settings - older enableBell setting (false)
check_vscode_import(
&mut store,
r#"{
}
"#
.unindent(),
r#"{ "terminal.integrated.enableBell": false }"#.to_owned(),
r#"{
"terminal": {
"bell": "off"
},
"base_keymap": "VSCode"
}
"#
.unindent(),
cx,
);
// newer accessibility setting takes precedence over older enableBell
check_vscode_import(
&mut store,
r#"{
}
"#
.unindent(),
r#"{
"accessibility.signals.terminalBell": { "sound": "off" },
"terminal.integrated.enableBell": true
}"#
.to_owned(),
r#"{
"terminal": {
"bell": "off"
},
"base_keymap": "VSCode"
}
"#
.unindent(),
cx,
);
// hover sticky settings
check_vscode_import(
&mut store,
r#"{
}
"#
.unindent(),
r#"{
"editor.hover.sticky": false,
"editor.hover.hidingDelay": 500
}"#
.to_owned(),
r#"{
"base_keymap": "VSCode",
"hover_popover_hiding_delay": 500,
"hover_popover_sticky": false
}
"#
.unindent(),
cx,
);
}
#[track_caller]
fn check_vscode_import(
store: &mut SettingsStore,
old: String,
vscode: String,
expected: String,
cx: &mut App,
) {
store.set_user_settings(&old, cx).ok();
let new = store
.get_vscode_edits(
old,
&VsCodeSettings::from_str(&vscode, VsCodeSettingsSource::VsCode).unwrap(),
)
.unwrap();
pretty_assertions::assert_eq!(new, expected);
}
#[gpui::test]
fn test_update_git_settings(cx: &mut App) {
let store = SettingsStore::new(cx, &test_settings());
let actual = store
.new_text_for_update("{}".to_string(), |current| {
current
.git
.get_or_insert_default()
.inline_blame
.get_or_insert_default()
.enabled = Some(true);
})
.unwrap();
pretty_assertions::assert_str_eq!(
actual,
r#"{
"git": {
"inline_blame": {
"enabled": true
}
}
}
"#
.unindent()
);
}
#[gpui::test]
fn test_global_settings(cx: &mut App) {
let mut store = SettingsStore::new(cx, &test_settings());
store.register_setting::<ItemSettings>();
// Set global settings - these should override defaults but not user settings
store
.set_global_settings(
r#"{
"tabs": {
"close_position": "right",
"git_status": true,
}
}"#,
cx,
)
.unwrap();
// Before user settings, global settings should apply
assert_eq!(
store.get::<ItemSettings>(None),
&ItemSettings {
close_position: ClosePosition::Right,
git_status: true,
}
);
// Set user settings - these should override both defaults and global
store
.set_user_settings(
r#"{
"tabs": {
"close_position": "left"
}
}"#,
cx,
)
.unwrap();
// User settings should override global settings
assert_eq!(
store.get::<ItemSettings>(None),
&ItemSettings {
close_position: ClosePosition::Left,
git_status: true, // Staff from global settings
}
);
}
#[gpui::test]
fn test_get_value_for_field_basic(cx: &mut App) {
let mut store = SettingsStore::new(cx, &test_settings());
store.register_setting::<DefaultLanguageSettings>();
store
.set_user_settings(r#"{"preferred_line_length": 0}"#, cx)
.unwrap();
let local = (WorktreeId::from_usize(0), RelPath::empty().into_arc());
store
.set_local_settings(
local.0,
LocalSettingsPath::InWorktree(local.1.clone()),
LocalSettingsKind::Settings,
Some(r#"{}"#),
cx,
)
.unwrap();
fn get(content: &SettingsContent) -> Option<&u32> {
content
.project
.all_languages
.defaults
.preferred_line_length
.as_ref()
}
let default_value = *get(&store.default_settings).unwrap();
assert_eq!(
store.get_value_from_file(SettingsFile::Project(local.clone()), get),
(SettingsFile::User, Some(&0))
);
assert_eq!(
store.get_value_from_file(SettingsFile::User, get),
(SettingsFile::User, Some(&0))
);
store.set_user_settings(r#"{}"#, cx).unwrap();
assert_eq!(
store.get_value_from_file(SettingsFile::Project(local.clone()), get),
(SettingsFile::Default, Some(&default_value))
);
store
.set_local_settings(
local.0,
LocalSettingsPath::InWorktree(local.1.clone()),
LocalSettingsKind::Settings,
Some(r#"{"preferred_line_length": 80}"#),
cx,
)
.unwrap();
assert_eq!(
store.get_value_from_file(SettingsFile::Project(local.clone()), get),
(SettingsFile::Project(local), Some(&80))
);
assert_eq!(
store.get_value_from_file(SettingsFile::User, get),
(SettingsFile::Default, Some(&default_value))
);
}
#[gpui::test]
fn test_get_value_for_field_local_worktrees_dont_interfere(cx: &mut App) {
let mut store = SettingsStore::new(cx, &test_settings());
store.register_setting::<DefaultLanguageSettings>();
store.register_setting::<AutoUpdateSetting>();
let local_1 = (WorktreeId::from_usize(0), RelPath::empty().into_arc());
let local_1_child = (
WorktreeId::from_usize(0),
RelPath::new(
std::path::Path::new("child1"),
util::paths::PathStyle::Posix,
)
.unwrap()
.into_arc(),
);
let local_2 = (WorktreeId::from_usize(1), RelPath::empty().into_arc());
let local_2_child = (
WorktreeId::from_usize(1),
RelPath::new(
std::path::Path::new("child2"),
util::paths::PathStyle::Posix,
)
.unwrap()
.into_arc(),
);
fn get(content: &SettingsContent) -> Option<&u32> {
content
.project
.all_languages
.defaults
.preferred_line_length
.as_ref()
}
store
.set_local_settings(
local_1.0,
LocalSettingsPath::InWorktree(local_1.1.clone()),
LocalSettingsKind::Settings,
Some(r#"{"preferred_line_length": 1}"#),
cx,
)
.unwrap();
store
.set_local_settings(
local_1_child.0,
LocalSettingsPath::InWorktree(local_1_child.1.clone()),
LocalSettingsKind::Settings,
Some(r#"{}"#),
cx,
)
.unwrap();
store
.set_local_settings(
local_2.0,
LocalSettingsPath::InWorktree(local_2.1.clone()),
LocalSettingsKind::Settings,
Some(r#"{"preferred_line_length": 2}"#),
cx,
)
.unwrap();
store
.set_local_settings(
local_2_child.0,
LocalSettingsPath::InWorktree(local_2_child.1.clone()),
LocalSettingsKind::Settings,
Some(r#"{}"#),
cx,
)
.unwrap();
// each local child should only inherit from it's parent
assert_eq!(
store.get_value_from_file(SettingsFile::Project(local_2_child), get),
(SettingsFile::Project(local_2), Some(&2))
);
assert_eq!(
store.get_value_from_file(SettingsFile::Project(local_1_child.clone()), get),
(SettingsFile::Project(local_1.clone()), Some(&1))
);
// adjacent children should be treated as siblings not inherit from each other
let local_1_adjacent_child = (local_1.0, rel_path("adjacent_child").into_arc());
store
.set_local_settings(
local_1_adjacent_child.0,
LocalSettingsPath::InWorktree(local_1_adjacent_child.1.clone()),
LocalSettingsKind::Settings,
Some(r#"{}"#),
cx,
)
.unwrap();
store
.set_local_settings(
local_1_child.0,
LocalSettingsPath::InWorktree(local_1_child.1.clone()),
LocalSettingsKind::Settings,
Some(r#"{"preferred_line_length": 3}"#),
cx,
)
.unwrap();
assert_eq!(
store.get_value_from_file(SettingsFile::Project(local_1_adjacent_child.clone()), get),
(SettingsFile::Project(local_1.clone()), Some(&1))
);
store
.set_local_settings(
local_1_adjacent_child.0,
LocalSettingsPath::InWorktree(local_1_adjacent_child.1),
LocalSettingsKind::Settings,
Some(r#"{"preferred_line_length": 3}"#),
cx,
)
.unwrap();
store
.set_local_settings(
local_1_child.0,
LocalSettingsPath::InWorktree(local_1_child.1.clone()),
LocalSettingsKind::Settings,
Some(r#"{}"#),
cx,
)
.unwrap();
assert_eq!(
store.get_value_from_file(SettingsFile::Project(local_1_child), get),
(SettingsFile::Project(local_1), Some(&1))
);
}
#[gpui::test]
fn test_get_overrides_for_field(cx: &mut App) {
let mut store = SettingsStore::new(cx, &test_settings());
store.register_setting::<DefaultLanguageSettings>();
let wt0_root = (WorktreeId::from_usize(0), RelPath::empty().into_arc());
let wt0_child1 = (WorktreeId::from_usize(0), rel_path("child1").into_arc());
let wt0_child2 = (WorktreeId::from_usize(0), rel_path("child2").into_arc());
let wt1_root = (WorktreeId::from_usize(1), RelPath::empty().into_arc());
let wt1_subdir = (WorktreeId::from_usize(1), rel_path("subdir").into_arc());
fn get(content: &SettingsContent) -> &Option<u32> {
&content.project.all_languages.defaults.preferred_line_length
}
store
.set_user_settings(r#"{"preferred_line_length": 100}"#, cx)
.unwrap();
store
.set_local_settings(
wt0_root.0,
LocalSettingsPath::InWorktree(wt0_root.1.clone()),
LocalSettingsKind::Settings,
Some(r#"{"preferred_line_length": 80}"#),
cx,
)
.unwrap();
store
.set_local_settings(
wt0_child1.0,
LocalSettingsPath::InWorktree(wt0_child1.1.clone()),
LocalSettingsKind::Settings,
Some(r#"{"preferred_line_length": 120}"#),
cx,
)
.unwrap();
store
.set_local_settings(
wt0_child2.0,
LocalSettingsPath::InWorktree(wt0_child2.1.clone()),
LocalSettingsKind::Settings,
Some(r#"{}"#),
cx,
)
.unwrap();
store
.set_local_settings(
wt1_root.0,
LocalSettingsPath::InWorktree(wt1_root.1.clone()),
LocalSettingsKind::Settings,
Some(r#"{"preferred_line_length": 90}"#),
cx,
)
.unwrap();
store
.set_local_settings(
wt1_subdir.0,
LocalSettingsPath::InWorktree(wt1_subdir.1.clone()),
LocalSettingsKind::Settings,
Some(r#"{}"#),
cx,
)
.unwrap();
let overrides = store.get_overrides_for_field(SettingsFile::Default, get);
assert_eq!(
overrides,
vec![
SettingsFile::User,
SettingsFile::Project(wt0_root.clone()),
SettingsFile::Project(wt0_child1.clone()),
SettingsFile::Project(wt1_root.clone()),
]
);
let overrides = store.get_overrides_for_field(SettingsFile::User, get);
assert_eq!(
overrides,
vec![
SettingsFile::Project(wt0_root.clone()),
SettingsFile::Project(wt0_child1.clone()),
SettingsFile::Project(wt1_root.clone()),
]
);
let overrides = store.get_overrides_for_field(SettingsFile::Project(wt0_root), get);
assert_eq!(overrides, vec![]);
let overrides =
store.get_overrides_for_field(SettingsFile::Project(wt0_child1.clone()), get);
assert_eq!(overrides, vec![]);
let overrides = store.get_overrides_for_field(SettingsFile::Project(wt0_child2), get);
assert_eq!(overrides, vec![]);
let overrides = store.get_overrides_for_field(SettingsFile::Project(wt1_root), get);
assert_eq!(overrides, vec![]);
let overrides = store.get_overrides_for_field(SettingsFile::Project(wt1_subdir), get);
assert_eq!(overrides, vec![]);
let wt0_deep_child = (
WorktreeId::from_usize(0),
rel_path("child1/subdir").into_arc(),
);
store
.set_local_settings(
wt0_deep_child.0,
LocalSettingsPath::InWorktree(wt0_deep_child.1.clone()),
LocalSettingsKind::Settings,
Some(r#"{"preferred_line_length": 140}"#),
cx,
)
.unwrap();
let overrides = store.get_overrides_for_field(SettingsFile::Project(wt0_deep_child), get);
assert_eq!(overrides, vec![]);
let overrides = store.get_overrides_for_field(SettingsFile::Project(wt0_child1), get);
assert_eq!(overrides, vec![]);
}
#[test]
fn test_file_ord() {
let wt0_root =
SettingsFile::Project((WorktreeId::from_usize(0), RelPath::empty().into_arc()));
let wt0_child1 =
SettingsFile::Project((WorktreeId::from_usize(0), rel_path("child1").into_arc()));
let wt0_child2 =
SettingsFile::Project((WorktreeId::from_usize(0), rel_path("child2").into_arc()));
let wt1_root =
SettingsFile::Project((WorktreeId::from_usize(1), RelPath::empty().into_arc()));
let wt1_subdir =
SettingsFile::Project((WorktreeId::from_usize(1), rel_path("subdir").into_arc()));
let mut files = vec![
&wt1_root,
&SettingsFile::Default,
&wt0_root,
&wt1_subdir,
&wt0_child2,
&SettingsFile::Server,
&wt0_child1,
&SettingsFile::User,
];
files.sort();
pretty_assertions::assert_eq!(
files,
vec![
&wt0_child2,
&wt0_child1,
&wt0_root,
&wt1_subdir,
&wt1_root,
&SettingsFile::Server,
&SettingsFile::User,
&SettingsFile::Default,
]
)
}
#[gpui::test]
fn test_lsp_settings_schema_generation(cx: &mut App) {
SettingsStore::test(cx);
let schema = SettingsStore::json_schema(&SettingsJsonSchemaParams {
language_names: &["Rust".to_string(), "TypeScript".to_string()],
font_names: &["Zed Mono".to_string()],
theme_names: &["One Dark".into()],
icon_theme_names: &["Zed Icons".into()],
lsp_adapter_names: &[
"rust-analyzer".to_string(),
"typescript-language-server".to_string(),
],
action_names: &[],
action_documentation: &HashMap::default(),
deprecations: &HashMap::default(),
deprecation_messages: &HashMap::default(),
});
let properties = schema
.pointer("/$defs/LspSettingsMap/properties")
.expect("LspSettingsMap should have properties")
.as_object()
.unwrap();
assert!(properties.contains_key("rust-analyzer"));
assert!(properties.contains_key("typescript-language-server"));
let init_options_ref = properties
.get("rust-analyzer")
.unwrap()
.pointer("/properties/initialization_options/$ref")
.expect("initialization_options should have a $ref")
.as_str()
.unwrap();
assert_eq!(
init_options_ref,
"zed://schemas/settings/lsp/rust-analyzer/initialization_options"
);
let settings_ref = properties
.get("rust-analyzer")
.unwrap()
.pointer("/properties/settings/$ref")
.expect("settings should have a $ref")
.as_str()
.unwrap();
assert_eq!(
settings_ref,
"zed://schemas/settings/lsp/rust-analyzer/settings"
);
}
#[gpui::test]
fn test_lsp_project_settings_schema_generation(cx: &mut App) {
SettingsStore::test(cx);
let schema = SettingsStore::project_json_schema(&SettingsJsonSchemaParams {
language_names: &["Rust".to_string(), "TypeScript".to_string()],
font_names: &["Zed Mono".to_string()],
theme_names: &["One Dark".into()],
icon_theme_names: &["Zed Icons".into()],
lsp_adapter_names: &[
"rust-analyzer".to_string(),
"typescript-language-server".to_string(),
],
action_names: &[],
action_documentation: &HashMap::default(),
deprecations: &HashMap::default(),
deprecation_messages: &HashMap::default(),
});
let properties = schema
.pointer("/$defs/LspSettingsMap/properties")
.expect("LspSettingsMap should have properties")
.as_object()
.unwrap();
assert!(properties.contains_key("rust-analyzer"));
assert!(properties.contains_key("typescript-language-server"));
let init_options_ref = properties
.get("rust-analyzer")
.unwrap()
.pointer("/properties/initialization_options/$ref")
.expect("initialization_options should have a $ref")
.as_str()
.unwrap();
assert_eq!(
init_options_ref,
"zed://schemas/settings/lsp/rust-analyzer/initialization_options"
);
let settings_ref = properties
.get("rust-analyzer")
.unwrap()
.pointer("/properties/settings/$ref")
.expect("settings should have a $ref")
.as_str()
.unwrap();
assert_eq!(
settings_ref,
"zed://schemas/settings/lsp/rust-analyzer/settings"
);
}
#[gpui::test]
fn test_project_json_schema_differs_from_user_schema(cx: &mut App) {
SettingsStore::test(cx);
let params = SettingsJsonSchemaParams {
language_names: &["Rust".to_string()],
font_names: &["Zed Mono".to_string()],
theme_names: &["One Dark".into()],
icon_theme_names: &["Zed Icons".into()],
lsp_adapter_names: &["rust-analyzer".to_string()],
action_names: &[],
action_documentation: &HashMap::default(),
deprecations: &HashMap::default(),
deprecation_messages: &HashMap::default(),
};
let user_schema = SettingsStore::json_schema(&params);
let project_schema = SettingsStore::project_json_schema(&params);
assert_ne!(user_schema, project_schema);
let user_schema_str = serde_json::to_string(&user_schema).unwrap();
let project_schema_str = serde_json::to_string(&project_schema).unwrap();
assert!(user_schema_str.contains("\"auto_update\""));
assert!(!project_schema_str.contains("\"auto_update\""));
}
}