1use std::collections::HashMap;
4
5use {
6 super::item::{ActionType, FlatItem, SettingItem, SettingSection, SettingValue},
7 reovim_core::{
8 config::ProfileConfig,
9 option::{OptionSpec, OptionValue},
10 },
11 tracing::error,
12};
13
14#[derive(Debug, Clone)]
16pub struct SettingChange {
17 pub key: String,
19 pub old_value: OptionValue,
21 pub new_value: OptionValue,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27pub enum SettingsInputMode {
28 #[default]
30 Normal,
31 NumberInput,
33 TextInput,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum MessageKind {
40 Info,
41 Success,
42 Error,
43}
44
45#[derive(Debug, Clone, Default)]
47pub struct MenuLayout {
48 pub x: u16,
49 pub y: u16,
50 pub width: u16,
51 pub height: u16,
52 pub visible_items: usize,
53}
54
55#[derive(Debug, Clone)]
57pub struct SectionMeta {
58 pub display_name: String,
60 pub order: u32,
62 pub description: Option<String>,
64}
65
66#[derive(Debug, Clone)]
68pub struct RegisteredOption {
69 pub spec: OptionSpec,
71 pub value: OptionValue,
73 pub display_order: u32,
75}
76
77#[derive(Debug, Clone, Default)]
79pub struct SettingsMenuState {
80 pub visible: bool,
82 pub sections: Vec<SettingSection>,
84 pub flat_items: Vec<FlatItem>,
86 pub selected_index: usize,
88 pub scroll_offset: usize,
90 pub layout: MenuLayout,
92 pub input_mode: SettingsInputMode,
94 pub input_buffer: String,
96 pub input_prompt: String,
98 pub pending_action: Option<ActionType>,
100 pub message: Option<(String, MessageKind)>,
102
103 pub registered_sections: HashMap<String, SectionMeta>,
106 pub registered_options: HashMap<String, Vec<RegisteredOption>>,
108 registered_option_keys: std::collections::HashSet<String>,
110}
111
112impl SettingsMenuState {
113 #[must_use]
115 pub fn new() -> Self {
116 Self::default()
117 }
118
119 pub fn register_section(&mut self, id: String, meta: SectionMeta) {
127 if self.registered_sections.contains_key(&id) {
128 error!("Duplicate settings section registration: '{}'. First registration wins.", id);
129 return;
130 }
131 self.registered_sections.insert(id, meta);
132 }
133
134 pub fn register_option(&mut self, spec: &OptionSpec, value: &OptionValue) {
140 if !spec.show_in_menu {
142 return;
143 }
144
145 let key = spec.name.to_string();
146 if self.registered_option_keys.contains(&key) {
147 error!("Duplicate option registration: '{}'. First registration wins.", key);
148 return;
149 }
150 self.registered_option_keys.insert(key);
151
152 let section_id = spec.effective_section().to_string();
154
155 let options = self.registered_options.entry(section_id).or_default();
157
158 options.push(RegisteredOption {
159 spec: spec.clone(),
160 value: value.clone(),
161 display_order: spec.display_order,
162 });
163 }
164
165 pub fn rebuild_from_registered(&mut self) {
171 self.sections.clear();
172
173 let mut section_entries: Vec<(String, u32, String)> = self
175 .registered_sections
176 .iter()
177 .map(|(id, meta)| (id.clone(), meta.order, meta.display_name.clone()))
178 .collect();
179
180 for section_id in self.registered_options.keys() {
182 if !self.registered_sections.contains_key(section_id) {
183 section_entries.push((section_id.clone(), 100, section_id.clone()));
185 }
186 }
187
188 section_entries.sort_by_key(|(_, order, _)| *order);
190
191 for (section_id, _, display_name) in section_entries {
193 let mut items = Vec::new();
194
195 if let Some(options) = self.registered_options.get(§ion_id) {
196 let mut sorted_options: Vec<_> = options.iter().collect();
198 sorted_options.sort_by_key(|opt| opt.display_order);
199
200 for opt in sorted_options {
201 items.push(SettingValue::item_from_spec(&opt.spec, &opt.value));
202 }
203 }
204
205 if !items.is_empty() {
206 self.sections.push(SettingSection {
207 name: display_name,
208 items,
209 });
210 }
211 }
212
213 self.rebuild_flat_items();
214 }
215
216 pub fn open(&mut self, _profile: &ProfileConfig, profile_name: &str) {
218 self.visible = true;
219 self.scroll_offset = 0;
220 self.input_mode = SettingsInputMode::Normal;
221 self.input_buffer.clear();
222 self.input_prompt.clear();
223 self.pending_action = None;
224 self.message = None;
225
226 self.rebuild_from_registered();
228
229 self.sections
231 .push(Self::build_profile_section(profile_name));
232 self.rebuild_flat_items();
233
234 self.selected_index = 0;
236 for (i, item) in self.flat_items.iter().enumerate() {
237 if item.is_setting() {
238 self.selected_index = i;
239 break;
240 }
241 }
242 }
243
244 pub fn close(&mut self) {
246 self.visible = false;
247 self.input_mode = SettingsInputMode::Normal;
248 self.input_buffer.clear();
249 self.input_prompt.clear();
250 self.pending_action = None;
251 self.message = None;
252 }
253
254 pub fn update_option_value(&mut self, key: &str, value: &OptionValue) {
259 for options in self.registered_options.values_mut() {
261 for opt in options {
262 if opt.spec.name == key {
263 opt.value = value.clone();
264 break;
265 }
266 }
267 }
268
269 if self.visible {
271 for section in &mut self.sections {
272 for item in &mut section.items {
273 if item.key == key {
274 item.value = SettingValue::from_option_with_constraint(
275 value,
276 &self
277 .registered_options
278 .values()
279 .flatten()
280 .find(|o| o.spec.name == key)
281 .map(|o| o.spec.constraint.clone())
282 .unwrap_or_default(),
283 );
284 return;
285 }
286 }
287 }
288 }
289 }
290
291 fn build_profile_section(profile_name: &str) -> SettingSection {
293 SettingSection {
294 name: "Profile".to_string(),
295 items: vec![
296 SettingItem {
297 key: "profile.current".to_string(),
298 label: "Current".to_string(),
299 description: Some("Active profile".to_string()),
300 value: SettingValue::Display(profile_name.to_string()),
301 },
302 SettingItem {
303 key: "profile.save".to_string(),
304 label: "Save as...".to_string(),
305 description: Some("Save current settings to profile".to_string()),
306 value: SettingValue::Action(ActionType::SaveProfile),
307 },
308 SettingItem {
309 key: "profile.load".to_string(),
310 label: "Load...".to_string(),
311 description: Some("Load a different profile".to_string()),
312 value: SettingValue::Action(ActionType::LoadProfile),
313 },
314 ],
315 }
316 }
317
318 fn rebuild_flat_items(&mut self) {
320 self.flat_items.clear();
321 for (section_idx, section) in self.sections.iter().enumerate() {
322 self.flat_items
323 .push(FlatItem::SectionHeader(section.name.clone()));
324 for item_idx in 0..section.items.len() {
325 self.flat_items.push(FlatItem::Setting {
326 section_idx,
327 item_idx,
328 });
329 }
330 }
331 }
332
333 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
335 pub fn calculate_layout(&mut self, screen_width: u16, screen_height: u16) {
336 let width = ((f32::from(screen_width) * 0.5) as u16).clamp(40, 60);
338
339 let content_items = self.flat_items.len();
341 let content_height = (content_items + 2) as u16; let max_height = ((f32::from(screen_height) * 0.8) as u16).max(10);
345 let height = content_height.clamp(10, max_height);
346
347 let x = (screen_width.saturating_sub(width)) / 2;
349 let y = (screen_height.saturating_sub(height)) / 2;
350
351 let visible_items = (height.saturating_sub(2)) as usize;
353
354 self.layout = MenuLayout {
355 x,
356 y,
357 width,
358 height,
359 visible_items,
360 };
361 }
362
363 pub fn select_next(&mut self) {
365 if self.flat_items.is_empty() {
366 return;
367 }
368
369 let mut next = self.selected_index + 1;
370 while next < self.flat_items.len() {
371 if self.flat_items[next].is_setting() {
372 self.selected_index = next;
373 self.ensure_visible();
374 return;
375 }
376 next += 1;
377 }
378 for (i, item) in self.flat_items.iter().enumerate() {
380 if item.is_setting() {
381 self.selected_index = i;
382 self.ensure_visible();
383 return;
384 }
385 }
386 }
387
388 pub fn select_prev(&mut self) {
390 if self.flat_items.is_empty() {
391 return;
392 }
393
394 let mut prev = self.selected_index.saturating_sub(1);
395 loop {
396 if self.flat_items[prev].is_setting() {
397 self.selected_index = prev;
398 self.ensure_visible();
399 return;
400 }
401 if prev == 0 {
402 break;
403 }
404 prev -= 1;
405 }
406 for i in (0..self.flat_items.len()).rev() {
408 if self.flat_items[i].is_setting() {
409 self.selected_index = i;
410 self.ensure_visible();
411 return;
412 }
413 }
414 }
415
416 const fn ensure_visible(&mut self) {
418 if self.selected_index < self.scroll_offset {
419 self.scroll_offset = self.selected_index;
420 } else if self.selected_index >= self.scroll_offset + self.layout.visible_items {
421 self.scroll_offset = self
422 .selected_index
423 .saturating_sub(self.layout.visible_items)
424 + 1;
425 }
426 }
427
428 pub fn selected_item_mut(&mut self) -> Option<&mut SettingItem> {
430 let flat_item = self.flat_items.get(self.selected_index)?;
431 if let FlatItem::Setting {
432 section_idx,
433 item_idx,
434 } = flat_item
435 {
436 self.sections
437 .get_mut(*section_idx)?
438 .items
439 .get_mut(*item_idx)
440 } else {
441 None
442 }
443 }
444
445 #[must_use]
447 pub fn selected_item(&self) -> Option<&SettingItem> {
448 let flat_item = self.flat_items.get(self.selected_index)?;
449 if let FlatItem::Setting {
450 section_idx,
451 item_idx,
452 } = flat_item
453 {
454 self.sections.get(*section_idx)?.items.get(*item_idx)
455 } else {
456 None
457 }
458 }
459
460 pub fn toggle_selected(&mut self) -> Option<SettingChange> {
462 let (key, section_idx, item_idx) = {
464 let flat_item = self.flat_items.get(self.selected_index)?;
465 if let FlatItem::Setting {
466 section_idx,
467 item_idx,
468 } = flat_item
469 {
470 let item = self.sections.get(*section_idx)?.items.get(*item_idx)?;
471 if !item.value.is_bool() {
472 return None;
473 }
474 (item.key.clone(), *section_idx, *item_idx)
475 } else {
476 return None;
477 }
478 };
479
480 let item = self
482 .sections
483 .get_mut(section_idx)?
484 .items
485 .get_mut(item_idx)?;
486 let old_value = item.value.to_option_value()?;
487 item.value.toggle();
488 let new_value = item.value.to_option_value()?;
489
490 for options in self.registered_options.values_mut() {
492 for opt in options.iter_mut() {
493 if opt.spec.name == key {
494 opt.value = new_value.clone();
495 break;
496 }
497 }
498 }
499
500 Some(SettingChange {
501 key,
502 old_value,
503 new_value,
504 })
505 }
506
507 fn sync_to_registered(&mut self, key: &str, new_value: &OptionValue) {
509 for options in self.registered_options.values_mut() {
510 for opt in options.iter_mut() {
511 if opt.spec.name == key {
512 opt.value = new_value.clone();
513 return;
514 }
515 }
516 }
517 }
518
519 pub fn cycle_next_selected(&mut self) -> Option<SettingChange> {
521 let (key, section_idx, item_idx) = {
522 let flat_item = self.flat_items.get(self.selected_index)?;
523 if let FlatItem::Setting {
524 section_idx,
525 item_idx,
526 } = flat_item
527 {
528 let item = self.sections.get(*section_idx)?.items.get(*item_idx)?;
529 match &item.value {
530 SettingValue::Choice { .. } | SettingValue::Number { .. } => {}
531 _ => return None,
532 }
533 (item.key.clone(), *section_idx, *item_idx)
534 } else {
535 return None;
536 }
537 };
538
539 let item = self
540 .sections
541 .get_mut(section_idx)?
542 .items
543 .get_mut(item_idx)?;
544 let old_value = item.value.to_option_value()?;
545 match &item.value {
546 SettingValue::Choice { .. } => item.value.cycle_next(),
547 SettingValue::Number { .. } => item.value.increment(),
548 _ => return None,
549 }
550 let new_value = item.value.to_option_value()?;
551 self.sync_to_registered(&key, &new_value);
552 Some(SettingChange {
553 key,
554 old_value,
555 new_value,
556 })
557 }
558
559 pub fn cycle_prev_selected(&mut self) -> Option<SettingChange> {
561 let (key, section_idx, item_idx) = {
562 let flat_item = self.flat_items.get(self.selected_index)?;
563 if let FlatItem::Setting {
564 section_idx,
565 item_idx,
566 } = flat_item
567 {
568 let item = self.sections.get(*section_idx)?.items.get(*item_idx)?;
569 match &item.value {
570 SettingValue::Choice { .. } | SettingValue::Number { .. } => {}
571 _ => return None,
572 }
573 (item.key.clone(), *section_idx, *item_idx)
574 } else {
575 return None;
576 }
577 };
578
579 let item = self
580 .sections
581 .get_mut(section_idx)?
582 .items
583 .get_mut(item_idx)?;
584 let old_value = item.value.to_option_value()?;
585 match &item.value {
586 SettingValue::Choice { .. } => item.value.cycle_prev(),
587 SettingValue::Number { .. } => item.value.decrement(),
588 _ => return None,
589 }
590 let new_value = item.value.to_option_value()?;
591 self.sync_to_registered(&key, &new_value);
592 Some(SettingChange {
593 key,
594 old_value,
595 new_value,
596 })
597 }
598
599 pub fn quick_select(&mut self, index: u8) -> Option<SettingChange> {
601 let (key, section_idx, item_idx) = {
602 let flat_item = self.flat_items.get(self.selected_index)?;
603 if let FlatItem::Setting {
604 section_idx,
605 item_idx,
606 } = flat_item
607 {
608 let item = self.sections.get(*section_idx)?.items.get(*item_idx)?;
609 if !item.value.is_choice() {
610 return None;
611 }
612 (item.key.clone(), *section_idx, *item_idx)
613 } else {
614 return None;
615 }
616 };
617
618 let item = self
619 .sections
620 .get_mut(section_idx)?
621 .items
622 .get_mut(item_idx)?;
623 let old_value = item.value.to_option_value()?;
624 item.value.quick_select(index);
625 let new_value = item.value.to_option_value()?;
626 self.sync_to_registered(&key, &new_value);
627 Some(SettingChange {
628 key,
629 old_value,
630 new_value,
631 })
632 }
633
634 pub fn increment_selected(&mut self) -> Option<SettingChange> {
636 let (key, section_idx, item_idx) = {
637 let flat_item = self.flat_items.get(self.selected_index)?;
638 if let FlatItem::Setting {
639 section_idx,
640 item_idx,
641 } = flat_item
642 {
643 let item = self.sections.get(*section_idx)?.items.get(*item_idx)?;
644 if !item.value.is_number() {
645 return None;
646 }
647 (item.key.clone(), *section_idx, *item_idx)
648 } else {
649 return None;
650 }
651 };
652
653 let item = self
654 .sections
655 .get_mut(section_idx)?
656 .items
657 .get_mut(item_idx)?;
658 let old_value = item.value.to_option_value()?;
659 item.value.increment();
660 let new_value = item.value.to_option_value()?;
661 self.sync_to_registered(&key, &new_value);
662 Some(SettingChange {
663 key,
664 old_value,
665 new_value,
666 })
667 }
668
669 pub fn decrement_selected(&mut self) -> Option<SettingChange> {
671 let (key, section_idx, item_idx) = {
672 let flat_item = self.flat_items.get(self.selected_index)?;
673 if let FlatItem::Setting {
674 section_idx,
675 item_idx,
676 } = flat_item
677 {
678 let item = self.sections.get(*section_idx)?.items.get(*item_idx)?;
679 if !item.value.is_number() {
680 return None;
681 }
682 (item.key.clone(), *section_idx, *item_idx)
683 } else {
684 return None;
685 }
686 };
687
688 let item = self
689 .sections
690 .get_mut(section_idx)?
691 .items
692 .get_mut(item_idx)?;
693 let old_value = item.value.to_option_value()?;
694 item.value.decrement();
695 let new_value = item.value.to_option_value()?;
696 self.sync_to_registered(&key, &new_value);
697 Some(SettingChange {
698 key,
699 old_value,
700 new_value,
701 })
702 }
703
704 #[must_use]
706 pub fn get_selected_action(&self) -> Option<ActionType> {
707 if let Some(item) = self.selected_item()
708 && let SettingValue::Action(action_type) = &item.value
709 {
710 return Some(*action_type);
711 }
712 None
713 }
714
715 pub fn set_message(&mut self, message: String, kind: MessageKind) {
717 self.message = Some((message, kind));
718 }
719
720 pub fn clear_message(&mut self) {
722 self.message = None;
723 }
724
725 #[must_use]
731 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
732 pub fn to_profile_config(&self) -> ProfileConfig {
733 let mut config = ProfileConfig::default();
734
735 for options in self.registered_options.values() {
737 for opt in options {
738 let key = opt.spec.name.as_ref();
739 match key {
740 "number" => {
741 if let OptionValue::Bool(b) = &opt.value {
742 config.editor.number = *b;
743 }
744 }
745 "relativenumber" => {
746 if let OptionValue::Bool(b) = &opt.value {
747 config.editor.relativenumber = *b;
748 }
749 }
750 "tabwidth" => {
751 if let OptionValue::Integer(i) = &opt.value {
752 config.editor.tabwidth = (*i).clamp(1, 8) as u8;
753 }
754 }
755 "expandtab" => {
756 if let OptionValue::Bool(b) = &opt.value {
757 config.editor.expandtab = *b;
758 }
759 }
760 "indentguide" => {
761 if let OptionValue::Bool(b) = &opt.value {
762 config.editor.indentguide = *b;
763 }
764 }
765 "scrollbar" => {
766 if let OptionValue::Bool(b) = &opt.value {
767 config.editor.scrollbar = *b;
768 }
769 }
770 "scrolloff" => {
771 if let OptionValue::Integer(i) = &opt.value {
772 config.editor.scrolloff = (*i).max(0) as u16;
773 }
774 }
775 "theme" => {
776 if let OptionValue::Choice { value, .. } = &opt.value {
777 config.editor.theme = value.clone();
778 }
779 }
780 "colormode" => {
781 if let OptionValue::Choice { value, .. } = &opt.value {
782 config.editor.colormode = value.clone();
783 }
784 }
785 "splitbelow" => {
786 if let OptionValue::Bool(b) = &opt.value {
787 if *b {
789 config.window.default_split = "horizontal".to_string();
790 }
791 }
792 }
793 "splitright" => {
794 if let OptionValue::Bool(b) = &opt.value {
795 if *b {
797 config.window.default_split = "vertical".to_string();
798 }
799 }
800 }
801 _ => {}
802 }
803 }
804 }
805
806 config
807 }
808
809 pub fn enter_text_input(&mut self, action: ActionType, prompt: &str, default_value: &str) {
813 self.input_mode = SettingsInputMode::TextInput;
814 self.pending_action = Some(action);
815 self.input_prompt = prompt.to_string();
816 self.input_buffer = default_value.to_string();
817 }
818
819 pub fn cancel_text_input(&mut self) {
821 self.input_mode = SettingsInputMode::Normal;
822 self.pending_action = None;
823 self.input_prompt.clear();
824 self.input_buffer.clear();
825 }
826
827 pub fn input_char(&mut self, c: char) {
829 if self.input_mode == SettingsInputMode::TextInput {
830 if c.is_alphanumeric() || c == '_' || c == '-' {
832 self.input_buffer.push(c);
833 }
834 }
835 }
836
837 pub fn input_backspace(&mut self) {
839 if self.input_mode == SettingsInputMode::TextInput {
840 self.input_buffer.pop();
841 }
842 }
843
844 #[must_use]
846 pub const fn is_text_input_mode(&self) -> bool {
847 matches!(self.input_mode, SettingsInputMode::TextInput)
848 }
849
850 #[must_use]
852 pub fn get_input_value(&self) -> &str {
853 &self.input_buffer
854 }
855
856 pub const fn take_pending_action(&mut self) -> Option<ActionType> {
858 self.pending_action.take()
859 }
860}
861
862#[cfg(test)]
863mod tests {
864 use super::*;
865
866 fn register_test_options(state: &mut SettingsMenuState) {
868 state.register_section(
870 "Test".to_string(),
871 SectionMeta {
872 display_name: "Test".to_string(),
873 order: 0,
874 description: None,
875 },
876 );
877
878 let bool_spec = OptionSpec::new("test_bool", "Test boolean", OptionValue::Bool(true))
880 .with_section("Test")
881 .with_display_order(10);
882 state.register_option(&bool_spec, &OptionValue::Bool(true));
883
884 let choice_spec = OptionSpec::new(
886 "test_choice",
887 "Test choice",
888 OptionValue::Choice {
889 value: "a".to_string(),
890 choices: vec!["a".to_string(), "b".to_string(), "c".to_string()],
891 },
892 )
893 .with_section("Test")
894 .with_display_order(20);
895 state.register_option(
896 &choice_spec,
897 &OptionValue::Choice {
898 value: "a".to_string(),
899 choices: vec!["a".to_string(), "b".to_string(), "c".to_string()],
900 },
901 );
902 }
903
904 #[test]
905 fn test_settings_menu_open_close() {
906 let mut state = SettingsMenuState::new();
907 assert!(!state.visible);
908
909 register_test_options(&mut state);
911
912 let profile = ProfileConfig::default();
913 state.open(&profile, "default");
914 assert!(state.visible);
915 assert!(!state.sections.is_empty());
916 assert!(!state.flat_items.is_empty());
917
918 state.close();
919 assert!(!state.visible);
920 }
921
922 #[test]
923 fn test_navigation() {
924 let mut state = SettingsMenuState::new();
925
926 register_test_options(&mut state);
928
929 let profile = ProfileConfig::default();
930 state.open(&profile, "default");
931 state.calculate_layout(120, 40);
932
933 let initial = state.selected_index;
935 assert!(state.flat_items[initial].is_setting());
936
937 state.select_next();
939 assert!(state.selected_index > initial);
940
941 state.select_prev();
943 assert_eq!(state.selected_index, initial);
944 }
945
946 #[test]
947 fn test_toggle() {
948 let mut state = SettingsMenuState::new();
949
950 register_test_options(&mut state);
952
953 let profile = ProfileConfig::default();
954 state.open(&profile, "default");
955
956 let mut found_bool = false;
958 for _ in 0..state.flat_items.len() {
959 if let Some(item) = state.selected_item()
960 && item.value.is_bool()
961 {
962 found_bool = true;
963 break;
964 }
965 state.select_next();
966 }
967
968 assert!(found_bool, "Should find at least one boolean setting");
969
970 if let Some(item) = state.selected_item()
971 && let SettingValue::Bool(initial) = item.value
972 {
973 state.toggle_selected();
974 if let Some(item) = state.selected_item()
975 && let SettingValue::Bool(after) = item.value
976 {
977 assert_ne!(initial, after);
978 }
979 }
980 }
981
982 #[test]
990 fn test_line_number_toggle_scenario() {
991 use {crate::settings_menu::item::SettingValue, reovim_core::option::OptionCategory};
992
993 let mut state = SettingsMenuState::new();
994
995 state.register_section(
997 "Editor".to_string(),
998 SectionMeta {
999 display_name: "Editor".to_string(),
1000 order: 0,
1001 description: Some("Core editor settings".into()),
1002 },
1003 );
1004
1005 let number_spec = OptionSpec::new("number", "Show line numbers", OptionValue::Bool(true))
1007 .with_short("nu")
1008 .with_category(OptionCategory::Editor)
1009 .with_section("Editor")
1010 .with_display_order(10);
1011 state.register_option(&number_spec, &OptionValue::Bool(true));
1012
1013 let profile = ProfileConfig::default();
1015 state.open(&profile, "default");
1016 state.calculate_layout(120, 40);
1017
1018 let mut found_number = false;
1020 for _ in 0..state.flat_items.len() {
1021 if let Some(item) = state.selected_item()
1022 && item.key == "number"
1023 {
1024 found_number = true;
1025 break;
1026 }
1027 state.select_next();
1028 }
1029
1030 assert!(found_number, "Should find the 'number' setting");
1031
1032 let item = state.selected_item().expect("Should have selected item");
1034 assert_eq!(item.key, "number");
1035 match &item.value {
1036 SettingValue::Bool(b) => assert!(*b, "Initial value should be true"),
1037 _ => panic!("number should be a Bool type"),
1038 }
1039
1040 let change = state.toggle_selected();
1042 assert!(change.is_some(), "Toggle should return a SettingChange");
1043
1044 let change = change.unwrap();
1045 assert_eq!(change.key, "number");
1046 assert_eq!(change.old_value, OptionValue::Bool(true));
1047 assert_eq!(change.new_value, OptionValue::Bool(false));
1048
1049 let item_after = state
1051 .selected_item()
1052 .expect("Should still have selected item");
1053 assert_eq!(item_after.key, "number");
1054 match &item_after.value {
1055 SettingValue::Bool(b) => assert!(!*b, "Value after toggle should be false"),
1056 _ => panic!("number should still be a Bool type"),
1057 }
1058
1059 let registered = state
1061 .registered_options
1062 .get("Editor")
1063 .expect("Should have Editor section in registered_options");
1064 let number_opt = registered
1065 .iter()
1066 .find(|o| o.spec.name == "number")
1067 .expect("Should find number in registered_options");
1068 assert_eq!(
1069 number_opt.value,
1070 OptionValue::Bool(false),
1071 "registered_options should have updated value"
1072 );
1073
1074 let change2 = state.toggle_selected();
1076 assert!(change2.is_some(), "Second toggle should return a SettingChange");
1077
1078 let item_final = state.selected_item().expect("Should have selected item");
1079 match &item_final.value {
1080 SettingValue::Bool(b) => assert!(*b, "Value after second toggle should be true"),
1081 _ => panic!("number should still be a Bool type"),
1082 }
1083 }
1084
1085 #[test]
1087 fn test_render_reflects_toggle() {
1088 use reovim_core::option::OptionCategory;
1089
1090 let mut state = SettingsMenuState::new();
1091
1092 state.register_section(
1094 "Editor".to_string(),
1095 SectionMeta {
1096 display_name: "Editor".to_string(),
1097 order: 0,
1098 description: None,
1099 },
1100 );
1101
1102 let number_spec = OptionSpec::new("number", "Show line numbers", OptionValue::Bool(true))
1103 .with_category(OptionCategory::Editor)
1104 .with_section("Editor")
1105 .with_display_order(10);
1106 state.register_option(&number_spec, &OptionValue::Bool(true));
1107
1108 state.open(&ProfileConfig::default(), "default");
1110 state.calculate_layout(120, 40);
1111
1112 for _ in 0..state.flat_items.len() {
1114 if let Some(item) = state.selected_item()
1115 && item.key == "number"
1116 {
1117 break;
1118 }
1119 state.select_next();
1120 }
1121
1122 let before = state.selected_item().unwrap().value.display_value();
1124 assert_eq!(before, "on", "Display should be 'on' initially");
1125
1126 state.toggle_selected();
1128
1129 let after = state.selected_item().unwrap().value.display_value();
1131 assert_eq!(after, "off", "Display should be 'off' after toggle");
1132
1133 let cloned_state = state.clone();
1135 let cloned_item = cloned_state.selected_item().unwrap();
1136 let cloned_display = cloned_item.value.display_value();
1137 assert_eq!(cloned_display, "off", "Cloned state should also show 'off'");
1138 }
1139}