use super::schema::{SettingCategory, SettingSchema, SettingType};
use crate::config_io::ConfigLayer;
use crate::view::controls::{
DropdownState, FocusState, KeybindingListState, MapState, NumberInputState, TextInputState,
TextListState, ToggleState,
};
use crate::view::ui::{FocusRegion, ScrollItem, TextEdit};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone)]
pub struct JsonEditState {
pub editor: TextEdit,
pub original_text: String,
pub label: String,
pub focus: FocusState,
pub scroll_offset: usize,
pub max_visible_lines: usize,
}
impl JsonEditState {
pub fn new(label: impl Into<String>, value: Option<&serde_json::Value>) -> Self {
let json_str = value
.map(|v| serde_json::to_string_pretty(v).unwrap_or_else(|_| "null".to_string()))
.unwrap_or_else(|| "null".to_string());
Self {
original_text: json_str.clone(),
editor: TextEdit::with_text(&json_str),
label: label.into(),
focus: FocusState::Normal,
scroll_offset: 0,
max_visible_lines: 6,
}
}
pub fn revert(&mut self) {
self.editor.set_value(&self.original_text);
self.scroll_offset = 0;
}
pub fn commit(&mut self) {
self.original_text = self.editor.value();
}
pub fn value(&self) -> String {
self.editor.value()
}
pub fn is_valid(&self) -> bool {
serde_json::from_str::<serde_json::Value>(&self.value()).is_ok()
}
pub fn display_height(&self) -> usize {
self.editor.line_count()
}
pub fn display_height_capped(&self) -> usize {
self.editor.line_count().min(self.max_visible_lines)
}
pub fn lines(&self) -> &[String] {
&self.editor.lines
}
pub fn cursor_pos(&self) -> (usize, usize) {
(self.editor.cursor_row, self.editor.cursor_col)
}
pub fn insert(&mut self, c: char) {
self.editor.insert_char(c);
}
pub fn insert_str(&mut self, s: &str) {
self.editor.insert_str(s);
}
pub fn backspace(&mut self) {
self.editor.backspace();
}
pub fn delete(&mut self) {
self.editor.delete();
}
pub fn move_left(&mut self) {
self.editor.move_left();
}
pub fn move_right(&mut self) {
self.editor.move_right();
}
pub fn move_up(&mut self) {
self.editor.move_up();
}
pub fn move_down(&mut self) {
self.editor.move_down();
}
pub fn move_home(&mut self) {
self.editor.move_home();
}
pub fn move_end(&mut self) {
self.editor.move_end();
}
pub fn move_word_left(&mut self) {
self.editor.move_word_left();
}
pub fn move_word_right(&mut self) {
self.editor.move_word_right();
}
pub fn has_selection(&self) -> bool {
self.editor.has_selection()
}
pub fn selection_range(&self) -> Option<((usize, usize), (usize, usize))> {
self.editor.selection_range()
}
pub fn selected_text(&self) -> Option<String> {
self.editor.selected_text()
}
pub fn delete_selection(&mut self) -> Option<String> {
self.editor.delete_selection()
}
pub fn clear_selection(&mut self) {
self.editor.clear_selection();
}
pub fn move_left_selecting(&mut self) {
self.editor.move_left_selecting();
}
pub fn move_right_selecting(&mut self) {
self.editor.move_right_selecting();
}
pub fn move_up_selecting(&mut self) {
self.editor.move_up_selecting();
}
pub fn move_down_selecting(&mut self) {
self.editor.move_down_selecting();
}
pub fn move_home_selecting(&mut self) {
self.editor.move_home_selecting();
}
pub fn move_end_selecting(&mut self) {
self.editor.move_end_selecting();
}
pub fn move_word_left_selecting(&mut self) {
self.editor.move_word_left_selecting();
}
pub fn move_word_right_selecting(&mut self) {
self.editor.move_word_right_selecting();
}
pub fn select_all(&mut self) {
self.editor.select_all();
}
pub fn delete_word_forward(&mut self) {
self.editor.delete_word_forward();
}
pub fn delete_word_backward(&mut self) {
self.editor.delete_word_backward();
}
pub fn delete_to_end(&mut self) {
self.editor.delete_to_end();
}
}
fn json_control(
name: &str,
current_value: Option<&serde_json::Value>,
default: Option<&serde_json::Value>,
) -> SettingControl {
let value = current_value.or(default);
SettingControl::Json(JsonEditState::new(name, value))
}
#[derive(Debug, Clone)]
pub struct SettingItem {
pub path: String,
pub name: String,
pub description: Option<String>,
pub control: SettingControl,
pub default: Option<serde_json::Value>,
pub modified: bool,
pub layer_source: ConfigLayer,
pub read_only: bool,
pub is_auto_managed: bool,
pub section: Option<String>,
pub is_section_start: bool,
}
#[derive(Debug, Clone)]
pub enum SettingControl {
Toggle(ToggleState),
Number(NumberInputState),
Dropdown(DropdownState),
Text(TextInputState),
TextList(TextListState),
Map(MapState),
ObjectArray(KeybindingListState),
Json(JsonEditState),
Complex {
type_name: String,
},
}
impl SettingControl {
pub fn control_height(&self) -> u16 {
match self {
Self::TextList(state) => {
(state.items.len() + 2) as u16
}
Self::Map(state) => {
let header_row = if state.display_field.is_some() { 1 } else { 0 };
let add_new_row = if state.no_add { 0 } else { 1 };
let base = 1 + header_row + state.entries.len() + add_new_row; let expanded_height: usize = state
.expanded
.iter()
.filter_map(|&idx| state.entries.get(idx))
.map(|(_, v)| {
if let Some(obj) = v.as_object() {
obj.len().min(5) + if obj.len() > 5 { 1 } else { 0 }
} else {
0
}
})
.sum();
(base + expanded_height) as u16
}
Self::Dropdown(state) => {
if state.open {
1 + state.options.len().min(8) as u16
} else {
1
}
}
Self::ObjectArray(state) => {
(state.bindings.len() + 2) as u16
}
Self::Json(state) => {
1 + state.display_height() as u16
}
_ => 1,
}
}
}
pub const SECTION_HEADER_HEIGHT: u16 = 2;
impl SettingItem {
pub fn item_height(&self) -> u16 {
let section_height = if self.is_section_start {
SECTION_HEADER_HEIGHT
} else {
0
};
let description_height = if self.description.is_some() { 1 } else { 0 };
section_height + self.control.control_height() + description_height + 1
}
pub fn item_height_expanded(&self, width: u16) -> u16 {
let section_height = if self.is_section_start {
SECTION_HEADER_HEIGHT
} else {
0
};
let description_height = self.description_height_expanded(width);
section_height + self.control.control_height() + description_height + 1
}
pub fn description_height_expanded(&self, width: u16) -> u16 {
if let Some(ref desc) = self.description {
if desc.is_empty() || width == 0 {
return 0;
}
let chars_per_line = width.saturating_sub(2) as usize; if chars_per_line == 0 {
return 1;
}
desc.len().div_ceil(chars_per_line) as u16
} else {
0
}
}
pub fn content_height(&self) -> u16 {
let description_height = if self.description.is_some() { 1 } else { 0 };
self.control.control_height() + description_height
}
pub fn content_height_expanded(&self, width: u16) -> u16 {
let description_height = self.description_height_expanded(width);
self.control.control_height() + description_height
}
}
pub fn clean_description(name: &str, description: Option<&str>) -> Option<String> {
let desc = description?;
if desc.is_empty() {
return None;
}
let name_words: HashSet<String> = name
.to_lowercase()
.split(|c: char| !c.is_alphanumeric())
.filter(|w| !w.is_empty() && w.len() > 2)
.map(String::from)
.collect();
let filler_words: HashSet<&str> = [
"the", "a", "an", "to", "for", "of", "in", "on", "is", "are", "be", "and", "or", "when",
"whether", "if", "this", "that", "with", "from", "by", "as", "at", "show", "enable",
"disable", "set", "use", "allow", "default", "true", "false",
]
.into_iter()
.collect();
let desc_words: Vec<&str> = desc
.split(|c: char| !c.is_alphanumeric())
.filter(|w| !w.is_empty())
.collect();
let has_new_info = desc_words.iter().any(|word| {
let lower = word.to_lowercase();
lower.len() > 2 && !name_words.contains(&lower) && !filler_words.contains(lower.as_str())
});
if !has_new_info {
return None;
}
Some(desc.to_string())
}
impl ScrollItem for SettingItem {
fn height(&self) -> u16 {
self.item_height()
}
fn focus_regions(&self) -> Vec<FocusRegion> {
match &self.control {
SettingControl::TextList(state) => {
let mut regions = Vec::new();
regions.push(FocusRegion {
id: 0,
y_offset: 0,
height: 1,
});
for i in 0..state.items.len() {
regions.push(FocusRegion {
id: 1 + i,
y_offset: 1 + i as u16,
height: 1,
});
}
regions.push(FocusRegion {
id: 1 + state.items.len(),
y_offset: 1 + state.items.len() as u16,
height: 1,
});
regions
}
SettingControl::Map(state) => {
let mut regions = Vec::new();
let mut y = 0u16;
regions.push(FocusRegion {
id: 0,
y_offset: y,
height: 1,
});
y += 1;
if state.display_field.is_some() {
y += 1;
}
for (i, (_, v)) in state.entries.iter().enumerate() {
let mut entry_height = 1u16;
if state.expanded.contains(&i) {
if let Some(obj) = v.as_object() {
entry_height += obj.len().min(5) as u16;
if obj.len() > 5 {
entry_height += 1;
}
}
}
regions.push(FocusRegion {
id: 1 + i,
y_offset: y,
height: entry_height,
});
y += entry_height;
}
regions.push(FocusRegion {
id: 1 + state.entries.len(),
y_offset: y,
height: 1,
});
regions
}
SettingControl::ObjectArray(state) => {
let mut regions = Vec::new();
regions.push(FocusRegion {
id: 0,
y_offset: 0,
height: 1,
});
for i in 0..state.bindings.len() {
regions.push(FocusRegion {
id: 1 + i,
y_offset: 1 + i as u16,
height: 1,
});
}
regions.push(FocusRegion {
id: 1 + state.bindings.len(),
y_offset: 1 + state.bindings.len() as u16,
height: 1,
});
regions
}
_ => {
vec![FocusRegion {
id: 0,
y_offset: 0,
height: self.item_height().saturating_sub(1), }]
}
}
}
}
#[derive(Debug, Clone)]
pub struct SettingsPage {
pub name: String,
pub path: String,
pub description: Option<String>,
pub items: Vec<SettingItem>,
pub subpages: Vec<SettingsPage>,
}
pub struct BuildContext<'a> {
pub config_value: &'a serde_json::Value,
pub layer_sources: &'a HashMap<String, ConfigLayer>,
pub target_layer: ConfigLayer,
}
pub fn build_pages(
categories: &[SettingCategory],
config_value: &serde_json::Value,
layer_sources: &HashMap<String, ConfigLayer>,
target_layer: ConfigLayer,
) -> Vec<SettingsPage> {
let ctx = BuildContext {
config_value,
layer_sources,
target_layer,
};
categories.iter().map(|cat| build_page(cat, &ctx)).collect()
}
fn build_page(category: &SettingCategory, ctx: &BuildContext) -> SettingsPage {
let mut items: Vec<SettingItem> = category
.settings
.iter()
.map(|s| build_item(s, ctx))
.collect();
items.sort_by(|a, b| match (&a.section, &b.section) {
(Some(sec_a), Some(sec_b)) => sec_a.cmp(sec_b).then_with(|| a.name.cmp(&b.name)),
(Some(_), None) => std::cmp::Ordering::Less,
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => a.name.cmp(&b.name),
});
let mut prev_section: Option<&String> = None;
for item in &mut items {
let is_new_section = match (&item.section, prev_section) {
(Some(sec), Some(prev)) => sec != prev,
(Some(_), None) => true,
(None, Some(_)) => false, (None, None) => false,
};
item.is_section_start = is_new_section;
prev_section = item.section.as_ref();
}
let subpages = category
.subcategories
.iter()
.map(|sub| build_page(sub, ctx))
.collect();
SettingsPage {
name: category.name.clone(),
path: category.path.clone(),
description: category.description.clone(),
items,
subpages,
}
}
pub fn build_item(schema: &SettingSchema, ctx: &BuildContext) -> SettingItem {
let current_value = ctx.config_value.pointer(&schema.path);
let is_auto_managed = matches!(&schema.setting_type, SettingType::Map { no_add: true, .. });
let control = match &schema.setting_type {
SettingType::Boolean => {
let checked = current_value
.and_then(|v| v.as_bool())
.or_else(|| schema.default.as_ref().and_then(|d| d.as_bool()))
.unwrap_or(false);
SettingControl::Toggle(ToggleState::new(checked, &schema.name))
}
SettingType::Integer { minimum, maximum } => {
let value = current_value
.and_then(|v| v.as_i64())
.or_else(|| schema.default.as_ref().and_then(|d| d.as_i64()))
.unwrap_or(0);
let mut state = NumberInputState::new(value, &schema.name);
if let Some(min) = minimum {
state = state.with_min(*min);
}
if let Some(max) = maximum {
state = state.with_max(*max);
}
SettingControl::Number(state)
}
SettingType::Number { minimum, maximum } => {
let value = current_value
.and_then(|v| v.as_f64())
.or_else(|| schema.default.as_ref().and_then(|d| d.as_f64()))
.unwrap_or(0.0);
let int_value = (value * 100.0).round() as i64;
let mut state = NumberInputState::new(int_value, &schema.name).with_percentage();
if let Some(min) = minimum {
state = state.with_min((*min * 100.0) as i64);
}
if let Some(max) = maximum {
state = state.with_max((*max * 100.0) as i64);
}
SettingControl::Number(state)
}
SettingType::String => {
let value = current_value
.and_then(|v| v.as_str())
.or_else(|| schema.default.as_ref().and_then(|d| d.as_str()))
.unwrap_or("");
let state = TextInputState::new(&schema.name).with_value(value);
SettingControl::Text(state)
}
SettingType::Enum { options } => {
let current = if current_value.map(|v| v.is_null()).unwrap_or(false) {
"" } else {
current_value
.and_then(|v| v.as_str())
.or_else(|| {
let default = schema.default.as_ref()?;
if default.is_null() {
Some("")
} else {
default.as_str()
}
})
.unwrap_or("")
};
let display_names: Vec<String> = options.iter().map(|o| o.name.clone()).collect();
let values: Vec<String> = options.iter().map(|o| o.value.clone()).collect();
let selected = values.iter().position(|v| v == current).unwrap_or(0);
let state = DropdownState::with_values(display_names, values, &schema.name)
.with_selected(selected);
SettingControl::Dropdown(state)
}
SettingType::StringArray => {
let items: Vec<String> = current_value
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.or_else(|| {
schema.default.as_ref().and_then(|d| {
d.as_array().map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
})
})
.unwrap_or_default();
let state = TextListState::new(&schema.name).with_items(items);
SettingControl::TextList(state)
}
SettingType::Object { .. } => {
json_control(&schema.name, current_value, schema.default.as_ref())
}
SettingType::Map {
value_schema,
display_field,
no_add,
} => {
let map_value = current_value
.cloned()
.or_else(|| schema.default.clone())
.unwrap_or_else(|| serde_json::json!({}));
let mut state = MapState::new(&schema.name).with_entries(&map_value);
state = state.with_value_schema((**value_schema).clone());
if let Some(field) = display_field {
state = state.with_display_field(field.clone());
}
if *no_add {
state = state.with_no_add(true);
}
SettingControl::Map(state)
}
SettingType::ObjectArray {
item_schema,
display_field,
} => {
let array_value = current_value
.cloned()
.or_else(|| schema.default.clone())
.unwrap_or_else(|| serde_json::json!([]));
let mut state = KeybindingListState::new(&schema.name).with_bindings(&array_value);
state = state.with_item_schema((**item_schema).clone());
if let Some(field) = display_field {
state = state.with_display_field(field.clone());
}
SettingControl::ObjectArray(state)
}
SettingType::Complex => json_control(&schema.name, current_value, schema.default.as_ref()),
};
let layer_source = ctx
.layer_sources
.get(&schema.path)
.copied()
.unwrap_or(ConfigLayer::System);
let modified = if is_auto_managed {
false } else {
layer_source == ctx.target_layer
};
let cleaned_description = clean_description(&schema.name, schema.description.as_deref());
SettingItem {
path: schema.path.clone(),
name: schema.name.clone(),
description: cleaned_description,
control,
default: schema.default.clone(),
modified,
layer_source,
read_only: schema.read_only,
is_auto_managed,
section: schema.section.clone(),
is_section_start: false, }
}
pub fn build_item_from_value(
schema: &SettingSchema,
current_value: Option<&serde_json::Value>,
) -> SettingItem {
let control = match &schema.setting_type {
SettingType::Boolean => {
let checked = current_value
.and_then(|v| v.as_bool())
.or_else(|| schema.default.as_ref().and_then(|d| d.as_bool()))
.unwrap_or(false);
SettingControl::Toggle(ToggleState::new(checked, &schema.name))
}
SettingType::Integer { minimum, maximum } => {
let value = current_value
.and_then(|v| v.as_i64())
.or_else(|| schema.default.as_ref().and_then(|d| d.as_i64()))
.unwrap_or(0);
let mut state = NumberInputState::new(value, &schema.name);
if let Some(min) = minimum {
state = state.with_min(*min);
}
if let Some(max) = maximum {
state = state.with_max(*max);
}
SettingControl::Number(state)
}
SettingType::Number { minimum, maximum } => {
let value = current_value
.and_then(|v| v.as_f64())
.or_else(|| schema.default.as_ref().and_then(|d| d.as_f64()))
.unwrap_or(0.0);
let int_value = (value * 100.0).round() as i64;
let mut state = NumberInputState::new(int_value, &schema.name).with_percentage();
if let Some(min) = minimum {
state = state.with_min((*min * 100.0) as i64);
}
if let Some(max) = maximum {
state = state.with_max((*max * 100.0) as i64);
}
SettingControl::Number(state)
}
SettingType::String => {
let value = current_value
.and_then(|v| v.as_str())
.or_else(|| schema.default.as_ref().and_then(|d| d.as_str()))
.unwrap_or("");
let state = TextInputState::new(&schema.name).with_value(value);
SettingControl::Text(state)
}
SettingType::Enum { options } => {
let current = if current_value.map(|v| v.is_null()).unwrap_or(false) {
"" } else {
current_value
.and_then(|v| v.as_str())
.or_else(|| {
let default = schema.default.as_ref()?;
if default.is_null() {
Some("")
} else {
default.as_str()
}
})
.unwrap_or("")
};
let display_names: Vec<String> = options.iter().map(|o| o.name.clone()).collect();
let values: Vec<String> = options.iter().map(|o| o.value.clone()).collect();
let selected = values.iter().position(|v| v == current).unwrap_or(0);
let state = DropdownState::with_values(display_names, values, &schema.name)
.with_selected(selected);
SettingControl::Dropdown(state)
}
SettingType::StringArray => {
let items: Vec<String> = current_value
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.or_else(|| {
schema.default.as_ref().and_then(|d| {
d.as_array().map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
})
})
.unwrap_or_default();
let state = TextListState::new(&schema.name).with_items(items);
SettingControl::TextList(state)
}
SettingType::Object { .. } => {
json_control(&schema.name, current_value, schema.default.as_ref())
}
SettingType::Map {
value_schema,
display_field,
no_add,
} => {
let map_value = current_value
.cloned()
.or_else(|| schema.default.clone())
.unwrap_or_else(|| serde_json::json!({}));
let mut state = MapState::new(&schema.name).with_entries(&map_value);
state = state.with_value_schema((**value_schema).clone());
if let Some(field) = display_field {
state = state.with_display_field(field.clone());
}
if *no_add {
state = state.with_no_add(true);
}
SettingControl::Map(state)
}
SettingType::ObjectArray {
item_schema,
display_field,
} => {
let array_value = current_value
.cloned()
.or_else(|| schema.default.clone())
.unwrap_or_else(|| serde_json::json!([]));
let mut state = KeybindingListState::new(&schema.name).with_bindings(&array_value);
state = state.with_item_schema((**item_schema).clone());
if let Some(field) = display_field {
state = state.with_display_field(field.clone());
}
SettingControl::ObjectArray(state)
}
SettingType::Complex => json_control(&schema.name, current_value, schema.default.as_ref()),
};
let modified = match (¤t_value, &schema.default) {
(Some(current), Some(default)) => *current != default,
(Some(_), None) => true,
_ => false,
};
let is_auto_managed = matches!(&schema.setting_type, SettingType::Map { no_add: true, .. });
SettingItem {
path: schema.path.clone(),
name: schema.name.clone(),
description: schema.description.clone(),
control,
default: schema.default.clone(),
modified,
layer_source: ConfigLayer::System,
read_only: schema.read_only,
is_auto_managed,
section: schema.section.clone(),
is_section_start: false, }
}
pub fn control_to_value(control: &SettingControl) -> serde_json::Value {
match control {
SettingControl::Toggle(state) => serde_json::Value::Bool(state.checked),
SettingControl::Number(state) => {
if state.is_percentage {
let float_value = state.value as f64 / 100.0;
serde_json::Number::from_f64(float_value)
.map(serde_json::Value::Number)
.unwrap_or(serde_json::Value::Number(state.value.into()))
} else {
serde_json::Value::Number(state.value.into())
}
}
SettingControl::Dropdown(state) => state
.selected_value()
.map(|s| {
if s.is_empty() {
serde_json::Value::Null
} else {
serde_json::Value::String(s.to_string())
}
})
.unwrap_or(serde_json::Value::Null),
SettingControl::Text(state) => serde_json::Value::String(state.value.clone()),
SettingControl::TextList(state) => {
let arr: Vec<serde_json::Value> = state
.items
.iter()
.map(|s| serde_json::Value::String(s.clone()))
.collect();
serde_json::Value::Array(arr)
}
SettingControl::Map(state) => state.to_value(),
SettingControl::ObjectArray(state) => state.to_value(),
SettingControl::Json(state) => {
serde_json::from_str(&state.value()).unwrap_or(serde_json::Value::Null)
}
SettingControl::Complex { .. } => serde_json::Value::Null,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_config() -> serde_json::Value {
serde_json::json!({
"theme": "monokai",
"check_for_updates": false,
"editor": {
"tab_size": 2,
"line_numbers": true
}
})
}
fn test_context(config: &serde_json::Value) -> BuildContext<'_> {
static EMPTY_SOURCES: std::sync::LazyLock<HashMap<String, ConfigLayer>> =
std::sync::LazyLock::new(HashMap::new);
BuildContext {
config_value: config,
layer_sources: &EMPTY_SOURCES,
target_layer: ConfigLayer::User,
}
}
fn test_context_with_sources<'a>(
config: &'a serde_json::Value,
layer_sources: &'a HashMap<String, ConfigLayer>,
target_layer: ConfigLayer,
) -> BuildContext<'a> {
BuildContext {
config_value: config,
layer_sources,
target_layer,
}
}
#[test]
fn test_build_toggle_item() {
let schema = SettingSchema {
path: "/check_for_updates".to_string(),
name: "Check For Updates".to_string(),
description: Some("Check for updates".to_string()),
setting_type: SettingType::Boolean,
default: Some(serde_json::Value::Bool(true)),
read_only: false,
section: None,
};
let config = sample_config();
let ctx = test_context(&config);
let item = build_item(&schema, &ctx);
assert_eq!(item.path, "/check_for_updates");
assert!(!item.modified);
assert_eq!(item.layer_source, ConfigLayer::System);
if let SettingControl::Toggle(state) = &item.control {
assert!(!state.checked); } else {
panic!("Expected toggle control");
}
}
#[test]
fn test_build_toggle_item_modified_in_user_layer() {
let schema = SettingSchema {
path: "/check_for_updates".to_string(),
name: "Check For Updates".to_string(),
description: Some("Check for updates".to_string()),
setting_type: SettingType::Boolean,
default: Some(serde_json::Value::Bool(true)),
read_only: false,
section: None,
};
let config = sample_config();
let mut layer_sources = HashMap::new();
layer_sources.insert("/check_for_updates".to_string(), ConfigLayer::User);
let ctx = test_context_with_sources(&config, &layer_sources, ConfigLayer::User);
let item = build_item(&schema, &ctx);
assert!(item.modified);
assert_eq!(item.layer_source, ConfigLayer::User);
}
#[test]
fn test_build_number_item() {
let schema = SettingSchema {
path: "/editor/tab_size".to_string(),
name: "Tab Size".to_string(),
description: None,
setting_type: SettingType::Integer {
minimum: Some(1),
maximum: Some(16),
},
default: Some(serde_json::Value::Number(4.into())),
read_only: false,
section: None,
};
let config = sample_config();
let ctx = test_context(&config);
let item = build_item(&schema, &ctx);
assert!(!item.modified);
if let SettingControl::Number(state) = &item.control {
assert_eq!(state.value, 2);
assert_eq!(state.min, Some(1));
assert_eq!(state.max, Some(16));
} else {
panic!("Expected number control");
}
}
#[test]
fn test_build_text_item() {
let schema = SettingSchema {
path: "/theme".to_string(),
name: "Theme".to_string(),
description: None,
setting_type: SettingType::String,
default: Some(serde_json::Value::String("high-contrast".to_string())),
read_only: false,
section: None,
};
let config = sample_config();
let ctx = test_context(&config);
let item = build_item(&schema, &ctx);
assert!(!item.modified);
if let SettingControl::Text(state) = &item.control {
assert_eq!(state.value, "monokai");
} else {
panic!("Expected text control");
}
}
#[test]
fn test_clean_description_keeps_full_desc_with_new_info() {
let result = clean_description("Tab Size", Some("Number of spaces per tab character"));
assert!(result.is_some());
let cleaned = result.unwrap();
assert!(cleaned.starts_with('N')); assert!(cleaned.contains("spaces"));
assert!(cleaned.contains("character"));
}
#[test]
fn test_clean_description_keeps_extra_info() {
let result = clean_description("Line Numbers", Some("Show line numbers in the gutter"));
assert!(result.is_some());
let cleaned = result.unwrap();
assert!(cleaned.contains("gutter"));
}
#[test]
fn test_clean_description_returns_none_for_pure_redundancy() {
let result = clean_description("Theme", Some("Theme"));
assert!(result.is_none());
let result = clean_description("Theme", Some("The theme to use"));
assert!(result.is_none());
}
#[test]
fn test_clean_description_returns_none_for_empty() {
let result = clean_description("Theme", Some(""));
assert!(result.is_none());
let result = clean_description("Theme", None);
assert!(result.is_none());
}
#[test]
fn test_control_to_value() {
let toggle = SettingControl::Toggle(ToggleState::new(true, "Test"));
assert_eq!(control_to_value(&toggle), serde_json::Value::Bool(true));
let number = SettingControl::Number(NumberInputState::new(42, "Test"));
assert_eq!(control_to_value(&number), serde_json::json!(42));
let text = SettingControl::Text(TextInputState::new("Test").with_value("hello"));
assert_eq!(
control_to_value(&text),
serde_json::Value::String("hello".to_string())
);
}
}