reovim_plugin_settings_menu/settings_menu/
state.rs

1//! Settings menu state management
2
3use 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/// Information about a setting change, used to emit `OptionChanged` events.
15#[derive(Debug, Clone)]
16pub struct SettingChange {
17    /// The setting key (e.g., "editor.theme")
18    pub key: String,
19    /// The value before the change
20    pub old_value: OptionValue,
21    /// The value after the change
22    pub new_value: OptionValue,
23}
24
25/// Input mode for the settings menu
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27pub enum SettingsInputMode {
28    /// Normal navigation mode
29    #[default]
30    Normal,
31    /// Typing a number value
32    NumberInput,
33    /// Typing a text value (e.g., profile name)
34    TextInput,
35}
36
37/// Message severity
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum MessageKind {
40    Info,
41    Success,
42    Error,
43}
44
45/// Layout information for the settings menu
46#[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/// Metadata for a registered settings section
56#[derive(Debug, Clone)]
57pub struct SectionMeta {
58    /// Display name for the section header
59    pub display_name: String,
60    /// Display order (lower = earlier)
61    pub order: u32,
62    /// Optional description
63    pub description: Option<String>,
64}
65
66/// A registered option with its current value
67#[derive(Debug, Clone)]
68pub struct RegisteredOption {
69    /// The option specification
70    pub spec: OptionSpec,
71    /// Current value
72    pub value: OptionValue,
73    /// Display order within section
74    pub display_order: u32,
75}
76
77/// Settings menu state
78#[derive(Debug, Clone, Default)]
79pub struct SettingsMenuState {
80    /// Whether the menu is visible
81    pub visible: bool,
82    /// Setting sections (built from registered options)
83    pub sections: Vec<SettingSection>,
84    /// Flattened items for navigation
85    pub flat_items: Vec<FlatItem>,
86    /// Currently selected flat item index
87    pub selected_index: usize,
88    /// Scroll offset for long lists
89    pub scroll_offset: usize,
90    /// Layout dimensions
91    pub layout: MenuLayout,
92    /// Current input mode
93    pub input_mode: SettingsInputMode,
94    /// Input buffer for text/number entry
95    pub input_buffer: String,
96    /// Input prompt label
97    pub input_prompt: String,
98    /// Pending action that triggered input mode
99    pub pending_action: Option<ActionType>,
100    /// Status message to display
101    pub message: Option<(String, MessageKind)>,
102
103    // --- Dynamic Registration ---
104    /// Registered sections (section_id -> metadata)
105    pub registered_sections: HashMap<String, SectionMeta>,
106    /// Registered options by section (section_id -> options)
107    pub registered_options: HashMap<String, Vec<RegisteredOption>>,
108    /// Set of registered option keys (for duplicate detection)
109    registered_option_keys: std::collections::HashSet<String>,
110}
111
112impl SettingsMenuState {
113    /// Create a new settings menu state
114    #[must_use]
115    pub fn new() -> Self {
116        Self::default()
117    }
118
119    // --- Dynamic Registration Methods ---
120
121    /// Register a settings section.
122    ///
123    /// # Errors
124    ///
125    /// Logs an error if a section with the same ID already exists (first wins).
126    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    /// Register an option from a `RegisterOption` event.
135    ///
136    /// # Errors
137    ///
138    /// Logs an error if an option with the same key already exists (first wins).
139    pub fn register_option(&mut self, spec: &OptionSpec, value: &OptionValue) {
140        // Skip options not meant for settings menu
141        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        // Determine section ID
153        let section_id = spec.effective_section().to_string();
154
155        // Create or get section entry
156        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    /// Rebuild sections from registered options.
166    ///
167    /// This method is called when opening the settings menu to build
168    /// the `sections` and `flat_items` from the dynamically registered
169    /// sections and options.
170    pub fn rebuild_from_registered(&mut self) {
171        self.sections.clear();
172
173        // Collect sections with their order
174        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        // Add sections for options that don't have explicit section registration
181        for section_id in self.registered_options.keys() {
182            if !self.registered_sections.contains_key(section_id) {
183                // Auto-create section with default order
184                section_entries.push((section_id.clone(), 100, section_id.clone()));
185            }
186        }
187
188        // Sort sections by order
189        section_entries.sort_by_key(|(_, order, _)| *order);
190
191        // Build sections
192        for (section_id, _, display_name) in section_entries {
193            let mut items = Vec::new();
194
195            if let Some(options) = self.registered_options.get(&section_id) {
196                // Sort options by display_order
197                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    /// Open the settings menu with current profile settings
217    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        // Build sections from dynamically registered options
227        self.rebuild_from_registered();
228
229        // Add profile management section (special case - not a registered option)
230        self.sections
231            .push(Self::build_profile_section(profile_name));
232        self.rebuild_flat_items();
233
234        // Select first actual setting (skip section headers)
235        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    /// Close the settings menu
245    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    /// Update a registered option value when receiving OptionChanged events.
255    ///
256    /// This method updates both the registered_options storage and any
257    /// currently displayed sections.
258    pub fn update_option_value(&mut self, key: &str, value: &OptionValue) {
259        // Update in registered_options
260        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        // Update in displayed sections (if menu is open)
270        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    /// Build the profile management section
292    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    /// Rebuild the flat items list from sections
319    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    /// Calculate layout based on screen dimensions and content
334    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
335    pub fn calculate_layout(&mut self, screen_width: u16, screen_height: u16) {
336        // Width: 50% of screen, clamped between 40 and 60
337        let width = ((f32::from(screen_width) * 0.5) as u16).clamp(40, 60);
338
339        // Height: based on content + borders (2 for top/bottom)
340        let content_items = self.flat_items.len();
341        let content_height = (content_items + 2) as u16; // items + top/bottom border
342
343        // Clamp height to reasonable bounds (min 10, max 80% of screen)
344        let max_height = ((f32::from(screen_height) * 0.8) as u16).max(10);
345        let height = content_height.clamp(10, max_height);
346
347        // Center on screen
348        let x = (screen_width.saturating_sub(width)) / 2;
349        let y = (screen_height.saturating_sub(height)) / 2;
350
351        // Visible items = height - 2 (borders)
352        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    /// Move selection to next item
364    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        // Wrap to first setting
379        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    /// Move selection to previous item
389    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        // Wrap to last setting
407        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    /// Ensure the selected item is visible
417    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    /// Get the currently selected setting item (mutable)
429    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    /// Get the currently selected setting item (immutable)
446    #[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    /// Toggle the selected boolean setting. Returns change info if toggled.
461    pub fn toggle_selected(&mut self) -> Option<SettingChange> {
462        // Get the key and check if it's a boolean
463        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        // Toggle the value in sections
481        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        // Also update registered_options
491        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    /// Helper to sync a value change to registered_options
508    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    /// Cycle to next value for selected setting. Returns change info if changed.
520    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    /// Cycle to previous value for selected setting. Returns change info if changed.
560    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    /// Quick select for the selected setting. Returns change info if changed.
600    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    /// Increment the selected number setting. Returns change info if changed.
635    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    /// Decrement the selected number setting. Returns change info if changed.
670    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    /// Get the action type if the selected item is an action
705    #[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    /// Set a message to display
716    pub fn set_message(&mut self, message: String, kind: MessageKind) {
717        self.message = Some((message, kind));
718    }
719
720    /// Clear any message
721    pub fn clear_message(&mut self) {
722        self.message = None;
723    }
724
725    /// Get the current profile config from settings state.
726    ///
727    /// This iterates over registered options and maps them to profile fields.
728    /// Note: The proper flow is for OptionChanged events to update OptionRegistry,
729    /// which then persists to profile. This method provides backwards compatibility.
730    #[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        // Iterate over all registered options
736        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 splitbelow is true, default_split should be "horizontal"
788                            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 splitright is true, default_split should be "vertical"
796                            if *b {
797                                config.window.default_split = "vertical".to_string();
798                            }
799                        }
800                    }
801                    _ => {}
802                }
803            }
804        }
805
806        config
807    }
808
809    // --- Text Input Mode Methods ---
810
811    /// Enter text input mode for a specific action
812    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    /// Exit text input mode without confirming
820    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    /// Add a character to the input buffer
828    pub fn input_char(&mut self, c: char) {
829        if self.input_mode == SettingsInputMode::TextInput {
830            // Only allow valid filename characters
831            if c.is_alphanumeric() || c == '_' || c == '-' {
832                self.input_buffer.push(c);
833            }
834        }
835    }
836
837    /// Remove the last character from the input buffer
838    pub fn input_backspace(&mut self) {
839        if self.input_mode == SettingsInputMode::TextInput {
840            self.input_buffer.pop();
841        }
842    }
843
844    /// Check if we're in text input mode
845    #[must_use]
846    pub const fn is_text_input_mode(&self) -> bool {
847        matches!(self.input_mode, SettingsInputMode::TextInput)
848    }
849
850    /// Get the current input value (for confirming)
851    #[must_use]
852    pub fn get_input_value(&self) -> &str {
853        &self.input_buffer
854    }
855
856    /// Take the pending action (consumes it)
857    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    /// Helper to register test options for settings menu tests
867    fn register_test_options(state: &mut SettingsMenuState) {
868        // Register a test section
869        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        // Register a boolean option
879        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        // Register a choice option
885        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 options before opening
910        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 options before opening
927        register_test_options(&mut state);
928
929        let profile = ProfileConfig::default();
930        state.open(&profile, "default");
931        state.calculate_layout(120, 40);
932
933        // First selectable item should be a setting (skip header)
934        let initial = state.selected_index;
935        assert!(state.flat_items[initial].is_setting());
936
937        // Move next
938        state.select_next();
939        assert!(state.selected_index > initial);
940
941        // Move prev
942        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 options before opening
951        register_test_options(&mut state);
952
953        let profile = ProfileConfig::default();
954        state.open(&profile, "default");
955
956        // Find a boolean setting
957        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 the line number toggle scenario
983    ///
984    /// This simulates what happens when a user:
985    /// 1. Opens settings menu (<SPC>s)
986    /// 2. Navigates to "Show line numbers" option
987    /// 3. Toggles it with Space
988    /// 4. Views the rendered output
989    #[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        // Register the Editor section (like CorePlugin does)
996        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        // Register the "number" option (line numbers) like CorePlugin does
1006        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        // Open the menu
1014        let profile = ProfileConfig::default();
1015        state.open(&profile, "default");
1016        state.calculate_layout(120, 40);
1017
1018        // Find the "number" setting
1019        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        // Verify initial value is true
1033        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        // Toggle the setting
1041        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        // Verify sections are updated (what render reads)
1050        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        // Verify registered_options are synced
1060        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        // Test toggling back
1075        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 that the value displayed in render reflects toggle changes
1086    #[test]
1087    fn test_render_reflects_toggle() {
1088        use reovim_core::option::OptionCategory;
1089
1090        let mut state = SettingsMenuState::new();
1091
1092        // Register section and option
1093        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        // Open menu
1109        state.open(&ProfileConfig::default(), "default");
1110        state.calculate_layout(120, 40);
1111
1112        // Navigate to number setting
1113        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        // Get display value BEFORE toggle
1123        let before = state.selected_item().unwrap().value.display_value();
1124        assert_eq!(before, "on", "Display should be 'on' initially");
1125
1126        // Toggle
1127        state.toggle_selected();
1128
1129        // Get display value AFTER toggle (simulates what render reads)
1130        let after = state.selected_item().unwrap().value.display_value();
1131        assert_eq!(after, "off", "Display should be 'off' after toggle");
1132
1133        // Simulate re-reading state (like render does with Clone::clone)
1134        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}