reovim_plugin_settings_menu/settings_menu/
item.rs

1//! Settings menu item types
2
3use reovim_core::option::{OptionConstraint, OptionSpec, OptionValue};
4
5/// A section grouping related settings
6#[derive(Debug, Clone)]
7pub struct SettingSection {
8    /// Section name (e.g., "Editor", "Cursor", "Window")
9    pub name: String,
10    /// Settings in this section
11    pub items: Vec<SettingItem>,
12}
13
14/// A single setting item
15#[derive(Debug, Clone)]
16pub struct SettingItem {
17    /// Internal key (e.g., "editor.theme", "plugin.treesitter.timeout")
18    ///
19    /// Changed from `&'static str` to `String` to support dynamic registration.
20    pub key: String,
21    /// Display label
22    pub label: String,
23    /// Help text (optional)
24    pub description: Option<String>,
25    /// The setting value and type
26    pub value: SettingValue,
27}
28
29/// Types of setting values
30#[derive(Debug, Clone)]
31pub enum SettingValue {
32    /// Boolean toggle (checkbox)
33    Bool(bool),
34    /// Multiple choice (dropdown)
35    Choice {
36        options: Vec<String>,
37        selected: usize,
38    },
39    /// Numeric value with bounds
40    Number {
41        value: i32,
42        min: i32,
43        max: i32,
44        step: i32,
45    },
46    /// Read-only display value
47    Display(String),
48    /// Action button
49    Action(ActionType),
50}
51
52/// Types of actions that can be triggered
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum ActionType {
55    SaveProfile,
56    LoadProfile,
57    ResetToDefault,
58}
59
60/// Flattened item for navigation (includes section headers)
61#[derive(Debug, Clone)]
62pub enum FlatItem {
63    /// Section header (not selectable for value changes)
64    SectionHeader(String),
65    /// Setting item reference
66    Setting { section_idx: usize, item_idx: usize },
67}
68
69impl SettingValue {
70    /// Toggle a boolean value
71    pub const fn toggle(&mut self) {
72        if let Self::Bool(b) = self {
73            *b = !*b;
74        }
75    }
76
77    /// Cycle to next choice option
78    pub const fn cycle_next(&mut self) {
79        if let Self::Choice { options, selected } = self {
80            *selected = (*selected + 1) % options.len();
81        }
82    }
83
84    /// Cycle to previous choice option
85    pub const fn cycle_prev(&mut self) {
86        if let Self::Choice { options, selected } = self {
87            if *selected == 0 {
88                *selected = options.len() - 1;
89            } else {
90                *selected -= 1;
91            }
92        }
93    }
94
95    /// Quick select a choice by index (1-based)
96    pub const fn quick_select(&mut self, index: u8) {
97        if let Self::Choice { options, selected } = self {
98            let idx = (index as usize).saturating_sub(1);
99            if idx < options.len() {
100                *selected = idx;
101            }
102        }
103    }
104
105    /// Increment a number value
106    pub fn increment(&mut self) {
107        if let Self::Number {
108            value, max, step, ..
109        } = self
110        {
111            *value = (*value + *step).min(*max);
112        }
113    }
114
115    /// Decrement a number value
116    pub fn decrement(&mut self) {
117        if let Self::Number {
118            value, min, step, ..
119        } = self
120        {
121            *value = (*value - *step).max(*min);
122        }
123    }
124
125    /// Get the current value as a display string
126    #[must_use]
127    pub fn display_value(&self) -> String {
128        match self {
129            Self::Bool(b) => if *b { "on" } else { "off" }.to_string(),
130            Self::Choice { options, selected } => {
131                options.get(*selected).cloned().unwrap_or_default()
132            }
133            Self::Number { value, .. } => value.to_string(),
134            Self::Display(s) => s.clone(),
135            Self::Action(_) => String::new(),
136        }
137    }
138
139    /// Check if this is a toggleable bool
140    #[must_use]
141    pub const fn is_bool(&self) -> bool {
142        matches!(self, Self::Bool(_))
143    }
144
145    /// Check if this is a choice/cycle value
146    #[must_use]
147    pub const fn is_choice(&self) -> bool {
148        matches!(self, Self::Choice { .. })
149    }
150
151    /// Check if this is a number value
152    #[must_use]
153    pub const fn is_number(&self) -> bool {
154        matches!(self, Self::Number { .. })
155    }
156
157    /// Check if this is an action
158    #[must_use]
159    pub const fn is_action(&self) -> bool {
160        matches!(self, Self::Action(_))
161    }
162
163    /// Check if this is a display-only value
164    #[must_use]
165    pub const fn is_display(&self) -> bool {
166        matches!(self, Self::Display(_))
167    }
168
169    /// Get choice options count (for rendering [1/2/3] hints)
170    #[must_use]
171    pub const fn choice_count(&self) -> Option<usize> {
172        if let Self::Choice { options, .. } = self {
173            Some(options.len())
174        } else {
175            None
176        }
177    }
178}
179
180impl FlatItem {
181    /// Check if this is a section header
182    #[must_use]
183    pub const fn is_header(&self) -> bool {
184        matches!(self, Self::SectionHeader(_))
185    }
186
187    /// Check if this is a setting
188    #[must_use]
189    pub const fn is_setting(&self) -> bool {
190        matches!(self, Self::Setting { .. })
191    }
192}
193
194// --- Conversions between OptionValue (core) and SettingValue (plugin) ---
195
196impl From<&OptionValue> for SettingValue {
197    /// Convert an `OptionValue` to a `SettingValue` without constraint information.
198    ///
199    /// For integers, uses unbounded min/max. Use `SettingValue::from_option_with_constraint`
200    /// for proper bounds.
201    fn from(opt: &OptionValue) -> Self {
202        match opt {
203            OptionValue::Bool(b) => Self::Bool(*b),
204            OptionValue::Integer(i) => Self::Number {
205                value: *i as i32,
206                min: i32::MIN,
207                max: i32::MAX,
208                step: 1,
209            },
210            OptionValue::String(s) => Self::Display(s.clone()),
211            OptionValue::Choice { value, choices } => Self::Choice {
212                options: choices.clone(),
213                selected: choices.iter().position(|c| c == value).unwrap_or(0),
214            },
215        }
216    }
217}
218
219impl SettingValue {
220    /// Create a `SettingValue` from an `OptionValue` with constraint information.
221    ///
222    /// Uses the constraint's min/max for integer bounds.
223    #[must_use]
224    pub fn from_option_with_constraint(opt: &OptionValue, constraint: &OptionConstraint) -> Self {
225        match opt {
226            OptionValue::Bool(b) => Self::Bool(*b),
227            OptionValue::Integer(i) => Self::Number {
228                value: *i as i32,
229                min: constraint.min.map(|v| v as i32).unwrap_or(i32::MIN),
230                max: constraint.max.map(|v| v as i32).unwrap_or(i32::MAX),
231                step: 1,
232            },
233            OptionValue::String(s) => Self::Display(s.clone()),
234            OptionValue::Choice { value, choices } => Self::Choice {
235                options: choices.clone(),
236                selected: choices.iter().position(|c| c == value).unwrap_or(0),
237            },
238        }
239    }
240
241    /// Create a `SettingItem` from an `OptionSpec` with its current value.
242    #[must_use]
243    pub fn item_from_spec(spec: &OptionSpec, value: &OptionValue) -> SettingItem {
244        SettingItem {
245            key: spec.name.to_string(),
246            label: spec.description.to_string(),
247            description: Some(spec.description.to_string()),
248            value: Self::from_option_with_constraint(value, &spec.constraint),
249        }
250    }
251
252    /// Convert this `SettingValue` back to an `OptionValue`.
253    ///
254    /// Note: `Display` and `Action` variants cannot be converted and return `None`.
255    #[must_use]
256    pub fn to_option_value(&self) -> Option<OptionValue> {
257        match self {
258            Self::Bool(b) => Some(OptionValue::Bool(*b)),
259            Self::Number { value, .. } => Some(OptionValue::Integer(i64::from(*value))),
260            Self::Choice { options, selected } => {
261                let value = options.get(*selected).cloned().unwrap_or_default();
262                Some(OptionValue::Choice {
263                    value,
264                    choices: options.clone(),
265                })
266            }
267            Self::Display(_) | Self::Action(_) => None,
268        }
269    }
270}