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 serde_json::Value;
12use std::collections::HashMap;
13
14/// State for the entry detail dialog
15#[derive(Debug, Clone)]
16pub struct EntryDialogState {
17    /// The entry key (e.g., "rust" for language)
18    pub entry_key: String,
19    /// The map path this entry belongs to (e.g., "/languages", "/lsp")
20    pub map_path: String,
21    /// Human-readable title for the dialog
22    pub title: String,
23    /// Whether this is a new entry (vs editing existing)
24    pub is_new: bool,
25    /// Items in the dialog (using same SettingItem structure as main settings)
26    pub items: Vec<SettingItem>,
27    /// Currently selected item index
28    pub selected_item: usize,
29    /// Sub-focus index within the selected item (for TextList/Map navigation)
30    pub sub_focus: Option<usize>,
31    /// Whether we're in text editing mode
32    pub editing_text: bool,
33    /// Currently focused button (0=Save, 1=Delete, 2=Cancel for existing; 0=Save, 1=Cancel for new)
34    pub focused_button: usize,
35    /// Whether focus is on buttons (true) or items (false)
36    pub focus_on_buttons: bool,
37    /// Whether deletion was requested
38    pub delete_requested: bool,
39    /// Scroll offset for the items area
40    pub scroll_offset: usize,
41    /// Last known viewport height (updated during render)
42    pub viewport_height: usize,
43    /// Hovered item index (for mouse hover feedback)
44    pub hover_item: Option<usize>,
45    /// Hovered button index (for mouse hover feedback)
46    pub hover_button: Option<usize>,
47    /// Original value when dialog was opened (for Cancel to restore)
48    pub original_value: Value,
49    /// Index of first editable item (items before this are read-only)
50    /// Used for rendering separator and focus navigation
51    pub first_editable_index: usize,
52    /// Whether deletion is disabled (for auto-managed entries like plugins)
53    pub no_delete: bool,
54    /// When true, the dialog wraps a single non-Object value (e.g., an ObjectArray).
55    /// `to_value()` returns the raw control value instead of wrapping in an Object.
56    pub is_single_value: bool,
57}
58
59impl EntryDialogState {
60    /// Create a dialog from a schema definition
61    ///
62    /// This is the primary, schema-driven constructor. It builds items
63    /// dynamically from the SettingSchema's properties using the same
64    /// build logic as the main settings UI.
65    pub fn from_schema(
66        key: String,
67        value: &Value,
68        schema: &SettingSchema,
69        map_path: &str,
70        is_new: bool,
71        no_delete: bool,
72        available_status_bar_tokens: &HashMap<String, String>,
73    ) -> Self {
74        let mut items = Vec::new();
75
76        // Add key field as first item (read-only for existing entries)
77        let key_item = SettingItem {
78            path: "__key__".to_string(),
79            name: "Key".to_string(),
80            description: Some("unique identifier for this entry".to_string()),
81            control: SettingControl::Text(TextInputState::new("Key").with_value(&key)),
82            default: None,
83            modified: false,
84            layer_source: crate::config_io::ConfigLayer::System,
85            read_only: !is_new, // Key is editable only for new entries
86            is_auto_managed: false,
87            nullable: false,
88            is_null: false,
89            section: None,
90            is_section_start: false,
91            style: ItemBoxStyle::default(),
92            dual_list_sibling: None,
93        };
94        items.push(key_item);
95
96        // Add schema-driven items from object properties
97        let is_single_value = !matches!(&schema.setting_type, SettingType::Object { .. });
98        if let SettingType::Object { properties } = &schema.setting_type {
99            for prop in properties {
100                let field_name = prop.path.trim_start_matches('/');
101                let field_value = value.get(field_name);
102                let item = build_item_from_value(prop, field_value, available_status_bar_tokens);
103                items.push(item);
104            }
105        } else {
106            // For non-object types (e.g., ObjectArray, Map), build a single item
107            // from the entire value so the dialog can render it
108            let item = build_item_from_value(schema, Some(value), available_status_bar_tokens);
109            items.push(item);
110        }
111
112        // Sort items: read-only first, then editable (stable sort preserves x-order)
113        items.sort_by_key(|item| !item.read_only);
114
115        // Compute is_section_start for section headers in entry dialogs
116        Self::compute_section_starts(&mut items);
117
118        // Find the first editable item index
119        let first_editable_index = items
120            .iter()
121            .position(|item| !item.read_only)
122            .unwrap_or(items.len());
123
124        // If all items are read-only, start with focus on buttons
125        let focus_on_buttons = first_editable_index >= items.len();
126        let selected_item = if focus_on_buttons {
127            0
128        } else {
129            first_editable_index
130        };
131
132        let title = if is_new {
133            format!("Add {}", schema.name)
134        } else {
135            format!("Edit {}", schema.name)
136        };
137
138        let mut result = Self {
139            entry_key: key,
140            map_path: map_path.to_string(),
141            title,
142            is_new,
143            items,
144            selected_item,
145            sub_focus: None,
146            editing_text: false,
147            focused_button: 0,
148            focus_on_buttons,
149            delete_requested: false,
150            scroll_offset: 0,
151            viewport_height: 20, // Default, updated during render
152            hover_item: None,
153            hover_button: None,
154            original_value: value.clone(),
155            first_editable_index,
156            no_delete,
157            is_single_value,
158        };
159        // Pre-focus the first item in any ObjectArray controls so pressing
160        // Enter opens the item editor instead of "Add new".
161        result.init_object_array_focus();
162        result
163    }
164
165    /// Create a dialog for an array item (no key field)
166    ///
167    /// Used for ObjectArray controls where items are identified by index, not key.
168    pub fn for_array_item(
169        index: Option<usize>,
170        value: &Value,
171        schema: &SettingSchema,
172        array_path: &str,
173        is_new: bool,
174        available_status_bar_tokens: &HashMap<String, String>,
175    ) -> Self {
176        let mut items = Vec::new();
177
178        // Add schema-driven items from object properties (no key field for arrays)
179        if let SettingType::Object { properties } = &schema.setting_type {
180            for prop in properties {
181                let field_name = prop.path.trim_start_matches('/');
182                let field_value = value.get(field_name);
183                let item = build_item_from_value(prop, field_value, available_status_bar_tokens);
184                items.push(item);
185            }
186        }
187
188        // Sort items: read-only first, then editable
189        items.sort_by_key(|item| !item.read_only);
190
191        // Compute is_section_start for section headers
192        Self::compute_section_starts(&mut items);
193
194        // Find the first editable item index
195        let first_editable_index = items
196            .iter()
197            .position(|item| !item.read_only)
198            .unwrap_or(items.len());
199
200        // If all items are read-only, start with focus on buttons
201        let focus_on_buttons = first_editable_index >= items.len();
202        let selected_item = if focus_on_buttons {
203            0
204        } else {
205            first_editable_index
206        };
207
208        let title = if is_new {
209            format!("Add {}", schema.name)
210        } else {
211            format!("Edit {}", schema.name)
212        };
213
214        Self {
215            entry_key: index.map_or(String::new(), |i| i.to_string()),
216            map_path: array_path.to_string(),
217            title,
218            is_new,
219            items,
220            selected_item,
221            sub_focus: None,
222            editing_text: false,
223            focused_button: 0,
224            focus_on_buttons,
225            delete_requested: false,
226            scroll_offset: 0,
227            viewport_height: 20,
228            hover_item: None,
229            hover_button: None,
230            original_value: value.clone(),
231            first_editable_index,
232            no_delete: false, // Arrays typically allow deletion
233            is_single_value: false,
234        }
235    }
236
237    /// Compute is_section_start flags for section headers.
238    /// Marks the first item in each new section so the renderer can draw headers.
239    fn compute_section_starts(items: &mut [SettingItem]) {
240        let mut last_section: Option<&str> = None;
241        for item in items.iter_mut() {
242            let current = item.section.as_deref();
243            if current.is_some() && current != last_section {
244                item.is_section_start = true;
245            }
246            if current.is_some() {
247                last_section = current;
248            }
249        }
250    }
251
252    /// Get the current key value from the key item
253    pub fn get_key(&self) -> String {
254        // Find the key item by path (may not be first after sorting)
255        for item in &self.items {
256            if item.path == "__key__" {
257                if let SettingControl::Text(state) = &item.control {
258                    return state.value.clone();
259                }
260            }
261        }
262        self.entry_key.clone()
263    }
264
265    /// Full JSON pointer path to the entry this dialog edits.
266    ///
267    /// For an existing map entry under `/universal_lsp` with key `quicklsp`,
268    /// this returns `/universal_lsp/quicklsp`. For array items, `entry_key`
269    /// is the stringified index. For brand-new map entries whose key has
270    /// not been chosen yet, this falls back to `map_path` (the parent
271    /// container) — callers are expected to avoid writing at that path.
272    ///
273    /// Nested dialogs and any pending-change paths derived from this dialog
274    /// must be rooted here — not at `map_path` — otherwise the entry key
275    /// segment is dropped and changes land under `""` in the saved config.
276    pub fn entry_path(&self) -> String {
277        // Use the live key field so new entries pick up whatever the user
278        // has typed before opening a nested dialog. For existing entries
279        // the key field is read-only and equals `entry_key`, so this is
280        // consistent with the on-disk path.
281        let key = self.get_key();
282        if key.is_empty() {
283            self.map_path.clone()
284        } else {
285            format!("{}/{}", self.map_path, key)
286        }
287    }
288
289    /// Get button count (3 for existing entries with Delete, 2 for new/no_delete entries)
290    pub fn button_count(&self) -> usize {
291        if self.is_new || self.no_delete {
292            2 // Save, Cancel (no Delete for new entries or when no_delete is set)
293        } else {
294            3
295        }
296    }
297
298    /// Convert dialog state back to JSON value (excludes the __key__ item)
299    pub fn to_value(&self) -> Value {
300        // For single-value dialogs (non-Object schemas like ObjectArray),
301        // return the control's value directly instead of wrapping in an Object.
302        if self.is_single_value {
303            for item in &self.items {
304                if item.path != "__key__" {
305                    return control_to_value(&item.control);
306                }
307            }
308        }
309
310        let mut obj = serde_json::Map::new();
311
312        for item in &self.items {
313            // Skip the special key item - it's stored separately
314            if item.path == "__key__" {
315                continue;
316            }
317
318            let field_name = item.path.trim_start_matches('/');
319            let value = control_to_value(&item.control);
320            obj.insert(field_name.to_string(), value);
321        }
322
323        Value::Object(obj)
324    }
325
326    /// Get currently selected item
327    pub fn current_item(&self) -> Option<&SettingItem> {
328        if self.focus_on_buttons {
329            None
330        } else {
331            self.items.get(self.selected_item)
332        }
333    }
334
335    /// Get currently selected item mutably
336    pub fn current_item_mut(&mut self) -> Option<&mut SettingItem> {
337        if self.focus_on_buttons {
338            None
339        } else {
340            self.items.get_mut(self.selected_item)
341        }
342    }
343
344    /// Move focus to next editable item, navigating within composite controls first.
345    ///
346    /// For composite controls (Map, ObjectArray, TextList), Down first navigates
347    /// through their internal entries and [+] Add new row before moving to the
348    /// next dialog item. When at the last editable item, wraps to buttons.
349    /// When on the last button, wraps back to the first editable item.
350    pub fn focus_next(&mut self) {
351        if self.editing_text {
352            return;
353        }
354
355        if self.focus_on_buttons {
356            if self.focused_button + 1 < self.button_count() {
357                self.focused_button += 1;
358            } else {
359                // Wrap to first editable item
360                if self.first_editable_index < self.items.len() {
361                    self.focus_on_buttons = false;
362                    self.selected_item = self.first_editable_index;
363                    self.sub_focus = None;
364                    self.init_composite_focus(true);
365                }
366            }
367        } else {
368            // Try navigating within a composite control first
369            let handled = self.try_composite_focus_next();
370            if !handled {
371                // Composite is at its exit boundary (or not a composite) — advance to next item
372                if self.selected_item + 1 < self.items.len() {
373                    self.selected_item += 1;
374                    self.sub_focus = None;
375                    self.init_composite_focus(true);
376                } else {
377                    // Past last item, go to buttons
378                    self.focus_on_buttons = true;
379                    self.focused_button = 0;
380                }
381            }
382        }
383
384        self.update_focus_states();
385        self.ensure_selected_visible(self.viewport_height);
386    }
387
388    /// Move focus to previous editable item, navigating within composite controls first.
389    ///
390    /// For composite controls, Up first navigates backwards through their internal
391    /// entries before moving to the previous dialog item. When at the first editable
392    /// item, wraps to buttons. When on the first button, wraps back to the last item.
393    pub fn focus_prev(&mut self) {
394        if self.editing_text {
395            return;
396        }
397
398        if self.focus_on_buttons {
399            if self.focused_button > 0 {
400                self.focused_button -= 1;
401            } else {
402                // Wrap to last editable item
403                if self.first_editable_index < self.items.len() {
404                    self.focus_on_buttons = false;
405                    self.selected_item = self.items.len().saturating_sub(1);
406                    self.sub_focus = None;
407                    self.init_composite_focus(false);
408                }
409            }
410        } else {
411            // Try navigating within a composite control first
412            let handled = self.try_composite_focus_prev();
413            if !handled {
414                // Composite is at its entry boundary (or not a composite) — go to previous item
415                if self.selected_item > self.first_editable_index {
416                    self.selected_item -= 1;
417                    self.sub_focus = None;
418                    self.init_composite_focus(false);
419                } else {
420                    // Before first editable item, go to buttons
421                    self.focus_on_buttons = true;
422                    self.focused_button = self.button_count().saturating_sub(1);
423                }
424            }
425        }
426
427        self.update_focus_states();
428        self.ensure_selected_visible(self.viewport_height);
429    }
430
431    /// Try to navigate forward within the current composite control.
432    /// Returns true if the navigation was handled internally, false if at the exit boundary.
433    fn try_composite_focus_next(&mut self) -> bool {
434        let item = match self.items.get(self.selected_item) {
435            Some(item) => item,
436            None => return false,
437        };
438        match &item.control {
439            SettingControl::Map(state) => {
440                // Map returns bool: true = handled internally, false = at boundary
441                let at_boundary = state.focused_entry.is_none(); // On add-new → exit
442                if at_boundary {
443                    return false;
444                }
445                if let Some(item) = self.items.get_mut(self.selected_item) {
446                    if let SettingControl::Map(state) = &mut item.control {
447                        return state.focus_next();
448                    }
449                }
450                false
451            }
452            SettingControl::ObjectArray(state) => {
453                // ObjectArray: None = on add-new → exit
454                if state.focused_index.is_none() {
455                    return false;
456                }
457                if let Some(item) = self.items.get_mut(self.selected_item) {
458                    if let SettingControl::ObjectArray(state) = &mut item.control {
459                        state.focus_next();
460                        return true;
461                    }
462                }
463                false
464            }
465            SettingControl::TextList(state) => {
466                // TextList: None = on add-new → exit
467                if state.focused_item.is_none() {
468                    return false;
469                }
470                if let Some(item) = self.items.get_mut(self.selected_item) {
471                    if let SettingControl::TextList(state) = &mut item.control {
472                        state.focus_next();
473                        return true;
474                    }
475                }
476                false
477            }
478            _ => false,
479        }
480    }
481
482    /// Try to navigate backward within the current composite control.
483    /// Returns true if the navigation was handled internally, false if at the entry boundary.
484    fn try_composite_focus_prev(&mut self) -> bool {
485        let item = match self.items.get(self.selected_item) {
486            Some(item) => item,
487            None => return false,
488        };
489        match &item.control {
490            SettingControl::Map(state) => {
491                // Map: Some(0) = at first entry → exit
492                let at_boundary = matches!(state.focused_entry, Some(0))
493                    || (state.focused_entry.is_none() && state.entries.is_empty());
494                if at_boundary {
495                    return false;
496                }
497                if let Some(item) = self.items.get_mut(self.selected_item) {
498                    if let SettingControl::Map(state) = &mut item.control {
499                        return state.focus_prev();
500                    }
501                }
502                false
503            }
504            SettingControl::ObjectArray(state) => {
505                // ObjectArray: Some(0) = at first entry → exit
506                if matches!(state.focused_index, Some(0))
507                    || (state.focused_index.is_none() && state.bindings.is_empty())
508                {
509                    return false;
510                }
511                if let Some(item) = self.items.get_mut(self.selected_item) {
512                    if let SettingControl::ObjectArray(state) = &mut item.control {
513                        state.focus_prev();
514                        return true;
515                    }
516                }
517                false
518            }
519            SettingControl::TextList(state) => {
520                // TextList: Some(0) = at first item → exit
521                if matches!(state.focused_item, Some(0))
522                    || (state.focused_item.is_none() && state.items.is_empty())
523                {
524                    return false;
525                }
526                if let Some(item) = self.items.get_mut(self.selected_item) {
527                    if let SettingControl::TextList(state) = &mut item.control {
528                        state.focus_prev();
529                        return true;
530                    }
531                }
532                false
533            }
534            _ => false,
535        }
536    }
537
538    /// Initialize a composite control's focus when entering it.
539    /// `from_above`: true = entering from the item above (start at first entry),
540    ///               false = entering from below (start at add-new / last entry).
541    fn init_composite_focus(&mut self, from_above: bool) {
542        if let Some(item) = self.items.get_mut(self.selected_item) {
543            match &mut item.control {
544                SettingControl::Map(state) => {
545                    state.init_focus(from_above);
546                }
547                SettingControl::ObjectArray(state) => {
548                    if from_above {
549                        state.focused_index = if state.bindings.is_empty() {
550                            None
551                        } else {
552                            Some(0)
553                        };
554                    } else {
555                        // Coming from below: start at add-new
556                        state.focused_index = None;
557                    }
558                }
559                SettingControl::TextList(state) => {
560                    if from_above {
561                        state.focused_item = if state.items.is_empty() {
562                            None
563                        } else {
564                            Some(0)
565                        };
566                    } else {
567                        // Coming from below: start at add-new
568                        state.focused_item = None;
569                    }
570                }
571                _ => {}
572            }
573        }
574    }
575
576    /// Toggle focus between items region and buttons region.
577    /// Used by Tab key to provide region-level navigation.
578    pub fn toggle_focus_region(&mut self) {
579        self.toggle_focus_region_direction(true);
580    }
581
582    /// Toggle between items and buttons regions.
583    /// When in buttons region, Tab cycles through buttons before returning to items.
584    /// `forward` controls direction: true = Tab, false = Shift+Tab.
585    pub fn toggle_focus_region_direction(&mut self, forward: bool) {
586        if self.editing_text {
587            return;
588        }
589
590        if self.focus_on_buttons {
591            if forward {
592                // Tab forward through buttons, then back to items
593                if self.focused_button + 1 < self.button_count() {
594                    self.focused_button += 1;
595                } else {
596                    // Past last button — return to items
597                    if self.first_editable_index < self.items.len() {
598                        self.focus_on_buttons = false;
599                        if self.selected_item < self.first_editable_index {
600                            self.selected_item = self.first_editable_index;
601                        }
602                    } else {
603                        // All items read-only, wrap to first button
604                        self.focused_button = 0;
605                    }
606                }
607            } else {
608                // Shift+Tab backward through buttons, then back to items
609                if self.focused_button > 0 {
610                    self.focused_button -= 1;
611                } else {
612                    // Before first button — return to items
613                    if self.first_editable_index < self.items.len() {
614                        self.focus_on_buttons = false;
615                        if self.selected_item < self.first_editable_index {
616                            self.selected_item = self.first_editable_index;
617                        }
618                    } else {
619                        // All items read-only, wrap to last button
620                        self.focused_button = self.button_count().saturating_sub(1);
621                    }
622                }
623            }
624        } else {
625            // Move to buttons
626            self.focus_on_buttons = true;
627            self.focused_button = if forward {
628                0
629            } else {
630                self.button_count().saturating_sub(1)
631            };
632        }
633
634        self.update_focus_states();
635        self.ensure_selected_visible(self.viewport_height);
636    }
637
638    /// Initialize composite control focus for the selected item (when dialog opens)
639    fn init_object_array_focus(&mut self) {
640        self.init_composite_focus(true);
641    }
642
643    /// Update focus states for all items
644    pub fn update_focus_states(&mut self) {
645        for (idx, item) in self.items.iter_mut().enumerate() {
646            let state = if !self.focus_on_buttons && idx == self.selected_item {
647                FocusState::Focused
648            } else {
649                FocusState::Normal
650            };
651
652            match &mut item.control {
653                SettingControl::Toggle(s) => s.focus = state,
654                SettingControl::Number(s) => s.focus = state,
655                SettingControl::Dropdown(s) => s.focus = state,
656                SettingControl::Text(s) => s.focus = state,
657                SettingControl::TextList(s) => s.focus = state,
658                SettingControl::DualList(s) => s.focus = state,
659                SettingControl::Map(s) => s.focus = state,
660                SettingControl::ObjectArray(s) => s.focus = state,
661                SettingControl::Json(s) => s.focus = state,
662                SettingControl::Complex { .. } => {}
663            }
664        }
665    }
666
667    /// Height of a section header (label + blank line)
668    const SECTION_HEADER_HEIGHT: usize = 2;
669
670    /// Calculate total content height for all items (including separator and section headers)
671    pub fn total_content_height(&self) -> usize {
672        let items_height: usize = self
673            .items
674            .iter()
675            .map(|item| {
676                let section_h = if item.is_section_start {
677                    Self::SECTION_HEADER_HEIGHT
678                } else {
679                    0
680                };
681                item.control.control_height() as usize + section_h
682            })
683            .sum();
684        // Add 1 for separator if we have both read-only and editable items
685        let separator_height =
686            if self.first_editable_index > 0 && self.first_editable_index < self.items.len() {
687                1
688            } else {
689                0
690            };
691        items_height + separator_height
692    }
693
694    /// Calculate the Y offset of the selected item (including separator and section headers)
695    pub fn selected_item_offset(&self) -> usize {
696        let items_offset: usize = self
697            .items
698            .iter()
699            .take(self.selected_item)
700            .map(|item| {
701                let section_h = if item.is_section_start {
702                    Self::SECTION_HEADER_HEIGHT
703                } else {
704                    0
705                };
706                item.control.control_height() as usize + section_h
707            })
708            .sum();
709        // Add 1 for separator if selected item is after it
710        let separator_offset = if self.first_editable_index > 0
711            && self.first_editable_index < self.items.len()
712            && self.selected_item >= self.first_editable_index
713        {
714            1
715        } else {
716            0
717        };
718        // Add section header height if the selected item itself starts a section
719        let own_section_h = self
720            .items
721            .get(self.selected_item)
722            .map(|item| {
723                if item.is_section_start {
724                    Self::SECTION_HEADER_HEIGHT
725                } else {
726                    0
727                }
728            })
729            .unwrap_or(0);
730        items_offset + separator_offset + own_section_h
731    }
732
733    /// Calculate the height of the selected item
734    pub fn selected_item_height(&self) -> usize {
735        self.items
736            .get(self.selected_item)
737            .map(|item| item.control.control_height() as usize)
738            .unwrap_or(1)
739    }
740
741    /// Ensure the selected item is visible within the viewport
742    pub fn ensure_selected_visible(&mut self, viewport_height: usize) {
743        if self.focus_on_buttons {
744            // Scroll to bottom when buttons are focused
745            let total = self.total_content_height();
746            if total > viewport_height {
747                self.scroll_offset = total.saturating_sub(viewport_height);
748            }
749            return;
750        }
751
752        let item_start = self.selected_item_offset();
753        let item_end = item_start + self.selected_item_height();
754
755        // If item starts before viewport, scroll up
756        if item_start < self.scroll_offset {
757            self.scroll_offset = item_start;
758        }
759        // If item ends after viewport, scroll down
760        else if item_end > self.scroll_offset + viewport_height {
761            self.scroll_offset = item_end.saturating_sub(viewport_height);
762        }
763    }
764
765    /// Ensure the cursor within a JSON editor is visible
766    ///
767    /// When editing a multiline JSON control, this adjusts scroll_offset
768    /// to keep the cursor row visible within the viewport.
769    pub fn ensure_cursor_visible(&mut self) {
770        if !self.editing_text || self.focus_on_buttons {
771            return;
772        }
773
774        // Get cursor row from current item (if it's a JSON editor)
775        let cursor_row = if let Some(item) = self.items.get(self.selected_item) {
776            if let SettingControl::Json(state) = &item.control {
777                state.cursor_pos().0
778            } else {
779                return; // Not a JSON editor
780            }
781        } else {
782            return;
783        };
784
785        // Calculate absolute position of cursor row in content:
786        // item_offset + 1 (for label row) + cursor_row
787        let item_offset = self.selected_item_offset();
788        let cursor_content_row = item_offset + 1 + cursor_row;
789
790        let viewport_height = self.viewport_height;
791
792        // If cursor is above viewport, scroll up
793        if cursor_content_row < self.scroll_offset {
794            self.scroll_offset = cursor_content_row;
795        }
796        // If cursor is below viewport, scroll down
797        else if cursor_content_row >= self.scroll_offset + viewport_height {
798            self.scroll_offset = cursor_content_row.saturating_sub(viewport_height) + 1;
799        }
800    }
801
802    /// Scroll up by one line
803    pub fn scroll_up(&mut self) {
804        self.scroll_offset = self.scroll_offset.saturating_sub(1);
805    }
806
807    /// Scroll down by one line
808    pub fn scroll_down(&mut self, viewport_height: usize) {
809        let max_scroll = self.total_content_height().saturating_sub(viewport_height);
810        if self.scroll_offset < max_scroll {
811            self.scroll_offset += 1;
812        }
813    }
814
815    /// Scroll to a position based on ratio (0.0 = top, 1.0 = bottom)
816    ///
817    /// Used for scrollbar drag operations.
818    pub fn scroll_to_ratio(&mut self, ratio: f32) {
819        let max_scroll = self
820            .total_content_height()
821            .saturating_sub(self.viewport_height);
822        let new_offset = (ratio * max_scroll as f32).round() as usize;
823        self.scroll_offset = new_offset.min(max_scroll);
824    }
825
826    /// Start text editing mode for the current control
827    pub fn start_editing(&mut self) {
828        if let Some(item) = self.current_item_mut() {
829            // Don't allow editing read-only fields
830            if item.read_only {
831                return;
832            }
833            match &mut item.control {
834                SettingControl::Text(state) => {
835                    state.cursor = state.value.len();
836                    state.editing = true;
837                    self.editing_text = true;
838                }
839                SettingControl::TextList(state) => {
840                    // Focus on the new item input by default
841                    state.focus_new_item();
842                    self.editing_text = true;
843                }
844                SettingControl::Number(state) => {
845                    state.start_editing();
846                    self.editing_text = true;
847                }
848                SettingControl::Json(_) => {
849                    // JSON editor is always ready to edit, just set the flag
850                    self.editing_text = true;
851                }
852                _ => {}
853            }
854        }
855    }
856
857    /// Stop text editing mode
858    pub fn stop_editing(&mut self) {
859        if let Some(item) = self.current_item_mut() {
860            match &mut item.control {
861                SettingControl::Number(state) => state.cancel_editing(),
862                SettingControl::Text(state) => state.editing = false,
863                _ => {}
864            }
865        }
866        self.editing_text = false;
867    }
868
869    /// Handle character input
870    pub fn insert_char(&mut self, c: char) {
871        if !self.editing_text {
872            return;
873        }
874        if let Some(item) = self.current_item_mut() {
875            match &mut item.control {
876                SettingControl::Text(state) => {
877                    state.insert(c);
878                }
879                SettingControl::TextList(state) => {
880                    state.insert(c);
881                }
882                SettingControl::Number(state) => {
883                    state.insert_char(c);
884                }
885                SettingControl::Json(state) => {
886                    state.insert(c);
887                }
888                _ => {}
889            }
890        }
891    }
892
893    pub fn insert_str(&mut self, s: &str) {
894        if !self.editing_text {
895            return;
896        }
897        if let Some(item) = self.current_item_mut() {
898            match &mut item.control {
899                SettingControl::Text(state) => {
900                    state.insert_str(s);
901                }
902                SettingControl::TextList(state) => {
903                    state.insert_str(s);
904                }
905                SettingControl::Number(state) => {
906                    for c in s.chars() {
907                        state.insert_char(c);
908                    }
909                }
910                SettingControl::Json(state) => {
911                    state.insert_str(s);
912                }
913                _ => {}
914            }
915        }
916    }
917
918    /// Handle backspace
919    pub fn backspace(&mut self) {
920        if !self.editing_text {
921            return;
922        }
923        if let Some(item) = self.current_item_mut() {
924            match &mut item.control {
925                SettingControl::Text(state) => {
926                    state.backspace();
927                }
928                SettingControl::TextList(state) => {
929                    state.backspace();
930                }
931                SettingControl::Number(state) => {
932                    state.backspace();
933                }
934                SettingControl::Json(state) => {
935                    state.backspace();
936                }
937                _ => {}
938            }
939        }
940    }
941
942    /// Handle cursor left
943    pub fn cursor_left(&mut self) {
944        if !self.editing_text {
945            return;
946        }
947        if let Some(item) = self.current_item_mut() {
948            match &mut item.control {
949                SettingControl::Text(state) => {
950                    state.move_left();
951                }
952                SettingControl::TextList(state) => {
953                    state.move_left();
954                }
955                SettingControl::Json(state) => {
956                    state.move_left();
957                }
958                _ => {}
959            }
960        }
961    }
962
963    /// Handle cursor left with selection (Shift+Left)
964    pub fn cursor_left_selecting(&mut self) {
965        if !self.editing_text {
966            return;
967        }
968        if let Some(item) = self.current_item_mut() {
969            if let SettingControl::Json(state) = &mut item.control {
970                state.editor.move_left_selecting();
971            }
972        }
973    }
974
975    /// Handle cursor right
976    pub fn cursor_right(&mut self) {
977        if !self.editing_text {
978            return;
979        }
980        if let Some(item) = self.current_item_mut() {
981            match &mut item.control {
982                SettingControl::Text(state) => {
983                    state.move_right();
984                }
985                SettingControl::TextList(state) => {
986                    state.move_right();
987                }
988                SettingControl::Json(state) => {
989                    state.move_right();
990                }
991                _ => {}
992            }
993        }
994    }
995
996    /// Handle cursor right with selection (Shift+Right)
997    pub fn cursor_right_selecting(&mut self) {
998        if !self.editing_text {
999            return;
1000        }
1001        if let Some(item) = self.current_item_mut() {
1002            if let SettingControl::Json(state) = &mut item.control {
1003                state.editor.move_right_selecting();
1004            }
1005        }
1006    }
1007
1008    /// Handle cursor up (for multiline controls)
1009    pub fn cursor_up(&mut self) {
1010        if !self.editing_text {
1011            return;
1012        }
1013        if let Some(item) = self.current_item_mut() {
1014            if let SettingControl::Json(state) = &mut item.control {
1015                state.move_up();
1016            }
1017        }
1018        self.ensure_cursor_visible();
1019    }
1020
1021    /// Handle cursor up with selection (Shift+Up)
1022    pub fn cursor_up_selecting(&mut self) {
1023        if !self.editing_text {
1024            return;
1025        }
1026        if let Some(item) = self.current_item_mut() {
1027            if let SettingControl::Json(state) = &mut item.control {
1028                state.editor.move_up_selecting();
1029            }
1030        }
1031        self.ensure_cursor_visible();
1032    }
1033
1034    /// Handle cursor down (for multiline controls)
1035    pub fn cursor_down(&mut self) {
1036        if !self.editing_text {
1037            return;
1038        }
1039        if let Some(item) = self.current_item_mut() {
1040            if let SettingControl::Json(state) = &mut item.control {
1041                state.move_down();
1042            }
1043        }
1044        self.ensure_cursor_visible();
1045    }
1046
1047    /// Handle cursor down with selection (Shift+Down)
1048    pub fn cursor_down_selecting(&mut self) {
1049        if !self.editing_text {
1050            return;
1051        }
1052        if let Some(item) = self.current_item_mut() {
1053            if let SettingControl::Json(state) = &mut item.control {
1054                state.editor.move_down_selecting();
1055            }
1056        }
1057        self.ensure_cursor_visible();
1058    }
1059
1060    /// Insert newline in JSON editor
1061    pub fn insert_newline(&mut self) {
1062        if !self.editing_text {
1063            return;
1064        }
1065        if let Some(item) = self.current_item_mut() {
1066            if let SettingControl::Json(state) = &mut item.control {
1067                state.insert('\n');
1068            }
1069        }
1070    }
1071
1072    /// Revert JSON changes to original and stop editing
1073    pub fn revert_json_and_stop(&mut self) {
1074        if let Some(item) = self.current_item_mut() {
1075            if let SettingControl::Json(state) = &mut item.control {
1076                state.revert();
1077            }
1078        }
1079        self.editing_text = false;
1080    }
1081
1082    /// Check if current control is a JSON editor
1083    pub fn is_editing_json(&self) -> bool {
1084        if !self.editing_text {
1085            return false;
1086        }
1087        self.current_item()
1088            .map(|item| matches!(&item.control, SettingControl::Json(_)))
1089            .unwrap_or(false)
1090    }
1091
1092    /// Toggle boolean value
1093    pub fn toggle_bool(&mut self) {
1094        if let Some(item) = self.current_item_mut() {
1095            // Don't allow toggling read-only fields
1096            if item.read_only {
1097                return;
1098            }
1099            if let SettingControl::Toggle(state) = &mut item.control {
1100                state.checked = !state.checked;
1101            }
1102        }
1103    }
1104
1105    /// Toggle dropdown open state
1106    pub fn toggle_dropdown(&mut self) {
1107        if let Some(item) = self.current_item_mut() {
1108            // Don't allow editing read-only fields
1109            if item.read_only {
1110                return;
1111            }
1112            if let SettingControl::Dropdown(state) = &mut item.control {
1113                state.open = !state.open;
1114            }
1115        }
1116    }
1117
1118    /// Move dropdown selection up
1119    pub fn dropdown_prev(&mut self) {
1120        if let Some(item) = self.current_item_mut() {
1121            if let SettingControl::Dropdown(state) = &mut item.control {
1122                if state.open {
1123                    state.select_prev();
1124                }
1125            }
1126        }
1127    }
1128
1129    /// Move dropdown selection down
1130    pub fn dropdown_next(&mut self) {
1131        if let Some(item) = self.current_item_mut() {
1132            if let SettingControl::Dropdown(state) = &mut item.control {
1133                if state.open {
1134                    state.select_next();
1135                }
1136            }
1137        }
1138    }
1139
1140    /// Confirm dropdown selection
1141    pub fn dropdown_confirm(&mut self) {
1142        if let Some(item) = self.current_item_mut() {
1143            if let SettingControl::Dropdown(state) = &mut item.control {
1144                state.open = false;
1145            }
1146        }
1147    }
1148
1149    /// Increment number value
1150    pub fn increment_number(&mut self) {
1151        if let Some(item) = self.current_item_mut() {
1152            // Don't allow editing read-only fields
1153            if item.read_only {
1154                return;
1155            }
1156            if let SettingControl::Number(state) = &mut item.control {
1157                state.increment();
1158            }
1159        }
1160    }
1161
1162    /// Decrement number value
1163    pub fn decrement_number(&mut self) {
1164        if let Some(item) = self.current_item_mut() {
1165            // Don't allow editing read-only fields
1166            if item.read_only {
1167                return;
1168            }
1169            if let SettingControl::Number(state) = &mut item.control {
1170                state.decrement();
1171            }
1172        }
1173    }
1174
1175    /// Delete the currently focused item from a TextList control
1176    pub fn delete_list_item(&mut self) {
1177        if let Some(item) = self.current_item_mut() {
1178            if let SettingControl::TextList(state) = &mut item.control {
1179                // Remove the currently focused item if any
1180                if let Some(idx) = state.focused_item {
1181                    state.remove_item(idx);
1182                }
1183            }
1184        }
1185    }
1186
1187    /// Delete character at cursor (forward delete)
1188    pub fn delete(&mut self) {
1189        if !self.editing_text {
1190            return;
1191        }
1192        if let Some(item) = self.current_item_mut() {
1193            match &mut item.control {
1194                SettingControl::Text(state) => {
1195                    state.delete();
1196                }
1197                SettingControl::TextList(state) => {
1198                    state.delete();
1199                }
1200                SettingControl::Json(state) => {
1201                    state.delete();
1202                }
1203                _ => {}
1204            }
1205        }
1206    }
1207
1208    /// Move cursor to beginning of line
1209    pub fn cursor_home(&mut self) {
1210        if !self.editing_text {
1211            return;
1212        }
1213        if let Some(item) = self.current_item_mut() {
1214            match &mut item.control {
1215                SettingControl::Text(state) => {
1216                    state.move_home();
1217                }
1218                SettingControl::TextList(state) => {
1219                    state.move_home();
1220                }
1221                SettingControl::Json(state) => {
1222                    state.move_home();
1223                }
1224                _ => {}
1225            }
1226        }
1227    }
1228
1229    /// Move cursor to end of line
1230    pub fn cursor_end(&mut self) {
1231        if !self.editing_text {
1232            return;
1233        }
1234        if let Some(item) = self.current_item_mut() {
1235            match &mut item.control {
1236                SettingControl::Text(state) => {
1237                    state.move_end();
1238                }
1239                SettingControl::TextList(state) => {
1240                    state.move_end();
1241                }
1242                SettingControl::Json(state) => {
1243                    state.move_end();
1244                }
1245                _ => {}
1246            }
1247        }
1248    }
1249
1250    /// Select all text in current control
1251    pub fn select_all(&mut self) {
1252        if !self.editing_text {
1253            return;
1254        }
1255        if let Some(item) = self.current_item_mut() {
1256            if let SettingControl::Json(state) = &mut item.control {
1257                state.select_all();
1258            }
1259            // Note: Text and TextList don't have select_all implemented
1260        }
1261    }
1262
1263    /// Get selected text from current JSON control
1264    pub fn selected_text(&self) -> Option<String> {
1265        if !self.editing_text {
1266            return None;
1267        }
1268        if let Some(item) = self.current_item() {
1269            if let SettingControl::Json(state) = &item.control {
1270                return state.selected_text();
1271            }
1272        }
1273        None
1274    }
1275
1276    /// Check if any field is currently in edit mode
1277    pub fn is_editing(&self) -> bool {
1278        self.editing_text
1279            || self
1280                .current_item()
1281                .map(|item| {
1282                    matches!(
1283                        &item.control,
1284                        SettingControl::Dropdown(s) if s.open
1285                    )
1286                })
1287                .unwrap_or(false)
1288    }
1289}
1290
1291#[cfg(test)]
1292mod tests {
1293    use super::*;
1294
1295    fn create_test_schema() -> SettingSchema {
1296        SettingSchema {
1297            path: "/test".to_string(),
1298            name: "Test".to_string(),
1299            description: Some("Test schema".to_string()),
1300            setting_type: SettingType::Object {
1301                properties: vec![
1302                    SettingSchema {
1303                        path: "/enabled".to_string(),
1304                        name: "Enabled".to_string(),
1305                        description: Some("Enable this".to_string()),
1306                        setting_type: SettingType::Boolean,
1307                        default: Some(serde_json::json!(true)),
1308                        read_only: false,
1309                        section: None,
1310                        order: None,
1311                        nullable: false,
1312                        enum_from: None,
1313                        dual_list_sibling: None,
1314                        dynamically_extendable_status_bar_elements: false,
1315                    },
1316                    SettingSchema {
1317                        path: "/command".to_string(),
1318                        name: "Command".to_string(),
1319                        description: Some("Command to run".to_string()),
1320                        setting_type: SettingType::String,
1321                        default: Some(serde_json::json!("")),
1322                        read_only: false,
1323                        section: None,
1324                        order: None,
1325                        nullable: false,
1326                        enum_from: None,
1327                        dual_list_sibling: None,
1328                        dynamically_extendable_status_bar_elements: false,
1329                    },
1330                ],
1331            },
1332            default: None,
1333            read_only: false,
1334            section: None,
1335            order: None,
1336            nullable: false,
1337            enum_from: None,
1338            dual_list_sibling: None,
1339            dynamically_extendable_status_bar_elements: false,
1340        }
1341    }
1342
1343    #[test]
1344    fn from_schema_creates_key_item_first() {
1345        let schema = create_test_schema();
1346        let dialog = EntryDialogState::from_schema(
1347            "test".to_string(),
1348            &serde_json::json!({}),
1349            &schema,
1350            "/test",
1351            false,
1352            false,
1353            &HashMap::new(),
1354        );
1355
1356        assert!(!dialog.items.is_empty());
1357        assert_eq!(dialog.items[0].path, "__key__");
1358        assert_eq!(dialog.items[0].name, "Key");
1359    }
1360
1361    #[test]
1362    fn from_schema_creates_items_from_properties() {
1363        let schema = create_test_schema();
1364        let dialog = EntryDialogState::from_schema(
1365            "test".to_string(),
1366            &serde_json::json!({"enabled": true, "command": "test-cmd"}),
1367            &schema,
1368            "/test",
1369            false,
1370            false,
1371            &HashMap::new(),
1372        );
1373
1374        // Key + 2 properties = 3 items
1375        assert_eq!(dialog.items.len(), 3);
1376        assert_eq!(dialog.items[1].name, "Enabled");
1377        assert_eq!(dialog.items[2].name, "Command");
1378    }
1379
1380    #[test]
1381    fn get_key_returns_key_value() {
1382        let schema = create_test_schema();
1383        let dialog = EntryDialogState::from_schema(
1384            "mykey".to_string(),
1385            &serde_json::json!({}),
1386            &schema,
1387            "/test",
1388            false,
1389            false,
1390            &HashMap::new(),
1391        );
1392
1393        assert_eq!(dialog.get_key(), "mykey");
1394    }
1395
1396    #[test]
1397    fn to_value_excludes_key() {
1398        let schema = create_test_schema();
1399        let dialog = EntryDialogState::from_schema(
1400            "test".to_string(),
1401            &serde_json::json!({"enabled": true, "command": "cmd"}),
1402            &schema,
1403            "/test",
1404            false,
1405            false,
1406            &HashMap::new(),
1407        );
1408
1409        let value = dialog.to_value();
1410        assert!(value.get("__key__").is_none());
1411        assert!(value.get("enabled").is_some());
1412    }
1413
1414    #[test]
1415    fn focus_navigation_works() {
1416        let schema = create_test_schema();
1417        let mut dialog = EntryDialogState::from_schema(
1418            "test".to_string(),
1419            &serde_json::json!({}),
1420            &schema,
1421            "/test",
1422            false, // existing entry - Key is read-only
1423            false, // allow delete
1424            &HashMap::new(),
1425        );
1426
1427        // With is_new=false, Key is read-only and sorted first
1428        // Items: [Key (read-only), Enabled, Command]
1429        // Focus starts at first editable item (index 1)
1430        assert_eq!(dialog.first_editable_index, 1);
1431        assert_eq!(dialog.selected_item, 1); // First editable (Enabled)
1432        assert!(!dialog.focus_on_buttons);
1433
1434        dialog.focus_next();
1435        assert_eq!(dialog.selected_item, 2); // Command
1436
1437        dialog.focus_next();
1438        assert!(dialog.focus_on_buttons); // No more editable items
1439        assert_eq!(dialog.focused_button, 0);
1440
1441        // Going back should skip read-only Key
1442        dialog.focus_prev();
1443        assert!(!dialog.focus_on_buttons);
1444        assert_eq!(dialog.selected_item, 2); // Last editable (Command)
1445
1446        dialog.focus_prev();
1447        assert_eq!(dialog.selected_item, 1); // First editable (Enabled)
1448
1449        dialog.focus_prev();
1450        assert!(dialog.focus_on_buttons); // Wraps to buttons, not to read-only Key
1451    }
1452
1453    #[test]
1454    fn entry_path_joins_map_path_and_entry_key() {
1455        let schema = create_test_schema();
1456
1457        // Existing entry: full path is map_path + "/" + entry_key
1458        let existing = EntryDialogState::from_schema(
1459            "rust".to_string(),
1460            &serde_json::json!({}),
1461            &schema,
1462            "/lsp",
1463            false,
1464            false,
1465            &HashMap::new(),
1466        );
1467        assert_eq!(existing.entry_path(), "/lsp/rust");
1468
1469        // New entry with no key typed yet falls back to the parent map path.
1470        // Nested dialogs keyed off this are outside the scope of this test.
1471        let new_entry = EntryDialogState::from_schema(
1472            String::new(),
1473            &serde_json::json!({}),
1474            &schema,
1475            "/lsp",
1476            true,
1477            false,
1478            &HashMap::new(),
1479        );
1480        assert_eq!(new_entry.entry_path(), "/lsp");
1481    }
1482
1483    #[test]
1484    fn entry_path_tracks_live_key_edits_for_new_entries() {
1485        let schema = create_test_schema();
1486        let mut dialog = EntryDialogState::from_schema(
1487            String::new(),
1488            &serde_json::json!({}),
1489            &schema,
1490            "/universal_lsp",
1491            true,
1492            false,
1493            &HashMap::new(),
1494        );
1495
1496        // User types a key into the editable key field.
1497        for item in dialog.items.iter_mut() {
1498            if item.path == "__key__" {
1499                if let SettingControl::Text(state) = &mut item.control {
1500                    state.value = "myserver".to_string();
1501                }
1502            }
1503        }
1504
1505        assert_eq!(dialog.entry_path(), "/universal_lsp/myserver");
1506    }
1507
1508    #[test]
1509    fn button_count_differs_for_new_vs_existing() {
1510        let schema = create_test_schema();
1511
1512        let new_dialog = EntryDialogState::from_schema(
1513            "test".to_string(),
1514            &serde_json::json!({}),
1515            &schema,
1516            "/test",
1517            true,
1518            false,
1519            &HashMap::new(),
1520        );
1521        assert_eq!(new_dialog.button_count(), 2); // Save, Cancel
1522
1523        let existing_dialog = EntryDialogState::from_schema(
1524            "test".to_string(),
1525            &serde_json::json!({}),
1526            &schema,
1527            "/test",
1528            false,
1529            false, // allow delete
1530            &HashMap::new(),
1531        );
1532        assert_eq!(existing_dialog.button_count(), 3); // Save, Delete, Cancel
1533
1534        // no_delete hides the Delete button even for existing entries
1535        let no_delete_dialog = EntryDialogState::from_schema(
1536            "test".to_string(),
1537            &serde_json::json!({}),
1538            &schema,
1539            "/test",
1540            false,
1541            true, // no delete (auto-managed entries like plugins)
1542            &HashMap::new(),
1543        );
1544        assert_eq!(no_delete_dialog.button_count(), 2); // Save, Cancel (no Delete)
1545    }
1546}