Skip to main content

fresh/view/settings/
entry_dialog.rs

1//! Entry detail dialog for editing complex map entries
2//!
3//! Provides a modal dialog for editing complex map entries using the same
4//! SettingItem/SettingControl infrastructure as the main settings UI.
5
6use super::items::{
7    build_item_from_value, control_to_value, ItemBoxStyle, SettingControl, SettingItem,
8};
9use super::schema::{SettingSchema, SettingType};
10use crate::view::controls::{FocusState, TextInputState};
11use rust_i18n::t;
12use serde_json::Value;
13use std::collections::{HashMap, HashSet};
14
15/// A per-field action affordance rendered at the right edge of a field's row.
16///
17/// These target different values:
18/// * `Reset` sets the field to its *built-in default* (the value the bundled
19///   config ships for this entry).
20/// * `Inherit` sets the field to `null`. It renders as `[Inherit]` when null
21///   falls back to a parent-scope value (e.g. a per-language `line_wrap`
22///   inheriting `editor.line_wrap`), or `[Clear]` when there's no such fallback
23///   and null just unsets the field (e.g. a `formatter`). See
24///   [`EntryDialogState::field_action_buttons`].
25///
26/// A field only offers the action(s) that lead to a *different* result, so a
27/// nullable field whose built-in default is itself `null` shows only the
28/// Inherit/Clear button (Reset would be identical), while a plain field with a
29/// built-in default and no inheritance chain shows only `Reset`.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum FieldAction {
32    /// Set the field to its built-in default value.
33    Reset,
34    /// Set the field to `null` — inherit a parent value, or clear it when there
35    /// is no parent to inherit from.
36    Inherit,
37}
38
39/// Simple scalar controls whose per-field action buttons join the Tab order.
40/// Composite controls (lists, maps, JSON) keep their own internal navigation,
41/// so their inherit affordance stays mouse-only.
42fn is_simple_field_control(control: &SettingControl) -> bool {
43    matches!(
44        control,
45        SettingControl::Toggle(_)
46            | SettingControl::Number(_)
47            | SettingControl::Text(_)
48            | SettingControl::Dropdown(_)
49    )
50}
51
52/// Lay out right-aligned per-field action buttons against `right_edge`
53/// (exclusive). Returns `(action, x, width)` left to right, with a one-column
54/// gap between buttons and a one-column margin at the right edge. Shared by the
55/// renderer and the click hit-tester so their geometry can't drift.
56pub fn layout_field_action_buttons(
57    buttons: &[(FieldAction, String)],
58    right_edge: u16,
59) -> Vec<(FieldAction, u16, u16)> {
60    if buttons.is_empty() {
61        return Vec::new();
62    }
63    let widths: Vec<u16> = buttons
64        .iter()
65        .map(|(_, label)| label.chars().count() as u16)
66        .collect();
67    let gaps = buttons.len().saturating_sub(1) as u16;
68    let total: u16 = widths.iter().sum::<u16>() + gaps + 1;
69    let mut x = right_edge.saturating_sub(total);
70    let mut out = Vec::with_capacity(buttons.len());
71    for ((action, _), w) in buttons.iter().zip(widths) {
72        out.push((*action, x, w));
73        x = x.saturating_add(w + 1);
74    }
75    out
76}
77
78/// State for the entry detail dialog
79#[derive(Debug, Clone)]
80pub struct EntryDialogState {
81    /// The entry key (e.g., "rust" for language)
82    pub entry_key: String,
83    /// The map path this entry belongs to (e.g., "/languages", "/lsp")
84    pub map_path: String,
85    /// Human-readable title for the dialog
86    pub title: String,
87    /// Whether this is a new entry (vs editing existing)
88    pub is_new: bool,
89    /// Items in the dialog (using same SettingItem structure as main settings)
90    pub items: Vec<SettingItem>,
91    /// Currently selected item index
92    pub selected_item: usize,
93    /// Sub-focus index within the selected item (for TextList/Map navigation)
94    pub sub_focus: Option<usize>,
95    /// Whether we're in text editing mode
96    pub editing_text: bool,
97    /// Currently focused button (0=Save, 1=Delete, 2=Cancel for existing; 0=Save, 1=Cancel for new)
98    pub focused_button: usize,
99    /// Whether focus is on buttons (true) or items (false)
100    pub focus_on_buttons: bool,
101    /// Whether deletion was requested
102    pub delete_requested: bool,
103    /// Scroll offset for the items area
104    pub scroll_offset: usize,
105    /// Last known viewport height (updated during render)
106    pub viewport_height: usize,
107    /// Hovered item index (for mouse hover feedback)
108    pub hover_item: Option<usize>,
109    /// Hovered button index (for mouse hover feedback)
110    pub hover_button: Option<usize>,
111    /// Original value when dialog was opened (for Cancel to restore)
112    pub original_value: Value,
113    /// Index of first editable item (items before this are read-only)
114    /// Used for rendering separator and focus navigation
115    pub first_editable_index: usize,
116    /// Whether deletion is disabled (for auto-managed entries like plugins)
117    pub no_delete: bool,
118    /// When true, the dialog wraps a single non-Object value (e.g., an ObjectArray).
119    /// `to_value()` returns the raw control value instead of wrapping in an Object.
120    pub is_single_value: bool,
121    /// True when the dialog edits an item in an array (constructed via
122    /// `for_array_item`); false for map entries (`from_schema`). Drives
123    /// the Delete button's label/confirmation copy so the prompt doesn't
124    /// show a numeric index as if it were a meaningful name.
125    pub is_array_item: bool,
126    /// Set to true on the first user-driven mutation (typed char,
127    /// toggled bool, list add/remove, etc.). Drives the dirty
128    /// indicator + the Esc discard prompt without relying on a
129    /// JSON-equality check that's too noisy at the schema layer.
130    pub user_edited: bool,
131    /// When `Some(i)`, keyboard focus is on the i-th per-field action button
132    /// (`[Reset]`/`[Inherit]`/`[Clear]`) of the currently selected field rather
133    /// than on the field's control — `i` indexes [`field_action_buttons`]. Tab
134    /// moves onto these buttons and Enter/Space activates them; it is the only
135    /// keyboard path to the per-field actions.
136    pub field_button_focus: Option<usize>,
137    /// Field names (item path without the leading `/`) that genuinely *inherit*
138    /// from a parent scope when unset — e.g. a per-language `line_wrap` falls
139    /// back to the global `editor.line_wrap`. For these the per-field "set to
140    /// null" button is labelled `[Inherit]`; for everything else (a `formatter`
141    /// with no global fallback) it's labelled `[Clear]`, since null just unsets
142    /// the value rather than inheriting one. Empty means "nothing inherits".
143    pub inheritable_fields: HashSet<String>,
144}
145
146impl EntryDialogState {
147    /// Create a dialog from a schema definition
148    ///
149    /// This is the primary, schema-driven constructor. It builds items
150    /// dynamically from the SettingSchema's properties using the same
151    /// build logic as the main settings UI.
152    pub fn from_schema(
153        key: String,
154        value: &Value,
155        schema: &SettingSchema,
156        map_path: &str,
157        is_new: bool,
158        no_delete: bool,
159        available_status_bar_tokens: &HashMap<String, String>,
160    ) -> Self {
161        let mut items = Vec::new();
162
163        // Add key field as first item (read-only for existing entries)
164        let key_item = SettingItem {
165            path: "__key__".to_string(),
166            name: "Key".to_string(),
167            description: Some("unique identifier for this entry".to_string()),
168            control: SettingControl::Text(TextInputState::new("Key").with_value(&key)),
169            default: None,
170            modified: false,
171            layer_source: crate::config_io::ConfigLayer::System,
172            read_only: !is_new, // Key is editable only for new entries
173            is_auto_managed: false,
174            nullable: false,
175            is_null: false,
176            section: None,
177            is_section_start: false,
178            style: ItemBoxStyle::default(),
179            dual_list_sibling: None,
180        };
181        items.push(key_item);
182
183        // Add schema-driven items from object properties
184        let is_single_value = !matches!(&schema.setting_type, SettingType::Object { .. });
185        if let SettingType::Object { properties } = &schema.setting_type {
186            for prop in properties {
187                let field_name = prop.path.trim_start_matches('/');
188                let field_value = value.get(field_name);
189                let item = build_item_from_value(prop, field_value, available_status_bar_tokens);
190                items.push(item);
191            }
192        } else {
193            // For non-object types (e.g., ObjectArray, Map), build a single item
194            // from the entire value so the dialog can render it
195            let item = build_item_from_value(schema, Some(value), available_status_bar_tokens);
196            items.push(item);
197        }
198
199        // Sort items: read-only first, then editable (stable sort preserves x-order)
200        items.sort_by_key(|item| !item.read_only);
201
202        // Compute is_section_start for section headers in entry dialogs
203        Self::compute_section_starts(&mut items);
204
205        // Find the first editable item index
206        let first_editable_index = items
207            .iter()
208            .position(|item| !item.read_only)
209            .unwrap_or(items.len());
210
211        // If all items are read-only, start with focus on buttons
212        let focus_on_buttons = first_editable_index >= items.len();
213        let selected_item = if focus_on_buttons {
214            0
215        } else {
216            first_editable_index
217        };
218
219        let title = if is_new {
220            format!("Add {}", schema.name)
221        } else {
222            format!("Edit {}", schema.name)
223        };
224
225        let mut result = Self {
226            entry_key: key,
227            map_path: map_path.to_string(),
228            title,
229            is_new,
230            items,
231            selected_item,
232            sub_focus: None,
233            editing_text: false,
234            focused_button: 0,
235            focus_on_buttons,
236            delete_requested: false,
237            scroll_offset: 0,
238            viewport_height: 20, // Default, updated during render
239            hover_item: None,
240            hover_button: None,
241            original_value: value.clone(),
242            first_editable_index,
243            no_delete,
244            is_single_value,
245            is_array_item: false,
246            user_edited: false,
247            field_button_focus: None,
248            inheritable_fields: HashSet::new(),
249        };
250        // Pre-focus the first item in any ObjectArray controls so pressing
251        // Enter opens the item editor instead of "Add new".
252        result.init_object_array_focus();
253        result
254    }
255
256    /// Create a dialog for an array item (no key field)
257    ///
258    /// Used for ObjectArray controls where items are identified by index, not key.
259    pub fn for_array_item(
260        index: Option<usize>,
261        value: &Value,
262        schema: &SettingSchema,
263        array_path: &str,
264        is_new: bool,
265        available_status_bar_tokens: &HashMap<String, String>,
266    ) -> Self {
267        let mut items = Vec::new();
268
269        // Add schema-driven items from object properties (no key field for arrays)
270        if let SettingType::Object { properties } = &schema.setting_type {
271            for prop in properties {
272                let field_name = prop.path.trim_start_matches('/');
273                let field_value = value.get(field_name);
274                let item = build_item_from_value(prop, field_value, available_status_bar_tokens);
275                items.push(item);
276            }
277        }
278
279        // Sort items: read-only first, then editable
280        items.sort_by_key(|item| !item.read_only);
281
282        // Compute is_section_start for section headers
283        Self::compute_section_starts(&mut items);
284
285        // Find the first editable item index
286        let first_editable_index = items
287            .iter()
288            .position(|item| !item.read_only)
289            .unwrap_or(items.len());
290
291        // If all items are read-only, start with focus on buttons
292        let focus_on_buttons = first_editable_index >= items.len();
293        let selected_item = if focus_on_buttons {
294            0
295        } else {
296            first_editable_index
297        };
298
299        let title = if is_new {
300            format!("Add {}", schema.name)
301        } else {
302            format!("Edit {}", schema.name)
303        };
304
305        Self {
306            entry_key: index.map_or(String::new(), |i| i.to_string()),
307            map_path: array_path.to_string(),
308            title,
309            is_new,
310            items,
311            selected_item,
312            sub_focus: None,
313            editing_text: false,
314            focused_button: 0,
315            focus_on_buttons,
316            delete_requested: false,
317            scroll_offset: 0,
318            viewport_height: 20,
319            hover_item: None,
320            hover_button: None,
321            original_value: value.clone(),
322            first_editable_index,
323            no_delete: false, // Arrays typically allow deletion
324            is_single_value: false,
325            is_array_item: true,
326            user_edited: false,
327            field_button_focus: None,
328            inheritable_fields: HashSet::new(),
329        }
330    }
331
332    /// Compute is_section_start flags for section headers.
333    /// Marks the first item in each new section so the renderer can draw headers.
334    fn compute_section_starts(items: &mut [SettingItem]) {
335        let mut last_section: Option<&str> = None;
336        for item in items.iter_mut() {
337            let current = item.section.as_deref();
338            if current.is_some() && current != last_section {
339                item.is_section_start = true;
340            }
341            if current.is_some() {
342                last_section = current;
343            }
344        }
345    }
346
347    /// Get the current key value from the key item
348    pub fn get_key(&self) -> String {
349        // Find the key item by path (may not be first after sorting)
350        for item in &self.items {
351            if item.path == "__key__" {
352                if let SettingControl::Text(state) = &item.control {
353                    return state.value.clone();
354                }
355            }
356        }
357        self.entry_key.clone()
358    }
359
360    /// Full JSON pointer path to the entry this dialog edits.
361    ///
362    /// For an existing map entry under `/universal_lsp` with key `quicklsp`,
363    /// this returns `/universal_lsp/quicklsp`. For array items, `entry_key`
364    /// is the stringified index. For brand-new map entries whose key has
365    /// not been chosen yet, this falls back to `map_path` (the parent
366    /// container) — callers are expected to avoid writing at that path.
367    ///
368    /// Nested dialogs and any pending-change paths derived from this dialog
369    /// must be rooted here — not at `map_path` — otherwise the entry key
370    /// segment is dropped and changes land under `""` in the saved config.
371    pub fn entry_path(&self) -> String {
372        // Use the live key field so new entries pick up whatever the user
373        // has typed before opening a nested dialog. For existing entries
374        // the key field is read-only and equals `entry_key`, so this is
375        // consistent with the on-disk path.
376        let key = self.get_key();
377        if key.is_empty() {
378            self.map_path.clone()
379        } else {
380            format!("{}/{}", self.map_path, key)
381        }
382    }
383
384    /// Get button count (3 for existing entries with Delete, 2 for new/no_delete entries)
385    pub fn button_count(&self) -> usize {
386        if self.is_new || self.no_delete {
387            2 // Save, Cancel (no Delete for new entries or when no_delete is set)
388        } else {
389            3
390        }
391    }
392
393    /// True when the user has made *any* change to the dialog since
394    /// it was opened. Tracked as an explicit flag (`user_edited`)
395    /// rather than comparing `to_value() != original_value`, because
396    /// the rebuilt JSON shape can differ from the input shape by
397    /// schema-default normalization (e.g. an absent optional field
398    /// rebuilds as an explicit empty string) — which would make the
399    /// dialog read as dirty at open, with no user input.
400    ///
401    /// Used to gate the Esc 'Discard changes?' prompt and to drive
402    /// the title-bar modified indicator.
403    pub fn is_dirty(&self) -> bool {
404        self.user_edited
405    }
406
407    /// Mark the dialog as edited. Called from every mutator path
408    /// (insert_char, toggle_bool, list add/remove, etc.) — anywhere
409    /// the user can produce a change the dialog should remember.
410    pub fn mark_edited(&mut self) {
411        self.user_edited = true;
412    }
413
414    /// Mark the *focused field* as explicitly edited: flags the dialog dirty
415    /// and clears the field's inherited state. Once the user gives a field a
416    /// value of their own it is no longer inherited, so `to_value` persists it
417    /// and the row shows a definite value rather than the neutral inherited
418    /// chip. Call this from value-changing mutators (not cursor moves).
419    fn mark_field_edited(&mut self) {
420        self.user_edited = true;
421        if let Some(item) = self.current_item_mut() {
422            item.is_null = false;
423            if let SettingControl::Toggle(state) = &mut item.control {
424                state.inherited = false;
425            }
426        }
427    }
428
429    /// Reset the field at `idx` to *inherited* (unset). The value falls back to
430    /// the global/default layer again: the row renders the neutral inherited
431    /// chip / `(Inherited)` badge and `to_value` omits the field. Returns true
432    /// if anything changed. No-op for read-only, non-nullable, or
433    /// already-inherited fields.
434    pub fn inherit_field(&mut self, idx: usize) -> bool {
435        let Some(item) = self.items.get_mut(idx) else {
436            return false;
437        };
438        if item.read_only || !item.nullable || item.is_null {
439            return false;
440        }
441        item.is_null = true;
442        item.modified = false;
443        if let SettingControl::Toggle(state) = &mut item.control {
444            state.inherited = true;
445        }
446        self.user_edited = true;
447        true
448    }
449
450    /// Reset the field at `idx` to its built-in default value. Returns true if
451    /// anything changed. No-op unless `reset_distinct_default` reports a
452    /// distinct, non-inherited default to reset to.
453    pub fn reset_field(&mut self, idx: usize) -> bool {
454        let Some(default) = self.reset_distinct_default(idx) else {
455            return false;
456        };
457        let Some(item) = self.items.get_mut(idx) else {
458            return false;
459        };
460        super::state::update_control_from_value(&mut item.control, &default);
461        // An explicit default value is a real (non-inherited) value.
462        item.is_null = false;
463        item.modified = false;
464        if let SettingControl::Toggle(state) = &mut item.control {
465            state.inherited = false;
466        }
467        self.user_edited = true;
468        true
469    }
470
471    /// The value `[Reset]` would set, when reset is a *distinct, meaningful*
472    /// action for the field at `idx` — i.e. a simple, editable field that
473    /// currently overrides its built-in default, where that default isn't just
474    /// `null` (which `[Inherit]` already covers). Returns `None` otherwise.
475    fn reset_distinct_default(&self, idx: usize) -> Option<Value> {
476        let item = self.items.get(idx)?;
477        // Reset is offered for simple scalar controls and for object/JSON
478        // controls (e.g. a language's `formatter`), whose only other per-field
479        // action — Inherit → null — would *clear* a non-null built-in default
480        // rather than restore it. Composite list/map controls and opaque
481        // Complex controls are excluded.
482        let resettable = is_simple_field_control(&item.control)
483            || matches!(item.control, SettingControl::Json(_));
484        if item.read_only || item.is_null || !resettable {
485            return None;
486        }
487        let default = item.default.as_ref()?;
488        // A nullable field whose default is `null` resets to the same place as
489        // Inherit, so don't offer a redundant Reset.
490        if item.nullable && default.is_null() {
491            return None;
492        }
493        // Only when the current value actually differs from the default.
494        if control_to_value(&item.control) == *default {
495            return None;
496        }
497        Some(default.clone())
498    }
499
500    /// True when unsetting the field at `idx` makes it inherit a parent-scope
501    /// value (e.g. `editor.line_wrap`) rather than simply clearing it.
502    fn field_inherits(&self, idx: usize) -> bool {
503        self.items
504            .get(idx)
505            .map(|item| {
506                self.inheritable_fields
507                    .contains(item.path.trim_start_matches('/'))
508            })
509            .unwrap_or(false)
510    }
511
512    /// The per-field action buttons to render at the right edge of the field at
513    /// `idx`, left to right, with their labels. Empty when the field offers
514    /// none (e.g. inherited/unset, at its default, or read-only). The
515    /// `(Inherited)` badge is rendered separately.
516    pub fn field_action_buttons(&self, idx: usize) -> Vec<(FieldAction, String)> {
517        let Some(item) = self.items.get(idx) else {
518            return Vec::new();
519        };
520        if item.read_only {
521            return Vec::new();
522        }
523        let mut buttons = Vec::new();
524        if self.reset_distinct_default(idx).is_some() {
525            buttons.push((
526                FieldAction::Reset,
527                format!("[{}]", t!("settings.btn_reset")),
528            ));
529        }
530        // "Set to null" is offered for any overriding nullable field (composite
531        // ones too — they're click-only, see `field_focusable_count`). The label
532        // is [Inherit] when null falls back to a parent value, else [Clear].
533        if item.nullable && !item.is_null {
534            let label = if self.field_inherits(idx) {
535                t!("settings.btn_inherit")
536            } else {
537                t!("settings.btn_clear")
538            };
539            buttons.push((FieldAction::Inherit, format!("[{}]", label)));
540        }
541        buttons
542    }
543
544    /// Perform the action button at focusable index `i` for the field at `idx`.
545    fn perform_field_action(&mut self, idx: usize, action: FieldAction) -> bool {
546        match action {
547            FieldAction::Reset => self.reset_field(idx),
548            FieldAction::Inherit => self.inherit_field(idx),
549        }
550    }
551
552    /// Activate the currently keyboard-focused field action button, if any.
553    /// Returns true if a button was focused (so the key is consumed).
554    pub fn activate_focused_field_button(&mut self) -> bool {
555        let Some(i) = self.field_button_focus else {
556            return false;
557        };
558        if self.focus_on_buttons {
559            return false;
560        }
561        let idx = self.selected_item;
562        if let Some(action) = self.field_action_buttons(idx).get(i).map(|(a, _)| *a) {
563            self.perform_field_action(idx, action);
564        }
565        self.field_button_focus = None;
566        self.update_focus_states();
567        true
568    }
569
570    /// Convert dialog state back to JSON value (excludes the __key__ item)
571    /// Auto-commit any draft text sitting in a TextList's trailing
572    /// `[+] Add new` slot. Without this, saving a dialog while the user
573    /// has typed (but not pressed Enter or ↓) into the new-item row
574    /// silently drops that text — the diverging commit semantics
575    /// between text fields ("typed value is just there") and list rows
576    /// ("typing isn't enough — you must commit") was the F21 surprise.
577    /// Run this from every save path so the saved value matches what
578    /// the user sees on screen.
579    pub fn commit_pending_list_drafts(&mut self) {
580        for item in &mut self.items {
581            if let SettingControl::TextList(state) = &mut item.control {
582                if !state.new_item_text.is_empty() {
583                    state.add_item();
584                }
585            }
586        }
587    }
588
589    pub fn to_value(&self) -> Value {
590        // For single-value dialogs (non-Object schemas like ObjectArray),
591        // return the control's value directly instead of wrapping in an Object.
592        if self.is_single_value {
593            for item in &self.items {
594                if item.path != "__key__" {
595                    return control_to_value(&item.control);
596                }
597            }
598        }
599
600        let mut obj = serde_json::Map::new();
601
602        for item in &self.items {
603            // Skip the special key item - it's stored separately
604            if item.path == "__key__" {
605                continue;
606            }
607
608            let field_name = item.path.trim_start_matches('/');
609
610            // Preserve inheritance: a nullable field whose value is inherited
611            // (`is_null`) must NOT be written back as a concrete value, or it
612            // stops inheriting from the global/default layer. `is_null` starts
613            // true for inherited fields, is cleared the moment the user edits
614            // the field (`mark_field_edited`), and is set again by the per-field
615            // Inherit action — so it precisely tracks "did the user give this
616            // field a value of its own?". Without this, opening a language entry
617            // and toggling one field would freeze every *other* inherited field
618            // (e.g. writing `line_wrap: false`), which then overrides the global
619            // Toggle Line Wrap command forever (issue #2345).
620            if item.nullable && item.is_null {
621                continue;
622            }
623
624            let value = control_to_value(&item.control);
625            obj.insert(field_name.to_string(), value);
626        }
627
628        Value::Object(obj)
629    }
630
631    /// Get currently selected item
632    pub fn current_item(&self) -> Option<&SettingItem> {
633        if self.focus_on_buttons {
634            None
635        } else {
636            self.items.get(self.selected_item)
637        }
638    }
639
640    /// Get currently selected item mutably
641    pub fn current_item_mut(&mut self) -> Option<&mut SettingItem> {
642        if self.focus_on_buttons {
643            None
644        } else {
645            self.items.get_mut(self.selected_item)
646        }
647    }
648
649    /// Move focus to next editable item, navigating within composite controls first.
650    ///
651    /// For composite controls (Map, ObjectArray, TextList), Down first navigates
652    /// through their internal entries and [+] Add new row before moving to the
653    /// next dialog item. When at the last editable item, wraps to buttons.
654    /// When on the last button, wraps back to the first editable item.
655    /// Number of focusable per-field action buttons (`[Reset]`/`[Inherit]`/
656    /// `[Clear]`) for the field at `idx`. Every field's buttons join the Tab
657    /// order — for composite controls they come *after* the control's own
658    /// internal sub-navigation (handled by `try_composite_focus_*`), so Tab is
659    /// the sole keyboard path to these actions.
660    fn field_focusable_count(&self, idx: usize) -> usize {
661        self.field_action_buttons(idx).len()
662    }
663
664    /// Advance focus to the next *field* (control), skipping any per-field
665    /// action buttons. Used by the form-style "commit and move on" flow
666    /// (Enter/Tab/arrows while editing), where stopping on the field's own
667    /// `[Reset]`/`[Inherit]` button would be surprising. Those buttons remain
668    /// reachable via Tab in navigation mode.
669    pub fn focus_next_field(&mut self) {
670        if self.editing_text {
671            return;
672        }
673        self.field_button_focus = None;
674        if self.selected_item + 1 < self.items.len() {
675            self.selected_item += 1;
676            self.sub_focus = None;
677            self.init_composite_focus(true);
678        } else {
679            self.focus_on_buttons = true;
680            self.focused_button = 0;
681        }
682        self.update_focus_states();
683        self.ensure_selected_visible(self.viewport_height);
684    }
685
686    /// Retreat focus to the previous *field* (control), skipping per-field
687    /// action buttons. The arrow-key counterpart to [`focus_next_field`].
688    pub fn focus_prev_field(&mut self) {
689        if self.editing_text {
690            return;
691        }
692        self.field_button_focus = None;
693        if self.selected_item > self.first_editable_index {
694            self.selected_item -= 1;
695            self.sub_focus = None;
696            self.init_composite_focus(false);
697        } else {
698            self.focus_on_buttons = true;
699            self.focused_button = self.button_count().saturating_sub(1);
700        }
701        self.update_focus_states();
702        self.ensure_selected_visible(self.viewport_height);
703    }
704
705    pub fn focus_next(&mut self) {
706        if self.editing_text {
707            return;
708        }
709
710        if self.focus_on_buttons {
711            if self.focused_button + 1 < self.button_count() {
712                self.focused_button += 1;
713            } else {
714                // Wrap to first editable item
715                if self.first_editable_index < self.items.len() {
716                    self.focus_on_buttons = false;
717                    self.selected_item = self.first_editable_index;
718                    self.sub_focus = None;
719                    self.field_button_focus = None;
720                    self.init_composite_focus(true);
721                }
722            }
723        } else if let Some(i) = self.field_button_focus {
724            // Advance through this field's action buttons, then to the next field.
725            if i + 1 < self.field_focusable_count(self.selected_item) {
726                self.field_button_focus = Some(i + 1);
727            } else {
728                self.field_button_focus = None;
729                if self.selected_item + 1 < self.items.len() {
730                    self.selected_item += 1;
731                    self.sub_focus = None;
732                    self.init_composite_focus(true);
733                } else {
734                    self.focus_on_buttons = true;
735                    self.focused_button = 0;
736                }
737            }
738        } else {
739            // Try navigating within a composite control first
740            let handled = self.try_composite_focus_next();
741            if !handled {
742                // Composite at its exit boundary (or not a composite). Stop on
743                // this field's action buttons before advancing, if it has any.
744                if self.field_focusable_count(self.selected_item) > 0 {
745                    self.field_button_focus = Some(0);
746                } else if self.selected_item + 1 < self.items.len() {
747                    self.selected_item += 1;
748                    self.sub_focus = None;
749                    self.init_composite_focus(true);
750                } else {
751                    // Past last item, go to buttons
752                    self.focus_on_buttons = true;
753                    self.focused_button = 0;
754                }
755            }
756        }
757
758        self.update_focus_states();
759        self.ensure_selected_visible(self.viewport_height);
760    }
761
762    /// Move focus to previous editable item, navigating within composite controls first.
763    ///
764    /// For composite controls, Up first navigates backwards through their internal
765    /// entries before moving to the previous dialog item. When at the first editable
766    /// item, wraps to buttons. When on the first button, wraps back to the last item.
767    pub fn focus_prev(&mut self) {
768        if self.editing_text {
769            return;
770        }
771
772        if self.focus_on_buttons {
773            if self.focused_button > 0 {
774                self.focused_button -= 1;
775            } else {
776                // Wrap to last editable item
777                if self.first_editable_index < self.items.len() {
778                    self.focus_on_buttons = false;
779                    self.selected_item = self.items.len().saturating_sub(1);
780                    self.sub_focus = None;
781                    self.init_composite_focus(false);
782                    // Land on the field's last action button, if it has any.
783                    self.field_button_focus = self
784                        .field_focusable_count(self.selected_item)
785                        .checked_sub(1);
786                }
787            }
788        } else if let Some(i) = self.field_button_focus {
789            // Step back through the action buttons, then to the control.
790            self.field_button_focus = i.checked_sub(1);
791        } else {
792            // Try navigating within a composite control first
793            let handled = self.try_composite_focus_prev();
794            if !handled {
795                // Composite is at its entry boundary (or not a composite) — go to previous item
796                if self.selected_item > self.first_editable_index {
797                    self.selected_item -= 1;
798                    self.sub_focus = None;
799                    self.init_composite_focus(false);
800                    // Going backwards lands on the previous field's last
801                    // element, which is its last action button when present.
802                    self.field_button_focus = self
803                        .field_focusable_count(self.selected_item)
804                        .checked_sub(1);
805                } else {
806                    // Before first editable item, go to buttons
807                    self.focus_on_buttons = true;
808                    self.focused_button = self.button_count().saturating_sub(1);
809                }
810            }
811        }
812
813        self.update_focus_states();
814        self.ensure_selected_visible(self.viewport_height);
815    }
816
817    /// Try to navigate forward within the current composite control.
818    /// Returns true if the navigation was handled internally, false if at the exit boundary.
819    fn try_composite_focus_next(&mut self) -> bool {
820        let item = match self.items.get(self.selected_item) {
821            Some(item) => item,
822            None => return false,
823        };
824        match &item.control {
825            SettingControl::Map(state) => {
826                // Map returns bool: true = handled internally, false = at boundary
827                let at_boundary = state.focused_entry.is_none(); // On add-new → exit
828                if at_boundary {
829                    return false;
830                }
831                if let Some(item) = self.items.get_mut(self.selected_item) {
832                    if let SettingControl::Map(state) = &mut item.control {
833                        return state.focus_next();
834                    }
835                }
836                false
837            }
838            SettingControl::ObjectArray(state) => {
839                // ObjectArray: None = on add-new → exit
840                if state.focused_index.is_none() {
841                    return false;
842                }
843                if let Some(item) = self.items.get_mut(self.selected_item) {
844                    if let SettingControl::ObjectArray(state) = &mut item.control {
845                        state.focus_next();
846                        return true;
847                    }
848                }
849                false
850            }
851            SettingControl::TextList(state) => {
852                // TextList: None = on add-new → exit
853                if state.focused_item.is_none() {
854                    return false;
855                }
856                if let Some(item) = self.items.get_mut(self.selected_item) {
857                    if let SettingControl::TextList(state) = &mut item.control {
858                        state.focus_next();
859                        return true;
860                    }
861                }
862                false
863            }
864            _ => false,
865        }
866    }
867
868    /// Try to navigate backward within the current composite control.
869    /// Returns true if the navigation was handled internally, false if at the entry boundary.
870    fn try_composite_focus_prev(&mut self) -> bool {
871        let item = match self.items.get(self.selected_item) {
872            Some(item) => item,
873            None => return false,
874        };
875        match &item.control {
876            SettingControl::Map(state) => {
877                // Map: Some(0) = at first entry → exit
878                let at_boundary = matches!(state.focused_entry, Some(0))
879                    || (state.focused_entry.is_none() && state.entries.is_empty());
880                if at_boundary {
881                    return false;
882                }
883                if let Some(item) = self.items.get_mut(self.selected_item) {
884                    if let SettingControl::Map(state) = &mut item.control {
885                        return state.focus_prev();
886                    }
887                }
888                false
889            }
890            SettingControl::ObjectArray(state) => {
891                // ObjectArray: Some(0) = at first entry → exit
892                if matches!(state.focused_index, Some(0))
893                    || (state.focused_index.is_none() && state.bindings.is_empty())
894                {
895                    return false;
896                }
897                if let Some(item) = self.items.get_mut(self.selected_item) {
898                    if let SettingControl::ObjectArray(state) = &mut item.control {
899                        state.focus_prev();
900                        return true;
901                    }
902                }
903                false
904            }
905            SettingControl::TextList(state) => {
906                // TextList: Some(0) = at first item → exit
907                if matches!(state.focused_item, Some(0))
908                    || (state.focused_item.is_none() && state.items.is_empty())
909                {
910                    return false;
911                }
912                if let Some(item) = self.items.get_mut(self.selected_item) {
913                    if let SettingControl::TextList(state) = &mut item.control {
914                        state.focus_prev();
915                        return true;
916                    }
917                }
918                false
919            }
920            _ => false,
921        }
922    }
923
924    /// Initialize a composite control's focus when entering it.
925    /// `from_above`: true = entering from the item above (start at first entry),
926    ///               false = entering from below (start at add-new / last entry).
927    fn init_composite_focus(&mut self, from_above: bool) {
928        if let Some(item) = self.items.get_mut(self.selected_item) {
929            match &mut item.control {
930                SettingControl::Map(state) => {
931                    state.init_focus(from_above);
932                }
933                SettingControl::ObjectArray(state) => {
934                    if from_above {
935                        state.focused_index = if state.bindings.is_empty() {
936                            None
937                        } else {
938                            Some(0)
939                        };
940                    } else {
941                        // Coming from below: start at add-new
942                        state.focused_index = None;
943                    }
944                }
945                SettingControl::TextList(state) => {
946                    if from_above {
947                        state.focused_item = if state.items.is_empty() {
948                            None
949                        } else {
950                            Some(0)
951                        };
952                    } else {
953                        // Coming from below: start at add-new
954                        state.focused_item = None;
955                    }
956                }
957                _ => {}
958            }
959        }
960    }
961
962    /// Toggle focus between items region and buttons region.
963    /// Used by Tab key to provide region-level navigation.
964    pub fn toggle_focus_region(&mut self) {
965        self.toggle_focus_region_direction(true);
966    }
967
968    /// Toggle between items and buttons regions.
969    /// When in buttons region, Tab cycles through buttons before returning to items.
970    /// `forward` controls direction: true = Tab, false = Shift+Tab.
971    pub fn toggle_focus_region_direction(&mut self, forward: bool) {
972        if self.editing_text {
973            return;
974        }
975
976        if self.focus_on_buttons {
977            if forward {
978                // Tab forward through buttons, then back to items
979                if self.focused_button + 1 < self.button_count() {
980                    self.focused_button += 1;
981                } else {
982                    // Past last button — return to items
983                    if self.first_editable_index < self.items.len() {
984                        self.focus_on_buttons = false;
985                        if self.selected_item < self.first_editable_index {
986                            self.selected_item = self.first_editable_index;
987                        }
988                    } else {
989                        // All items read-only, wrap to first button
990                        self.focused_button = 0;
991                    }
992                }
993            } else {
994                // Shift+Tab backward through buttons, then back to items
995                if self.focused_button > 0 {
996                    self.focused_button -= 1;
997                } else {
998                    // Before first button — return to items
999                    if self.first_editable_index < self.items.len() {
1000                        self.focus_on_buttons = false;
1001                        if self.selected_item < self.first_editable_index {
1002                            self.selected_item = self.first_editable_index;
1003                        }
1004                    } else {
1005                        // All items read-only, wrap to last button
1006                        self.focused_button = self.button_count().saturating_sub(1);
1007                    }
1008                }
1009            }
1010        } else {
1011            // Move to buttons
1012            self.focus_on_buttons = true;
1013            self.focused_button = if forward {
1014                0
1015            } else {
1016                self.button_count().saturating_sub(1)
1017            };
1018        }
1019
1020        self.update_focus_states();
1021        self.ensure_selected_visible(self.viewport_height);
1022    }
1023
1024    /// Initialize composite control focus for the selected item (when dialog opens)
1025    fn init_object_array_focus(&mut self) {
1026        self.init_composite_focus(true);
1027    }
1028
1029    /// Update focus states for all items
1030    pub fn update_focus_states(&mut self) {
1031        for (idx, item) in self.items.iter_mut().enumerate() {
1032            // When focus is on one of the field's action buttons, the control
1033            // itself is not the active element, so render it Normal — only the
1034            // button shows the focused highlight.
1035            let state = if !self.focus_on_buttons
1036                && idx == self.selected_item
1037                && self.field_button_focus.is_none()
1038            {
1039                FocusState::Focused
1040            } else {
1041                FocusState::Normal
1042            };
1043
1044            match &mut item.control {
1045                SettingControl::Toggle(s) => s.focus = state,
1046                SettingControl::Number(s) => s.focus = state,
1047                SettingControl::Dropdown(s) => s.focus = state,
1048                SettingControl::Text(s) => s.focus = state,
1049                SettingControl::TextList(s) => s.focus = state,
1050                SettingControl::DualList(s) => s.focus = state,
1051                SettingControl::Map(s) => s.focus = state,
1052                SettingControl::ObjectArray(s) => s.focus = state,
1053                SettingControl::Json(s) => s.focus = state,
1054                SettingControl::Complex { .. } => {}
1055            }
1056        }
1057    }
1058
1059    /// Height of a section header (label + blank line)
1060    const SECTION_HEADER_HEIGHT: usize = 2;
1061
1062    /// Calculate total content height for all items (including separator and section headers)
1063    pub fn total_content_height(&self) -> usize {
1064        let items_height: usize = self
1065            .items
1066            .iter()
1067            .map(|item| {
1068                let section_h = if item.is_section_start {
1069                    Self::SECTION_HEADER_HEIGHT
1070                } else {
1071                    0
1072                };
1073                item.control.control_height() as usize + section_h
1074            })
1075            .sum();
1076        // Add 1 for separator if we have both read-only and editable items
1077        let separator_height =
1078            if self.first_editable_index > 0 && self.first_editable_index < self.items.len() {
1079                1
1080            } else {
1081                0
1082            };
1083        items_height + separator_height
1084    }
1085
1086    /// Calculate the Y offset of the selected item (including separator and section headers)
1087    pub fn selected_item_offset(&self) -> usize {
1088        let items_offset: usize = self
1089            .items
1090            .iter()
1091            .take(self.selected_item)
1092            .map(|item| {
1093                let section_h = if item.is_section_start {
1094                    Self::SECTION_HEADER_HEIGHT
1095                } else {
1096                    0
1097                };
1098                item.control.control_height() as usize + section_h
1099            })
1100            .sum();
1101        // Add 1 for separator if selected item is after it
1102        let separator_offset = if self.first_editable_index > 0
1103            && self.first_editable_index < self.items.len()
1104            && self.selected_item >= self.first_editable_index
1105        {
1106            1
1107        } else {
1108            0
1109        };
1110        // Add section header height if the selected item itself starts a section
1111        let own_section_h = self
1112            .items
1113            .get(self.selected_item)
1114            .map(|item| {
1115                if item.is_section_start {
1116                    Self::SECTION_HEADER_HEIGHT
1117                } else {
1118                    0
1119                }
1120            })
1121            .unwrap_or(0);
1122        items_offset + separator_offset + own_section_h
1123    }
1124
1125    /// Calculate the height of the selected item
1126    pub fn selected_item_height(&self) -> usize {
1127        self.items
1128            .get(self.selected_item)
1129            .map(|item| item.control.control_height() as usize)
1130            .unwrap_or(1)
1131    }
1132
1133    /// Ensure the selected item is visible within the viewport
1134    pub fn ensure_selected_visible(&mut self, viewport_height: usize) {
1135        if self.focus_on_buttons {
1136            // Scroll to bottom when buttons are focused
1137            let total = self.total_content_height();
1138            if total > viewport_height {
1139                self.scroll_offset = total.saturating_sub(viewport_height);
1140            }
1141            return;
1142        }
1143
1144        let item_start = self.selected_item_offset();
1145        let item_end = item_start + self.selected_item_height();
1146
1147        // If item starts before viewport, scroll up
1148        if item_start < self.scroll_offset {
1149            self.scroll_offset = item_start;
1150        }
1151        // If item ends after viewport, scroll down
1152        else if item_end > self.scroll_offset + viewport_height {
1153            self.scroll_offset = item_end.saturating_sub(viewport_height);
1154        }
1155    }
1156
1157    /// Ensure the cursor within a JSON editor is visible
1158    ///
1159    /// When editing a multiline JSON control, this adjusts scroll_offset
1160    /// to keep the cursor row visible within the viewport.
1161    pub fn ensure_cursor_visible(&mut self) {
1162        if !self.editing_text || self.focus_on_buttons {
1163            return;
1164        }
1165
1166        // Get cursor row from current item (if it's a JSON editor)
1167        let cursor_row = if let Some(item) = self.items.get(self.selected_item) {
1168            if let SettingControl::Json(state) = &item.control {
1169                state.cursor_pos().0
1170            } else {
1171                return; // Not a JSON editor
1172            }
1173        } else {
1174            return;
1175        };
1176
1177        // Calculate absolute position of cursor row in content:
1178        // item_offset + 1 (for label row) + cursor_row
1179        let item_offset = self.selected_item_offset();
1180        let cursor_content_row = item_offset + 1 + cursor_row;
1181
1182        let viewport_height = self.viewport_height;
1183
1184        // If cursor is above viewport, scroll up
1185        if cursor_content_row < self.scroll_offset {
1186            self.scroll_offset = cursor_content_row;
1187        }
1188        // If cursor is below viewport, scroll down
1189        else if cursor_content_row >= self.scroll_offset + viewport_height {
1190            self.scroll_offset = cursor_content_row.saturating_sub(viewport_height) + 1;
1191        }
1192    }
1193
1194    /// Scroll up by one line
1195    pub fn scroll_up(&mut self) {
1196        self.scroll_offset = self.scroll_offset.saturating_sub(1);
1197    }
1198
1199    /// Scroll down by one line
1200    pub fn scroll_down(&mut self, viewport_height: usize) {
1201        let max_scroll = self.total_content_height().saturating_sub(viewport_height);
1202        if self.scroll_offset < max_scroll {
1203            self.scroll_offset += 1;
1204        }
1205    }
1206
1207    /// Scroll to a position based on ratio (0.0 = top, 1.0 = bottom)
1208    ///
1209    /// Used for scrollbar drag operations.
1210    pub fn scroll_to_ratio(&mut self, ratio: f32) {
1211        let max_scroll = self
1212            .total_content_height()
1213            .saturating_sub(self.viewport_height);
1214        let new_offset = (ratio * max_scroll as f32).round() as usize;
1215        self.scroll_offset = new_offset.min(max_scroll);
1216    }
1217
1218    /// Start text editing mode for the current control
1219    pub fn start_editing(&mut self) {
1220        if let Some(item) = self.current_item_mut() {
1221            // Don't allow editing read-only fields
1222            if item.read_only {
1223                return;
1224            }
1225            match &mut item.control {
1226                SettingControl::Text(state) => {
1227                    state.cursor = state.value.len();
1228                    state.editing = true;
1229                    self.editing_text = true;
1230                }
1231                SettingControl::TextList(state) => {
1232                    // If focused on a committed item, leave focus there
1233                    // and just flip into edit mode. Otherwise (focus on
1234                    // the trailing `[+] Add new` slot), explicitly
1235                    // activate input mode so the row morphs from
1236                    // `[+] Add new` into the bracketed input box.
1237                    if state.focused_item.is_none() {
1238                        state.activate_pending();
1239                    }
1240                    self.editing_text = true;
1241                }
1242                SettingControl::Number(state) => {
1243                    state.start_editing();
1244                    self.editing_text = true;
1245                }
1246                SettingControl::Json(state) => {
1247                    // Wipe the `null` placeholder so typing replaces it
1248                    // instead of concatenating onto the literal text.
1249                    state.clear_placeholder_for_edit();
1250                    self.editing_text = true;
1251                }
1252                _ => {}
1253            }
1254        }
1255    }
1256
1257    /// Stop text editing mode
1258    pub fn stop_editing(&mut self) {
1259        if let Some(item) = self.current_item_mut() {
1260            match &mut item.control {
1261                SettingControl::Number(state) => state.cancel_editing(),
1262                SettingControl::Text(state) => state.editing = false,
1263                // Cancelling on a pending list row (the trailing
1264                // [+] add-new slot) discards whatever the user typed
1265                // and collapses the row back to `[+] Add new`. Without
1266                // this, Esc was a silent no-op that left the draft
1267                // text dangling until the user committed or cleared it
1268                // manually.
1269                SettingControl::TextList(state) if state.focused_item.is_none() => {
1270                    state.cancel_pending();
1271                }
1272                // If the user opened a JSON field but didn't type
1273                // anything (or deleted everything), put the `null`
1274                // sentinel back so the value still round-trips as JSON.
1275                SettingControl::Json(state) => state.restore_unset_if_empty(),
1276                _ => {}
1277            }
1278        }
1279        self.editing_text = false;
1280    }
1281
1282    /// Handle character input
1283    pub fn insert_char(&mut self, c: char) {
1284        if !self.editing_text {
1285            return;
1286        }
1287        self.mark_field_edited();
1288        if let Some(item) = self.current_item_mut() {
1289            match &mut item.control {
1290                SettingControl::Text(state) => {
1291                    state.insert(c);
1292                }
1293                SettingControl::TextList(state) => {
1294                    state.insert(c);
1295                }
1296                SettingControl::Number(state) => {
1297                    state.insert_char(c);
1298                }
1299                SettingControl::Json(state) => {
1300                    state.insert(c);
1301                }
1302                _ => {}
1303            }
1304        }
1305    }
1306
1307    pub fn insert_str(&mut self, s: &str) {
1308        if !self.editing_text {
1309            return;
1310        }
1311        self.mark_field_edited();
1312        if let Some(item) = self.current_item_mut() {
1313            match &mut item.control {
1314                SettingControl::Text(state) => {
1315                    state.insert_str(s);
1316                }
1317                SettingControl::TextList(state) => {
1318                    state.insert_str(s);
1319                }
1320                SettingControl::Number(state) => {
1321                    for c in s.chars() {
1322                        state.insert_char(c);
1323                    }
1324                }
1325                SettingControl::Json(state) => {
1326                    state.insert_str(s);
1327                }
1328                _ => {}
1329            }
1330        }
1331    }
1332
1333    /// Handle backspace
1334    pub fn backspace(&mut self) {
1335        if !self.editing_text {
1336            return;
1337        }
1338        self.mark_field_edited();
1339        if let Some(item) = self.current_item_mut() {
1340            match &mut item.control {
1341                SettingControl::Text(state) => {
1342                    state.backspace();
1343                }
1344                SettingControl::TextList(state) => {
1345                    state.backspace();
1346                }
1347                SettingControl::Number(state) => {
1348                    state.backspace();
1349                }
1350                SettingControl::Json(state) => {
1351                    state.backspace();
1352                }
1353                _ => {}
1354            }
1355        }
1356    }
1357
1358    /// Handle cursor left
1359    pub fn cursor_left(&mut self) {
1360        if !self.editing_text {
1361            return;
1362        }
1363        if let Some(item) = self.current_item_mut() {
1364            match &mut item.control {
1365                SettingControl::Text(state) => {
1366                    state.move_left();
1367                }
1368                SettingControl::TextList(state) => {
1369                    state.move_left();
1370                }
1371                SettingControl::Json(state) => {
1372                    state.move_left();
1373                }
1374                _ => {}
1375            }
1376        }
1377    }
1378
1379    /// Handle cursor left with selection (Shift+Left)
1380    pub fn cursor_left_selecting(&mut self) {
1381        if !self.editing_text {
1382            return;
1383        }
1384        if let Some(item) = self.current_item_mut() {
1385            if let SettingControl::Json(state) = &mut item.control {
1386                state.editor.move_left_selecting();
1387            }
1388        }
1389    }
1390
1391    /// Handle cursor right
1392    pub fn cursor_right(&mut self) {
1393        if !self.editing_text {
1394            return;
1395        }
1396        if let Some(item) = self.current_item_mut() {
1397            match &mut item.control {
1398                SettingControl::Text(state) => {
1399                    state.move_right();
1400                }
1401                SettingControl::TextList(state) => {
1402                    state.move_right();
1403                }
1404                SettingControl::Json(state) => {
1405                    state.move_right();
1406                }
1407                _ => {}
1408            }
1409        }
1410    }
1411
1412    /// Handle cursor right with selection (Shift+Right)
1413    pub fn cursor_right_selecting(&mut self) {
1414        if !self.editing_text {
1415            return;
1416        }
1417        if let Some(item) = self.current_item_mut() {
1418            if let SettingControl::Json(state) = &mut item.control {
1419                state.editor.move_right_selecting();
1420            }
1421        }
1422    }
1423
1424    /// Handle cursor up (for multiline controls)
1425    pub fn cursor_up(&mut self) {
1426        if !self.editing_text {
1427            return;
1428        }
1429        if let Some(item) = self.current_item_mut() {
1430            if let SettingControl::Json(state) = &mut item.control {
1431                state.move_up();
1432            }
1433        }
1434        self.ensure_cursor_visible();
1435    }
1436
1437    /// Handle cursor up with selection (Shift+Up)
1438    pub fn cursor_up_selecting(&mut self) {
1439        if !self.editing_text {
1440            return;
1441        }
1442        if let Some(item) = self.current_item_mut() {
1443            if let SettingControl::Json(state) = &mut item.control {
1444                state.editor.move_up_selecting();
1445            }
1446        }
1447        self.ensure_cursor_visible();
1448    }
1449
1450    /// Handle cursor down (for multiline controls)
1451    pub fn cursor_down(&mut self) {
1452        if !self.editing_text {
1453            return;
1454        }
1455        if let Some(item) = self.current_item_mut() {
1456            if let SettingControl::Json(state) = &mut item.control {
1457                state.move_down();
1458            }
1459        }
1460        self.ensure_cursor_visible();
1461    }
1462
1463    /// Handle cursor down with selection (Shift+Down)
1464    pub fn cursor_down_selecting(&mut self) {
1465        if !self.editing_text {
1466            return;
1467        }
1468        if let Some(item) = self.current_item_mut() {
1469            if let SettingControl::Json(state) = &mut item.control {
1470                state.editor.move_down_selecting();
1471            }
1472        }
1473        self.ensure_cursor_visible();
1474    }
1475
1476    /// Insert newline in JSON editor
1477    pub fn insert_newline(&mut self) {
1478        self.mark_field_edited();
1479        if !self.editing_text {
1480            return;
1481        }
1482        if let Some(item) = self.current_item_mut() {
1483            if let SettingControl::Json(state) = &mut item.control {
1484                state.insert('\n');
1485            }
1486        }
1487    }
1488
1489    /// Revert JSON changes to original and stop editing
1490    pub fn revert_json_and_stop(&mut self) {
1491        if let Some(item) = self.current_item_mut() {
1492            if let SettingControl::Json(state) = &mut item.control {
1493                state.revert();
1494            }
1495        }
1496        self.editing_text = false;
1497    }
1498
1499    /// Check if current control is a JSON editor
1500    pub fn is_editing_json(&self) -> bool {
1501        if !self.editing_text {
1502            return false;
1503        }
1504        self.current_item()
1505            .map(|item| matches!(&item.control, SettingControl::Json(_)))
1506            .unwrap_or(false)
1507    }
1508
1509    /// Toggle boolean value
1510    pub fn toggle_bool(&mut self) {
1511        // Don't allow toggling read-only / non-toggle fields, and don't flag
1512        // the dialog dirty when nothing actually changes.
1513        let editable = self
1514            .current_item()
1515            .map(|i| !i.read_only && matches!(i.control, SettingControl::Toggle(_)))
1516            .unwrap_or(false);
1517        if !editable {
1518            return;
1519        }
1520        self.mark_field_edited();
1521        if let Some(item) = self.current_item_mut() {
1522            if let SettingControl::Toggle(state) = &mut item.control {
1523                state.toggle();
1524            }
1525        }
1526    }
1527
1528    /// Toggle dropdown open state
1529    pub fn toggle_dropdown(&mut self) {
1530        if let Some(item) = self.current_item_mut() {
1531            // Don't allow editing read-only fields
1532            if item.read_only {
1533                return;
1534            }
1535            if let SettingControl::Dropdown(state) = &mut item.control {
1536                state.open = !state.open;
1537            }
1538        }
1539    }
1540
1541    /// Move dropdown selection up
1542    pub fn dropdown_prev(&mut self) {
1543        self.mark_field_edited();
1544        if let Some(item) = self.current_item_mut() {
1545            if let SettingControl::Dropdown(state) = &mut item.control {
1546                if state.open {
1547                    state.select_prev();
1548                }
1549            }
1550        }
1551    }
1552
1553    /// Move dropdown selection down
1554    pub fn dropdown_next(&mut self) {
1555        self.mark_field_edited();
1556        if let Some(item) = self.current_item_mut() {
1557            if let SettingControl::Dropdown(state) = &mut item.control {
1558                if state.open {
1559                    state.select_next();
1560                }
1561            }
1562        }
1563    }
1564
1565    /// Confirm dropdown selection
1566    pub fn dropdown_confirm(&mut self) {
1567        if let Some(item) = self.current_item_mut() {
1568            if let SettingControl::Dropdown(state) = &mut item.control {
1569                state.open = false;
1570            }
1571        }
1572    }
1573
1574    /// Delete the currently focused item from a TextList control
1575    pub fn delete_list_item(&mut self) {
1576        self.mark_field_edited();
1577        if let Some(item) = self.current_item_mut() {
1578            if let SettingControl::TextList(state) = &mut item.control {
1579                // Remove the currently focused item if any
1580                if let Some(idx) = state.focused_item {
1581                    state.remove_item(idx);
1582                }
1583            }
1584        }
1585    }
1586
1587    /// Delete character at cursor (forward delete)
1588    pub fn delete(&mut self) {
1589        if !self.editing_text {
1590            return;
1591        }
1592        self.mark_field_edited();
1593        if let Some(item) = self.current_item_mut() {
1594            match &mut item.control {
1595                SettingControl::Text(state) => {
1596                    state.delete();
1597                }
1598                SettingControl::TextList(state) => {
1599                    state.delete();
1600                }
1601                SettingControl::Json(state) => {
1602                    state.delete();
1603                }
1604                _ => {}
1605            }
1606        }
1607    }
1608
1609    /// Move cursor to beginning of line
1610    pub fn cursor_home(&mut self) {
1611        if !self.editing_text {
1612            return;
1613        }
1614        if let Some(item) = self.current_item_mut() {
1615            match &mut item.control {
1616                SettingControl::Text(state) => {
1617                    state.move_home();
1618                }
1619                SettingControl::TextList(state) => {
1620                    state.move_home();
1621                }
1622                SettingControl::Json(state) => {
1623                    state.move_home();
1624                }
1625                _ => {}
1626            }
1627        }
1628    }
1629
1630    /// Move cursor to end of line
1631    pub fn cursor_end(&mut self) {
1632        if !self.editing_text {
1633            return;
1634        }
1635        if let Some(item) = self.current_item_mut() {
1636            match &mut item.control {
1637                SettingControl::Text(state) => {
1638                    state.move_end();
1639                }
1640                SettingControl::TextList(state) => {
1641                    state.move_end();
1642                }
1643                SettingControl::Json(state) => {
1644                    state.move_end();
1645                }
1646                _ => {}
1647            }
1648        }
1649    }
1650
1651    /// Select all text in current control
1652    pub fn select_all(&mut self) {
1653        if !self.editing_text {
1654            return;
1655        }
1656        if let Some(item) = self.current_item_mut() {
1657            if let SettingControl::Json(state) = &mut item.control {
1658                state.select_all();
1659            }
1660            // Note: Text and TextList don't have select_all implemented
1661        }
1662    }
1663
1664    /// Get selected text from current JSON control
1665    pub fn selected_text(&self) -> Option<String> {
1666        if !self.editing_text {
1667            return None;
1668        }
1669        if let Some(item) = self.current_item() {
1670            if let SettingControl::Json(state) = &item.control {
1671                return state.selected_text();
1672            }
1673        }
1674        None
1675    }
1676
1677    /// Check if any field is currently in edit mode
1678    pub fn is_editing(&self) -> bool {
1679        self.editing_text
1680            || self
1681                .current_item()
1682                .map(|item| {
1683                    matches!(
1684                        &item.control,
1685                        SettingControl::Dropdown(s) if s.open
1686                    )
1687                })
1688                .unwrap_or(false)
1689    }
1690}
1691
1692#[cfg(test)]
1693mod tests {
1694    use super::*;
1695
1696    fn create_test_schema() -> SettingSchema {
1697        SettingSchema {
1698            path: "/test".to_string(),
1699            name: "Test".to_string(),
1700            description: Some("Test schema".to_string()),
1701            setting_type: SettingType::Object {
1702                properties: vec![
1703                    SettingSchema {
1704                        path: "/enabled".to_string(),
1705                        name: "Enabled".to_string(),
1706                        description: Some("Enable this".to_string()),
1707                        setting_type: SettingType::Boolean,
1708                        default: Some(serde_json::json!(true)),
1709                        read_only: false,
1710                        section: None,
1711                        order: None,
1712                        nullable: false,
1713                        enum_from: None,
1714                        dual_list_sibling: None,
1715                        dynamically_extendable_status_bar_elements: false,
1716                    },
1717                    SettingSchema {
1718                        path: "/command".to_string(),
1719                        name: "Command".to_string(),
1720                        description: Some("Command to run".to_string()),
1721                        setting_type: SettingType::String,
1722                        default: Some(serde_json::json!("")),
1723                        read_only: false,
1724                        section: None,
1725                        order: None,
1726                        nullable: false,
1727                        enum_from: None,
1728                        dual_list_sibling: None,
1729                        dynamically_extendable_status_bar_elements: false,
1730                    },
1731                ],
1732            },
1733            default: None,
1734            read_only: false,
1735            section: None,
1736            order: None,
1737            nullable: false,
1738            enum_from: None,
1739            dual_list_sibling: None,
1740            dynamically_extendable_status_bar_elements: false,
1741        }
1742    }
1743
1744    #[test]
1745    fn from_schema_creates_key_item_first() {
1746        let schema = create_test_schema();
1747        let dialog = EntryDialogState::from_schema(
1748            "test".to_string(),
1749            &serde_json::json!({}),
1750            &schema,
1751            "/test",
1752            false,
1753            false,
1754            &HashMap::new(),
1755        );
1756
1757        assert!(!dialog.items.is_empty());
1758        assert_eq!(dialog.items[0].path, "__key__");
1759        assert_eq!(dialog.items[0].name, "Key");
1760    }
1761
1762    #[test]
1763    fn from_schema_creates_items_from_properties() {
1764        let schema = create_test_schema();
1765        let dialog = EntryDialogState::from_schema(
1766            "test".to_string(),
1767            &serde_json::json!({"enabled": true, "command": "test-cmd"}),
1768            &schema,
1769            "/test",
1770            false,
1771            false,
1772            &HashMap::new(),
1773        );
1774
1775        // Key + 2 properties = 3 items
1776        assert_eq!(dialog.items.len(), 3);
1777        assert_eq!(dialog.items[1].name, "Enabled");
1778        assert_eq!(dialog.items[2].name, "Command");
1779    }
1780
1781    #[test]
1782    fn get_key_returns_key_value() {
1783        let schema = create_test_schema();
1784        let dialog = EntryDialogState::from_schema(
1785            "mykey".to_string(),
1786            &serde_json::json!({}),
1787            &schema,
1788            "/test",
1789            false,
1790            false,
1791            &HashMap::new(),
1792        );
1793
1794        assert_eq!(dialog.get_key(), "mykey");
1795    }
1796
1797    #[test]
1798    fn to_value_excludes_key() {
1799        let schema = create_test_schema();
1800        let dialog = EntryDialogState::from_schema(
1801            "test".to_string(),
1802            &serde_json::json!({"enabled": true, "command": "cmd"}),
1803            &schema,
1804            "/test",
1805            false,
1806            false,
1807            &HashMap::new(),
1808        );
1809
1810        let value = dialog.to_value();
1811        assert!(value.get("__key__").is_none());
1812        assert!(value.get("enabled").is_some());
1813    }
1814
1815    #[test]
1816    fn focus_navigation_works() {
1817        let schema = create_test_schema();
1818        let mut dialog = EntryDialogState::from_schema(
1819            "test".to_string(),
1820            &serde_json::json!({}),
1821            &schema,
1822            "/test",
1823            false, // existing entry - Key is read-only
1824            false, // allow delete
1825            &HashMap::new(),
1826        );
1827
1828        // With is_new=false, Key is read-only and sorted first
1829        // Items: [Key (read-only), Enabled, Command]
1830        // Focus starts at first editable item (index 1)
1831        assert_eq!(dialog.first_editable_index, 1);
1832        assert_eq!(dialog.selected_item, 1); // First editable (Enabled)
1833        assert!(!dialog.focus_on_buttons);
1834
1835        dialog.focus_next();
1836        assert_eq!(dialog.selected_item, 2); // Command
1837
1838        dialog.focus_next();
1839        assert!(dialog.focus_on_buttons); // No more editable items
1840        assert_eq!(dialog.focused_button, 0);
1841
1842        // Going back should skip read-only Key
1843        dialog.focus_prev();
1844        assert!(!dialog.focus_on_buttons);
1845        assert_eq!(dialog.selected_item, 2); // Last editable (Command)
1846
1847        dialog.focus_prev();
1848        assert_eq!(dialog.selected_item, 1); // First editable (Enabled)
1849
1850        dialog.focus_prev();
1851        assert!(dialog.focus_on_buttons); // Wraps to buttons, not to read-only Key
1852    }
1853
1854    /// A nullable field whose built-in default is itself non-null *and* is
1855    /// currently overridden to a third value offers both `[Reset]` and
1856    /// `[Inherit]`. Tab must step control → Reset → Inherit → (footer), and
1857    /// Shift+Tab must reverse exactly: (footer) → Inherit → Reset → control.
1858    #[test]
1859    fn focus_cycles_through_both_field_action_buttons() {
1860        let schema = SettingSchema {
1861            path: "/test".to_string(),
1862            name: "Test".to_string(),
1863            description: None,
1864            setting_type: SettingType::Object {
1865                properties: vec![SettingSchema {
1866                    path: "/wrap".to_string(),
1867                    name: "Wrap".to_string(),
1868                    description: None,
1869                    setting_type: SettingType::Boolean,
1870                    // Non-null built-in default, so Reset (→true) differs from
1871                    // Inherit (→null).
1872                    default: Some(serde_json::json!(true)),
1873                    read_only: false,
1874                    section: None,
1875                    order: None,
1876                    nullable: true,
1877                    enum_from: None,
1878                    dual_list_sibling: None,
1879                    dynamically_extendable_status_bar_elements: false,
1880                }],
1881            },
1882            default: None,
1883            read_only: false,
1884            section: None,
1885            order: None,
1886            nullable: false,
1887            enum_from: None,
1888            dual_list_sibling: None,
1889            dynamically_extendable_status_bar_elements: false,
1890        };
1891        // Overridden to `false` — distinct from both the default (true) and
1892        // inherit (null).
1893        let mut dialog = EntryDialogState::from_schema(
1894            "k".to_string(),
1895            &serde_json::json!({ "wrap": false }),
1896            &schema,
1897            "/test",
1898            false,
1899            false,
1900            &HashMap::new(),
1901        );
1902
1903        // Field is index 1 (after read-only Key) and offers both buttons.
1904        assert_eq!(dialog.selected_item, 1);
1905        let buttons = dialog.field_action_buttons(1);
1906        assert_eq!(
1907            buttons.iter().map(|(a, _)| *a).collect::<Vec<_>>(),
1908            vec![FieldAction::Reset, FieldAction::Inherit]
1909        );
1910        assert_eq!(dialog.field_button_focus, None);
1911
1912        // Forward: control → Reset → Inherit → footer.
1913        dialog.focus_next();
1914        assert_eq!(dialog.field_button_focus, Some(0)); // Reset
1915        dialog.focus_next();
1916        assert_eq!(dialog.field_button_focus, Some(1)); // Inherit
1917        dialog.focus_next();
1918        assert!(dialog.focus_on_buttons);
1919
1920        // Backward: footer → Inherit → Reset → control.
1921        dialog.focus_prev();
1922        assert!(!dialog.focus_on_buttons);
1923        assert_eq!(dialog.field_button_focus, Some(1)); // Inherit
1924        dialog.focus_prev();
1925        assert_eq!(dialog.field_button_focus, Some(0)); // Reset
1926        dialog.focus_prev();
1927        assert_eq!(dialog.field_button_focus, None); // back on the control
1928        dialog.focus_prev();
1929        assert!(dialog.focus_on_buttons); // before first field → footer
1930    }
1931
1932    /// A JSON/object field (like a language `formatter`) is not a "simple"
1933    /// control, but its per-field action buttons must still be reachable by Tab
1934    /// — that's the only keyboard path now that Ctrl+R is gone.
1935    #[test]
1936    fn focus_reaches_action_buttons_on_json_field() {
1937        let schema = SettingSchema {
1938            path: "/test".to_string(),
1939            name: "Test".to_string(),
1940            description: None,
1941            setting_type: SettingType::Object {
1942                properties: vec![SettingSchema {
1943                    path: "/formatter".to_string(),
1944                    name: "Formatter".to_string(),
1945                    description: None,
1946                    // Object => rendered as a JSON control.
1947                    setting_type: SettingType::Object { properties: vec![] },
1948                    default: Some(serde_json::json!({ "command": "clang-format" })),
1949                    read_only: false,
1950                    section: None,
1951                    order: None,
1952                    nullable: true,
1953                    enum_from: None,
1954                    dual_list_sibling: None,
1955                    dynamically_extendable_status_bar_elements: false,
1956                }],
1957            },
1958            default: None,
1959            read_only: false,
1960            section: None,
1961            order: None,
1962            nullable: false,
1963            enum_from: None,
1964            dual_list_sibling: None,
1965            dynamically_extendable_status_bar_elements: false,
1966        };
1967        // Overridden to a different command, so it differs from the default.
1968        let mut dialog = EntryDialogState::from_schema(
1969            "c".to_string(),
1970            &serde_json::json!({ "formatter": { "command": "my-fmt" } }),
1971            &schema,
1972            "/languages",
1973            false,
1974            false,
1975            &HashMap::new(),
1976        );
1977
1978        // The JSON field offers buttons (at least [Reset]); Tab steps onto them.
1979        assert_eq!(dialog.selected_item, 1);
1980        assert!(
1981            !dialog.field_action_buttons(1).is_empty(),
1982            "overridden JSON field should offer action buttons"
1983        );
1984        assert_eq!(dialog.field_button_focus, None);
1985        dialog.focus_next();
1986        assert_eq!(
1987            dialog.field_button_focus,
1988            Some(0),
1989            "Tab should land on the JSON field's first action button"
1990        );
1991    }
1992
1993    #[test]
1994    fn entry_path_joins_map_path_and_entry_key() {
1995        let schema = create_test_schema();
1996
1997        // Existing entry: full path is map_path + "/" + entry_key
1998        let existing = EntryDialogState::from_schema(
1999            "rust".to_string(),
2000            &serde_json::json!({}),
2001            &schema,
2002            "/lsp",
2003            false,
2004            false,
2005            &HashMap::new(),
2006        );
2007        assert_eq!(existing.entry_path(), "/lsp/rust");
2008
2009        // New entry with no key typed yet falls back to the parent map path.
2010        // Nested dialogs keyed off this are outside the scope of this test.
2011        let new_entry = EntryDialogState::from_schema(
2012            String::new(),
2013            &serde_json::json!({}),
2014            &schema,
2015            "/lsp",
2016            true,
2017            false,
2018            &HashMap::new(),
2019        );
2020        assert_eq!(new_entry.entry_path(), "/lsp");
2021    }
2022
2023    #[test]
2024    fn entry_path_tracks_live_key_edits_for_new_entries() {
2025        let schema = create_test_schema();
2026        let mut dialog = EntryDialogState::from_schema(
2027            String::new(),
2028            &serde_json::json!({}),
2029            &schema,
2030            "/universal_lsp",
2031            true,
2032            false,
2033            &HashMap::new(),
2034        );
2035
2036        // User types a key into the editable key field.
2037        for item in dialog.items.iter_mut() {
2038            if item.path == "__key__" {
2039                if let SettingControl::Text(state) = &mut item.control {
2040                    state.value = "myserver".to_string();
2041                }
2042            }
2043        }
2044
2045        assert_eq!(dialog.entry_path(), "/universal_lsp/myserver");
2046    }
2047
2048    #[test]
2049    fn button_count_differs_for_new_vs_existing() {
2050        let schema = create_test_schema();
2051
2052        let new_dialog = EntryDialogState::from_schema(
2053            "test".to_string(),
2054            &serde_json::json!({}),
2055            &schema,
2056            "/test",
2057            true,
2058            false,
2059            &HashMap::new(),
2060        );
2061        assert_eq!(new_dialog.button_count(), 2); // Save, Cancel
2062
2063        let existing_dialog = EntryDialogState::from_schema(
2064            "test".to_string(),
2065            &serde_json::json!({}),
2066            &schema,
2067            "/test",
2068            false,
2069            false, // allow delete
2070            &HashMap::new(),
2071        );
2072        assert_eq!(existing_dialog.button_count(), 3); // Save, Delete, Cancel
2073
2074        // no_delete hides the Delete button even for existing entries
2075        let no_delete_dialog = EntryDialogState::from_schema(
2076            "test".to_string(),
2077            &serde_json::json!({}),
2078            &schema,
2079            "/test",
2080            false,
2081            true, // no delete (auto-managed entries like plugins)
2082            &HashMap::new(),
2083        );
2084        assert_eq!(no_delete_dialog.button_count(), 2); // Save, Cancel (no Delete)
2085    }
2086}