1use super::entry_dialog::EntryDialogState;
7use super::items::{control_to_value, SettingControl, SettingItem, SettingsPage};
8use super::layout::SettingsHit;
9use super::schema::{parse_schema, SettingCategory, SettingSchema};
10use super::search::{search_settings, DeepMatch, SearchResult};
11use crate::config::Config;
12use crate::config_io::ConfigLayer;
13use crate::view::controls::FocusState;
14use crate::view::ui::{FocusManager, ScrollablePanel};
15use std::collections::HashMap;
16
17enum NestedDialogInfo {
19 MapEntry {
20 key: String,
21 value: serde_json::Value,
22 schema: SettingSchema,
23 path: String,
24 is_new: bool,
25 no_delete: bool,
26 },
27 ArrayItem {
28 index: Option<usize>,
29 value: serde_json::Value,
30 schema: SettingSchema,
31 path: String,
32 is_new: bool,
33 },
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
38pub enum FocusPanel {
39 #[default]
41 Categories,
42 Settings,
44 Footer,
46}
47
48#[derive(Debug)]
50pub struct SettingsState {
51 categories: Vec<SettingCategory>,
53 pub pages: Vec<SettingsPage>,
55 pub selected_category: usize,
57 pub selected_item: usize,
59 pub focus: FocusManager<FocusPanel>,
61 pub footer_button_index: usize,
63 pub pending_changes: HashMap<String, serde_json::Value>,
65 original_config: serde_json::Value,
67 pub visible: bool,
69 pub search_query: String,
71 pub search_active: bool,
73 pub search_results: Vec<SearchResult>,
75 pub selected_search_result: usize,
77 pub search_scroll_offset: usize,
79 pub search_max_visible: usize,
81 pub showing_confirm_dialog: bool,
83 pub confirm_dialog_selection: usize,
85 pub confirm_dialog_hover: Option<usize>,
87 pub showing_reset_dialog: bool,
89 pub reset_dialog_selection: usize,
91 pub reset_dialog_hover: Option<usize>,
93 pub showing_help: bool,
95 pub scroll_panel: ScrollablePanel,
97 pub sub_focus: Option<usize>,
99 pub editing_text: bool,
101 pub hover_position: Option<(u16, u16)>,
103 pub hover_hit: Option<SettingsHit>,
105 pub entry_dialog_stack: Vec<EntryDialogState>,
108 pub target_layer: ConfigLayer,
112 pub layer_sources: HashMap<String, ConfigLayer>,
116 pub pending_deletions: std::collections::HashSet<String>,
120 pub layout_width: u16,
123}
124
125impl SettingsState {
126 pub fn new(schema_json: &str, config: &Config) -> Result<Self, serde_json::Error> {
128 let categories = parse_schema(schema_json)?;
129 let config_value = serde_json::to_value(config)?;
130 let layer_sources = HashMap::new(); let target_layer = ConfigLayer::User; let pages =
133 super::items::build_pages(&categories, &config_value, &layer_sources, target_layer);
134
135 Ok(Self {
136 categories,
137 pages,
138 selected_category: 0,
139 selected_item: 0,
140 focus: FocusManager::new(vec![
141 FocusPanel::Categories,
142 FocusPanel::Settings,
143 FocusPanel::Footer,
144 ]),
145 footer_button_index: 2, pending_changes: HashMap::new(),
147 original_config: config_value,
148 visible: false,
149 search_query: String::new(),
150 search_active: false,
151 search_results: Vec::new(),
152 selected_search_result: 0,
153 search_scroll_offset: 0,
154 search_max_visible: 5, showing_confirm_dialog: false,
156 confirm_dialog_selection: 0,
157 confirm_dialog_hover: None,
158 showing_reset_dialog: false,
159 reset_dialog_selection: 0,
160 reset_dialog_hover: None,
161 showing_help: false,
162 scroll_panel: ScrollablePanel::new(),
163 sub_focus: None,
164 editing_text: false,
165 hover_position: None,
166 hover_hit: None,
167 entry_dialog_stack: Vec::new(),
168 target_layer,
169 layer_sources,
170 pending_deletions: std::collections::HashSet::new(),
171 layout_width: 0,
172 })
173 }
174
175 #[inline]
177 pub fn focus_panel(&self) -> FocusPanel {
178 self.focus.current().unwrap_or_default()
179 }
180
181 pub fn show(&mut self) {
183 self.visible = true;
184 self.focus.set(FocusPanel::Categories);
185 self.footer_button_index = 2; self.selected_category = 0;
187 self.selected_item = 0;
188 self.scroll_panel = ScrollablePanel::new();
189 self.sub_focus = None;
190 self.showing_confirm_dialog = false;
192 self.confirm_dialog_selection = 0;
193 self.confirm_dialog_hover = None;
194 self.showing_reset_dialog = false;
195 self.reset_dialog_selection = 0;
196 self.reset_dialog_hover = None;
197 self.showing_help = false;
198 }
199
200 pub fn hide(&mut self) {
202 self.visible = false;
203 self.search_active = false;
204 self.search_query.clear();
205 }
206
207 pub fn entry_dialog(&self) -> Option<&EntryDialogState> {
209 self.entry_dialog_stack.last()
210 }
211
212 pub fn entry_dialog_mut(&mut self) -> Option<&mut EntryDialogState> {
214 self.entry_dialog_stack.last_mut()
215 }
216
217 pub fn has_entry_dialog(&self) -> bool {
219 !self.entry_dialog_stack.is_empty()
220 }
221
222 pub fn current_page(&self) -> Option<&SettingsPage> {
224 self.pages.get(self.selected_category)
225 }
226
227 pub fn current_page_mut(&mut self) -> Option<&mut SettingsPage> {
229 self.pages.get_mut(self.selected_category)
230 }
231
232 pub fn current_item(&self) -> Option<&SettingItem> {
234 self.current_page()
235 .and_then(|page| page.items.get(self.selected_item))
236 }
237
238 pub fn current_item_mut(&mut self) -> Option<&mut SettingItem> {
240 self.pages
241 .get_mut(self.selected_category)
242 .and_then(|page| page.items.get_mut(self.selected_item))
243 }
244
245 pub fn can_exit_text_editing(&self) -> bool {
247 self.current_item()
248 .map(|item| {
249 if let SettingControl::Text(state) = &item.control {
250 state.is_valid()
251 } else {
252 true
253 }
254 })
255 .unwrap_or(true)
256 }
257
258 pub fn entry_dialog_can_exit_text_editing(&self) -> bool {
260 self.entry_dialog()
261 .and_then(|dialog| dialog.current_item())
262 .map(|item| {
263 if let SettingControl::Text(state) = &item.control {
264 state.is_valid()
265 } else {
266 true
267 }
268 })
269 .unwrap_or(true)
270 }
271
272 fn init_map_focus(&mut self, from_above: bool) {
275 if let Some(item) = self.current_item_mut() {
276 if let SettingControl::Map(ref mut map_state) = item.control {
277 map_state.init_focus(from_above);
278 }
279 }
280 self.update_map_sub_focus();
282 }
283
284 pub(super) fn update_control_focus(&mut self, focused: bool) {
288 let focus_state = if focused {
289 FocusState::Focused
290 } else {
291 FocusState::Normal
292 };
293 if let Some(item) = self.current_item_mut() {
294 match &mut item.control {
295 SettingControl::Map(ref mut state) => state.focus = focus_state,
296 SettingControl::TextList(ref mut state) => state.focus = focus_state,
297 SettingControl::DualList(ref mut state) => state.focus = focus_state,
298 SettingControl::ObjectArray(ref mut state) => state.focus = focus_state,
299 SettingControl::Toggle(ref mut state) => state.focus = focus_state,
300 SettingControl::Number(ref mut state) => state.focus = focus_state,
301 SettingControl::Dropdown(ref mut state) => state.focus = focus_state,
302 SettingControl::Text(ref mut state) => state.focus = focus_state,
303 SettingControl::Json(_) | SettingControl::Complex { .. } => {} }
305 }
306 }
307
308 fn update_map_sub_focus(&mut self) {
311 self.sub_focus = self.current_item().and_then(|item| {
312 if let SettingControl::Map(ref map_state) = item.control {
313 Some(match map_state.focused_entry {
315 Some(i) => 1 + i,
316 None => 1 + map_state.entries.len(), })
318 } else {
319 None
320 }
321 });
322 }
323
324 pub fn select_prev(&mut self) {
326 match self.focus_panel() {
327 FocusPanel::Categories => {
328 if self.selected_category > 0 {
329 self.update_control_focus(false); self.selected_category -= 1;
331 self.selected_item = 0;
332 self.scroll_panel = ScrollablePanel::new();
333 self.sub_focus = None;
334 self.update_control_focus(true); }
336 }
337 FocusPanel::Settings => {
338 let handled = self
340 .current_item_mut()
341 .and_then(|item| match &mut item.control {
342 SettingControl::Map(map_state) => Some(map_state.focus_prev()),
343 _ => None,
344 })
345 .unwrap_or(false);
346
347 if handled {
348 self.update_map_sub_focus();
350 } else if self.selected_item > 0 {
351 self.update_control_focus(false); self.selected_item -= 1;
353 self.sub_focus = None;
354 self.init_map_focus(false); self.update_control_focus(true); }
357 self.ensure_visible();
358 }
359 FocusPanel::Footer => {
360 if self.footer_button_index > 0 {
362 self.footer_button_index -= 1;
363 }
364 }
365 }
366 }
367
368 pub fn select_next(&mut self) {
370 match self.focus_panel() {
371 FocusPanel::Categories => {
372 if self.selected_category + 1 < self.pages.len() {
373 self.update_control_focus(false); self.selected_category += 1;
375 self.selected_item = 0;
376 self.scroll_panel = ScrollablePanel::new();
377 self.sub_focus = None;
378 self.update_control_focus(true); }
380 }
381 FocusPanel::Settings => {
382 let handled = self
384 .current_item_mut()
385 .and_then(|item| match &mut item.control {
386 SettingControl::Map(map_state) => Some(map_state.focus_next()),
387 _ => None,
388 })
389 .unwrap_or(false);
390
391 if handled {
392 self.update_map_sub_focus();
394 } else {
395 let can_move = self
396 .current_page()
397 .is_some_and(|page| self.selected_item + 1 < page.items.len());
398 if can_move {
399 self.update_control_focus(false); self.selected_item += 1;
401 self.sub_focus = None;
402 self.init_map_focus(true); self.update_control_focus(true); }
405 }
406 self.ensure_visible();
407 }
408 FocusPanel::Footer => {
409 if self.footer_button_index < 2 {
411 self.footer_button_index += 1;
412 }
413 }
414 }
415 }
416
417 pub fn select_next_page(&mut self) {
419 let page_size = self.scroll_panel.viewport_height().max(1);
420 for _ in 0..page_size {
421 self.select_next();
422 }
423 }
424
425 pub fn select_prev_page(&mut self) {
427 let page_size = self.scroll_panel.viewport_height().max(1);
428 for _ in 0..page_size {
429 self.select_prev();
430 }
431 }
432
433 pub fn toggle_focus(&mut self) {
435 let old_panel = self.focus_panel();
436 self.focus.focus_next();
437 self.on_panel_changed(old_panel, true);
438 }
439
440 pub fn toggle_focus_backward(&mut self) {
442 let old_panel = self.focus_panel();
443 self.focus.focus_prev();
444 self.on_panel_changed(old_panel, false);
445 }
446
447 fn on_panel_changed(&mut self, old_panel: FocusPanel, forward: bool) {
449 if old_panel == FocusPanel::Settings {
451 self.update_control_focus(false);
452 }
453
454 if self.focus_panel() == FocusPanel::Settings
456 && self.selected_item >= self.current_page().map_or(0, |p| p.items.len())
457 {
458 self.selected_item = 0;
459 }
460 self.sub_focus = None;
461
462 if self.focus_panel() == FocusPanel::Settings {
463 self.init_map_focus(forward); self.update_control_focus(true); }
466
467 if self.focus_panel() == FocusPanel::Footer {
469 self.footer_button_index = if forward {
470 0 } else {
472 4 };
474 }
475
476 self.ensure_visible();
477 }
478
479 pub fn update_layout_widths(&mut self) {
483 let width = self.layout_width;
484 if width > 0 {
485 if let Some(page) = self.pages.get_mut(self.selected_category) {
486 for item in &mut page.items {
487 item.layout_width = width;
488 }
489 }
490 }
491 }
492
493 pub fn ensure_visible(&mut self) {
494 if self.focus_panel() != FocusPanel::Settings {
495 return;
496 }
497
498 self.update_layout_widths();
499
500 let selected_item = self.selected_item;
502 let sub_focus = self.sub_focus;
503 if let Some(page) = self.pages.get(self.selected_category) {
504 self.scroll_panel
505 .ensure_focused_visible(&page.items, selected_item, sub_focus);
506 }
507 }
508
509 pub fn set_pending_change(&mut self, path: &str, value: serde_json::Value) {
511 let original = self.original_config.pointer(path);
513 if original == Some(&value) {
514 self.pending_changes.remove(path);
515 } else {
516 self.pending_changes.insert(path.to_string(), value);
517 }
518 }
519
520 pub fn has_changes(&self) -> bool {
522 !self.pending_changes.is_empty() || !self.pending_deletions.is_empty()
523 }
524
525 pub fn apply_changes(&self, config: &Config) -> Result<Config, serde_json::Error> {
527 let mut config_value = serde_json::to_value(config)?;
528
529 for (path, value) in &self.pending_changes {
530 if let Some(target) = config_value.pointer_mut(path) {
531 *target = value.clone();
532 }
533 }
534
535 serde_json::from_value(config_value)
536 }
537
538 pub fn discard_changes(&mut self) {
540 self.pending_changes.clear();
541 self.pending_deletions.clear();
542 self.pages = super::items::build_pages(
544 &self.categories,
545 &self.original_config,
546 &self.layer_sources,
547 self.target_layer,
548 );
549 }
550
551 pub fn set_target_layer(&mut self, layer: ConfigLayer) {
553 if layer != ConfigLayer::System {
554 self.target_layer = layer;
556 self.pending_changes.clear();
558 self.pending_deletions.clear();
559 self.pages = super::items::build_pages(
561 &self.categories,
562 &self.original_config,
563 &self.layer_sources,
564 self.target_layer,
565 );
566 }
567 }
568
569 pub fn cycle_target_layer(&mut self) {
571 self.target_layer = match self.target_layer {
572 ConfigLayer::System => ConfigLayer::User, ConfigLayer::User => ConfigLayer::Project,
574 ConfigLayer::Project => ConfigLayer::Session,
575 ConfigLayer::Session => ConfigLayer::User,
576 };
577 self.pending_changes.clear();
579 self.pending_deletions.clear();
580 self.pages = super::items::build_pages(
582 &self.categories,
583 &self.original_config,
584 &self.layer_sources,
585 self.target_layer,
586 );
587 }
588
589 pub fn target_layer_name(&self) -> &'static str {
591 match self.target_layer {
592 ConfigLayer::System => "System (read-only)",
593 ConfigLayer::User => "User",
594 ConfigLayer::Project => "Project",
595 ConfigLayer::Session => "Session",
596 }
597 }
598
599 pub fn set_layer_sources(&mut self, sources: HashMap<String, ConfigLayer>) {
602 self.layer_sources = sources;
603 self.pages = super::items::build_pages(
605 &self.categories,
606 &self.original_config,
607 &self.layer_sources,
608 self.target_layer,
609 );
610 }
611
612 pub fn get_layer_source(&self, path: &str) -> ConfigLayer {
615 self.layer_sources
616 .get(path)
617 .copied()
618 .unwrap_or(ConfigLayer::System)
619 }
620
621 pub fn layer_source_label(layer: ConfigLayer) -> &'static str {
623 match layer {
624 ConfigLayer::System => "default",
625 ConfigLayer::User => "user",
626 ConfigLayer::Project => "project",
627 ConfigLayer::Session => "session",
628 }
629 }
630
631 pub fn reset_current_to_default(&mut self) {
639 let reset_info = self.current_item().and_then(|item| {
641 if !item.modified || item.is_auto_managed {
644 return None;
645 }
646 item.default
647 .as_ref()
648 .map(|default| (item.path.clone(), default.clone()))
649 });
650
651 if let Some((path, default)) = reset_info {
652 self.pending_deletions.insert(path.clone());
654 self.pending_changes.remove(&path);
656
657 if let Some(item) = self.current_item_mut() {
661 update_control_from_value(&mut item.control, &default);
662 item.modified = false;
663 item.layer_source = ConfigLayer::System; }
666 }
667 }
668
669 pub fn set_current_to_null(&mut self) {
675 let target_layer = self.target_layer;
676 let change_info = self.current_item().and_then(|item| {
677 if !item.nullable || item.is_null || item.read_only {
678 return None;
679 }
680 Some(item.path.clone())
681 });
682
683 if let Some(path) = change_info {
684 self.pending_changes
686 .insert(path.clone(), serde_json::Value::Null);
687 self.pending_deletions.remove(&path);
688
689 if let Some(item) = self.current_item_mut() {
691 item.is_null = true;
692 item.modified = true;
693 item.layer_source = target_layer;
694 }
695 }
696 }
697
698 pub fn clear_current_category(&mut self) {
704 let target_layer = self.target_layer;
705 let page = match self.current_page() {
706 Some(p) if p.nullable => p,
707 _ => return,
708 };
709 let page_path = page.path.clone();
710
711 self.pending_changes
713 .insert(page_path.clone(), serde_json::Value::Null);
714
715 let prefix = format!("{}/", page_path);
717 self.pending_changes
718 .retain(|path, _| !path.starts_with(&prefix));
719 self.pending_deletions
720 .retain(|path| !path.starts_with(&prefix));
721
722 if let Some(page) = self.current_page_mut() {
724 for item in &mut page.items {
725 if item.nullable {
726 item.is_null = true;
727 item.modified = false;
728 item.layer_source = target_layer;
729 }
730 }
731 }
732 }
733
734 pub fn current_category_has_values(&self) -> bool {
736 match self.current_page() {
737 Some(page) if page.nullable => {
738 page.items.iter().any(|item| !item.is_null && item.nullable)
739 || page.items.iter().any(|item| item.modified)
740 }
741 _ => false,
742 }
743 }
744
745 pub fn on_value_changed(&mut self) {
747 let target_layer = self.target_layer;
749
750 let change_info = self.current_item().map(|item| {
752 let value = control_to_value(&item.control);
753 (item.path.clone(), value)
754 });
755
756 if let Some((path, value)) = change_info {
757 self.pending_deletions.remove(&path);
760
761 if let Some(item) = self.current_item_mut() {
763 item.modified = true; item.layer_source = target_layer; item.is_null = false; }
767 self.set_pending_change(&path, value);
768 }
769 }
770
771 pub fn update_focus_states(&mut self) {
773 let current_focus = self.focus_panel();
774 for (page_idx, page) in self.pages.iter_mut().enumerate() {
775 for (item_idx, item) in page.items.iter_mut().enumerate() {
776 let is_focused = current_focus == FocusPanel::Settings
777 && page_idx == self.selected_category
778 && item_idx == self.selected_item;
779
780 let focus = if is_focused {
781 FocusState::Focused
782 } else {
783 FocusState::Normal
784 };
785
786 match &mut item.control {
787 SettingControl::Toggle(state) => state.focus = focus,
788 SettingControl::Number(state) => state.focus = focus,
789 SettingControl::Dropdown(state) => state.focus = focus,
790 SettingControl::Text(state) => state.focus = focus,
791 SettingControl::TextList(state) => state.focus = focus,
792 SettingControl::DualList(state) => state.focus = focus,
793 SettingControl::Map(state) => state.focus = focus,
794 SettingControl::ObjectArray(state) => state.focus = focus,
795 SettingControl::Json(state) => state.focus = focus,
796 SettingControl::Complex { .. } => {}
797 }
798 }
799 }
800 }
801
802 pub fn start_search(&mut self) {
804 self.search_active = true;
805 self.search_query.clear();
806 self.search_results.clear();
807 self.selected_search_result = 0;
808 self.search_scroll_offset = 0;
809 }
810
811 pub fn cancel_search(&mut self) {
813 self.search_active = false;
814 self.search_query.clear();
815 self.search_results.clear();
816 self.selected_search_result = 0;
817 self.search_scroll_offset = 0;
818 }
819
820 pub fn set_search_query(&mut self, query: String) {
822 self.search_query = query;
823 self.search_results = search_settings(&self.pages, &self.search_query);
824 self.selected_search_result = 0;
825 self.search_scroll_offset = 0;
826 }
827
828 pub fn search_push_char(&mut self, c: char) {
830 self.search_query.push(c);
831 self.search_results = search_settings(&self.pages, &self.search_query);
832 self.selected_search_result = 0;
833 self.search_scroll_offset = 0;
834 }
835
836 pub fn search_pop_char(&mut self) {
838 self.search_query.pop();
839 self.search_results = search_settings(&self.pages, &self.search_query);
840 self.selected_search_result = 0;
841 self.search_scroll_offset = 0;
842 }
843
844 pub fn search_prev(&mut self) {
846 if !self.search_results.is_empty() && self.selected_search_result > 0 {
847 self.selected_search_result -= 1;
848 if self.selected_search_result < self.search_scroll_offset {
850 self.search_scroll_offset = self.selected_search_result;
851 }
852 }
853 }
854
855 pub fn search_next(&mut self) {
857 if !self.search_results.is_empty()
858 && self.selected_search_result + 1 < self.search_results.len()
859 {
860 self.selected_search_result += 1;
861 if self.selected_search_result >= self.search_scroll_offset + self.search_max_visible {
863 self.search_scroll_offset =
864 self.selected_search_result - self.search_max_visible + 1;
865 }
866 }
867 }
868
869 pub fn search_scroll_up(&mut self, delta: usize) -> bool {
871 if self.search_results.is_empty() || self.search_scroll_offset == 0 {
872 return false;
873 }
874 self.search_scroll_offset = self.search_scroll_offset.saturating_sub(delta);
875 if self.selected_search_result >= self.search_scroll_offset + self.search_max_visible {
877 self.selected_search_result = self.search_scroll_offset + self.search_max_visible - 1;
878 }
879 true
880 }
881
882 pub fn search_scroll_down(&mut self, delta: usize) -> bool {
884 if self.search_results.is_empty() {
885 return false;
886 }
887 let max_offset = self
888 .search_results
889 .len()
890 .saturating_sub(self.search_max_visible);
891 if self.search_scroll_offset >= max_offset {
892 return false;
893 }
894 self.search_scroll_offset = (self.search_scroll_offset + delta).min(max_offset);
895 if self.selected_search_result < self.search_scroll_offset {
897 self.selected_search_result = self.search_scroll_offset;
898 }
899 true
900 }
901
902 pub fn search_scroll_to_ratio(&mut self, ratio: f32) -> bool {
904 if self.search_results.is_empty() {
905 return false;
906 }
907 let max_offset = self
908 .search_results
909 .len()
910 .saturating_sub(self.search_max_visible);
911 let new_offset = (ratio * max_offset as f32) as usize;
912 if new_offset != self.search_scroll_offset {
913 self.search_scroll_offset = new_offset.min(max_offset);
914 if self.selected_search_result < self.search_scroll_offset {
916 self.selected_search_result = self.search_scroll_offset;
917 } else if self.selected_search_result
918 >= self.search_scroll_offset + self.search_max_visible
919 {
920 self.selected_search_result =
921 self.search_scroll_offset + self.search_max_visible - 1;
922 }
923 return true;
924 }
925 false
926 }
927
928 pub fn jump_to_search_result(&mut self) {
930 let Some(result) = self
932 .search_results
933 .get(self.selected_search_result)
934 .cloned()
935 else {
936 return;
937 };
938 let page_index = result.page_index;
939 let item_index = result.item_index;
940
941 self.update_control_focus(false);
943 self.selected_category = page_index;
944 self.selected_item = item_index;
945 self.focus.set(FocusPanel::Settings);
946 self.scroll_panel.scroll.offset = 0;
948 self.update_layout_widths();
950 if let Some(page) = self.pages.get(self.selected_category) {
951 self.scroll_panel.update_content_height(&page.items);
952 }
953 self.sub_focus = None;
954 self.init_map_focus(true);
955
956 if let Some(ref deep_match) = result.deep_match {
958 self.jump_to_deep_match(deep_match);
959 }
960
961 self.update_control_focus(true); self.ensure_visible();
963 self.cancel_search();
964 }
965
966 fn jump_to_deep_match(&mut self, deep_match: &DeepMatch) {
968 match deep_match {
969 DeepMatch::MapKey { entry_index, .. } | DeepMatch::MapValue { entry_index, .. } => {
970 if let Some(item) = self.current_item_mut() {
971 if let SettingControl::Map(ref mut map_state) = item.control {
972 map_state.focused_entry = Some(*entry_index);
973 }
974 }
975 self.update_map_sub_focus();
976 }
977 DeepMatch::TextListItem { item_index, .. } => {
978 if let Some(item) = self.current_item_mut() {
979 if let SettingControl::TextList(ref mut list_state) = item.control {
980 list_state.focused_item = Some(*item_index);
981 }
982 }
983 self.sub_focus = Some(1 + *item_index);
985 }
986 }
987 }
988
989 pub fn current_search_result(&self) -> Option<&SearchResult> {
991 self.search_results.get(self.selected_search_result)
992 }
993
994 pub fn show_confirm_dialog(&mut self) {
996 self.showing_confirm_dialog = true;
997 self.confirm_dialog_selection = 0; }
999
1000 pub fn hide_confirm_dialog(&mut self) {
1002 self.showing_confirm_dialog = false;
1003 self.confirm_dialog_selection = 0;
1004 }
1005
1006 pub fn confirm_dialog_next(&mut self) {
1008 self.confirm_dialog_selection = (self.confirm_dialog_selection + 1) % 3;
1009 }
1010
1011 pub fn confirm_dialog_prev(&mut self) {
1013 self.confirm_dialog_selection = if self.confirm_dialog_selection == 0 {
1014 2
1015 } else {
1016 self.confirm_dialog_selection - 1
1017 };
1018 }
1019
1020 pub fn toggle_help(&mut self) {
1022 self.showing_help = !self.showing_help;
1023 }
1024
1025 pub fn hide_help(&mut self) {
1027 self.showing_help = false;
1028 }
1029
1030 pub fn showing_entry_dialog(&self) -> bool {
1032 self.has_entry_dialog()
1033 }
1034
1035 pub fn open_entry_dialog(&mut self) {
1037 let Some(item) = self.current_item() else {
1038 return;
1039 };
1040
1041 let path = item.path.as_str();
1043 let SettingControl::Map(map_state) = &item.control else {
1044 return;
1045 };
1046
1047 let Some(entry_idx) = map_state.focused_entry else {
1049 return;
1050 };
1051 let Some((key, value)) = map_state.entries.get(entry_idx) else {
1052 return;
1053 };
1054
1055 let Some(schema) = map_state.value_schema.as_ref() else {
1057 return; };
1059
1060 let no_delete = map_state.no_add;
1062
1063 let dialog =
1065 EntryDialogState::from_schema(key.clone(), value, schema, path, false, no_delete);
1066 self.entry_dialog_stack.push(dialog);
1067 }
1068
1069 pub fn open_add_entry_dialog(&mut self) {
1071 let Some(item) = self.current_item() else {
1072 return;
1073 };
1074 let SettingControl::Map(map_state) = &item.control else {
1075 return;
1076 };
1077 let Some(schema) = map_state.value_schema.as_ref() else {
1078 return;
1079 };
1080 let path = item.path.clone();
1081
1082 let dialog = EntryDialogState::from_schema(
1085 String::new(),
1086 &serde_json::json!({}),
1087 schema,
1088 &path,
1089 true,
1090 false,
1091 );
1092 self.entry_dialog_stack.push(dialog);
1093 }
1094
1095 pub fn open_add_array_item_dialog(&mut self) {
1097 let Some(item) = self.current_item() else {
1098 return;
1099 };
1100 let SettingControl::ObjectArray(array_state) = &item.control else {
1101 return;
1102 };
1103 let Some(schema) = array_state.item_schema.as_ref() else {
1104 return;
1105 };
1106 let path = item.path.clone();
1107
1108 let dialog =
1110 EntryDialogState::for_array_item(None, &serde_json::json!({}), schema, &path, true);
1111 self.entry_dialog_stack.push(dialog);
1112 }
1113
1114 pub fn open_edit_array_item_dialog(&mut self) {
1116 let Some(item) = self.current_item() else {
1117 return;
1118 };
1119 let SettingControl::ObjectArray(array_state) = &item.control else {
1120 return;
1121 };
1122 let Some(schema) = array_state.item_schema.as_ref() else {
1123 return;
1124 };
1125 let Some(index) = array_state.focused_index else {
1126 return;
1127 };
1128 let Some(value) = array_state.bindings.get(index) else {
1129 return;
1130 };
1131 let path = item.path.clone();
1132
1133 let dialog = EntryDialogState::for_array_item(Some(index), value, schema, &path, false);
1134 self.entry_dialog_stack.push(dialog);
1135 }
1136
1137 pub fn close_entry_dialog(&mut self) {
1139 self.entry_dialog_stack.pop();
1140 }
1141
1142 pub fn open_nested_entry_dialog(&mut self) {
1147 let nested_info = self.entry_dialog().and_then(|dialog| {
1149 let item = dialog.current_item()?;
1150 let base = dialog.entry_path();
1156 let relative = item.path.trim_start_matches('/');
1157 let path = if relative.is_empty() {
1158 base
1162 } else {
1163 format!("{}/{}", base, relative)
1164 };
1165
1166 match &item.control {
1167 SettingControl::Map(map_state) => {
1168 let schema = map_state.value_schema.as_ref()?;
1169 let no_delete = map_state.no_add; if let Some(entry_idx) = map_state.focused_entry {
1171 let (key, value) = map_state.entries.get(entry_idx)?;
1173 Some(NestedDialogInfo::MapEntry {
1174 key: key.clone(),
1175 value: value.clone(),
1176 schema: schema.as_ref().clone(),
1177 path,
1178 is_new: false,
1179 no_delete,
1180 })
1181 } else {
1182 Some(NestedDialogInfo::MapEntry {
1184 key: String::new(),
1185 value: serde_json::json!({}),
1186 schema: schema.as_ref().clone(),
1187 path,
1188 is_new: true,
1189 no_delete: false, })
1191 }
1192 }
1193 SettingControl::ObjectArray(array_state) => {
1194 let schema = array_state.item_schema.as_ref()?;
1195 if let Some(index) = array_state.focused_index {
1196 let value = array_state.bindings.get(index)?;
1198 Some(NestedDialogInfo::ArrayItem {
1199 index: Some(index),
1200 value: value.clone(),
1201 schema: schema.as_ref().clone(),
1202 path,
1203 is_new: false,
1204 })
1205 } else {
1206 Some(NestedDialogInfo::ArrayItem {
1208 index: None,
1209 value: serde_json::json!({}),
1210 schema: schema.as_ref().clone(),
1211 path,
1212 is_new: true,
1213 })
1214 }
1215 }
1216 _ => None,
1217 }
1218 });
1219
1220 if let Some(info) = nested_info {
1222 let dialog = match info {
1223 NestedDialogInfo::MapEntry {
1224 key,
1225 value,
1226 schema,
1227 path,
1228 is_new,
1229 no_delete,
1230 } => EntryDialogState::from_schema(key, &value, &schema, &path, is_new, no_delete),
1231 NestedDialogInfo::ArrayItem {
1232 index,
1233 value,
1234 schema,
1235 path,
1236 is_new,
1237 } => EntryDialogState::for_array_item(index, &value, &schema, &path, is_new),
1238 };
1239 self.entry_dialog_stack.push(dialog);
1240 }
1241 }
1242
1243 pub fn save_entry_dialog(&mut self) {
1248 let is_array = if self.entry_dialog_stack.len() > 1 {
1252 self.entry_dialog_stack
1254 .get(self.entry_dialog_stack.len() - 2)
1255 .and_then(|parent| parent.current_item())
1256 .map(|item| matches!(item.control, SettingControl::ObjectArray(_)))
1257 .unwrap_or(false)
1258 } else {
1259 self.current_item()
1261 .map(|item| matches!(item.control, SettingControl::ObjectArray(_)))
1262 .unwrap_or(false)
1263 };
1264
1265 if is_array {
1266 self.save_array_item_dialog_inner();
1267 } else {
1268 self.save_map_entry_dialog_inner();
1269 }
1270 }
1271
1272 fn save_map_entry_dialog_inner(&mut self) {
1274 let Some(dialog) = self.entry_dialog_stack.pop() else {
1275 return;
1276 };
1277
1278 let key = dialog.get_key();
1280 if key.is_empty() {
1281 return; }
1283
1284 let value = dialog.to_value();
1285 let map_path = dialog.map_path.clone();
1286 let original_key = dialog.entry_key.clone();
1287 let is_new = dialog.is_new;
1288 let key_changed = !is_new && key != original_key;
1289
1290 if let Some(item) = self.current_item_mut() {
1292 if let SettingControl::Map(map_state) = &mut item.control {
1293 if key_changed {
1295 if let Some(idx) = map_state
1296 .entries
1297 .iter()
1298 .position(|(k, _)| k == &original_key)
1299 {
1300 map_state.entries.remove(idx);
1301 }
1302 }
1303
1304 if let Some(entry) = map_state.entries.iter_mut().find(|(k, _)| k == &key) {
1306 entry.1 = value.clone();
1307 } else {
1308 map_state.entries.push((key.clone(), value.clone()));
1309 map_state.entries.sort_by(|a, b| a.0.cmp(&b.0));
1310 }
1311 }
1312 }
1313
1314 if key_changed {
1316 let old_path = format!("{}/{}", map_path, original_key);
1317 self.pending_changes
1318 .insert(old_path, serde_json::Value::Null);
1319 }
1320
1321 let path = format!("{}/{}", map_path, key);
1323 self.set_pending_change(&path, value);
1324 }
1325
1326 fn save_array_item_dialog_inner(&mut self) {
1328 let Some(dialog) = self.entry_dialog_stack.pop() else {
1329 return;
1330 };
1331
1332 let value = dialog.to_value();
1333 let array_path = dialog.map_path.clone();
1334 let is_new = dialog.is_new;
1335 let entry_key = dialog.entry_key.clone();
1336
1337 let is_nested = !self.entry_dialog_stack.is_empty();
1339
1340 if is_nested {
1341 let parent_entry_path = self
1349 .entry_dialog_stack
1350 .last()
1351 .map(|p| p.entry_path())
1352 .unwrap_or_default();
1353 let item_path = array_path
1354 .strip_prefix(parent_entry_path.as_str())
1355 .unwrap_or(&array_path)
1356 .trim_end_matches('/')
1357 .to_string();
1358
1359 if let Some(parent) = self.entry_dialog_stack.last_mut() {
1361 if let Some(item) = parent.items.iter_mut().find(|i| i.path == item_path) {
1362 if let SettingControl::ObjectArray(array_state) = &mut item.control {
1363 if is_new {
1364 array_state.bindings.push(value.clone());
1365 } else if let Ok(index) = entry_key.parse::<usize>() {
1366 if index < array_state.bindings.len() {
1367 array_state.bindings[index] = value.clone();
1368 }
1369 }
1370 }
1371 }
1372 }
1373
1374 if let Some(parent) = self.entry_dialog_stack.last() {
1377 if let Some(item) = parent.items.iter().find(|i| i.path == item_path) {
1378 if let SettingControl::ObjectArray(array_state) = &item.control {
1379 let array_value = serde_json::Value::Array(array_state.bindings.clone());
1380 self.set_pending_change(&array_path, array_value);
1381 }
1382 }
1383 }
1384 } else {
1385 if let Some(item) = self.current_item_mut() {
1387 if let SettingControl::ObjectArray(array_state) = &mut item.control {
1388 if is_new {
1389 array_state.bindings.push(value.clone());
1390 } else if let Ok(index) = entry_key.parse::<usize>() {
1391 if index < array_state.bindings.len() {
1392 array_state.bindings[index] = value.clone();
1393 }
1394 }
1395 }
1396 }
1397
1398 if let Some(item) = self.current_item() {
1400 if let SettingControl::ObjectArray(array_state) = &item.control {
1401 let array_value = serde_json::Value::Array(array_state.bindings.clone());
1402 self.set_pending_change(&array_path, array_value);
1403 }
1404 }
1405 }
1406 }
1407
1408 pub fn delete_entry_dialog(&mut self) {
1410 let is_nested = self.entry_dialog_stack.len() > 1;
1412
1413 let Some(dialog) = self.entry_dialog_stack.pop() else {
1414 return;
1415 };
1416
1417 let path = format!("{}/{}", dialog.map_path, dialog.entry_key);
1418
1419 if is_nested {
1421 let map_field = dialog.map_path.rsplit('/').next().unwrap_or("").to_string();
1424 let item_path = format!("/{}", map_field);
1425
1426 if let Some(parent) = self.entry_dialog_stack.last_mut() {
1428 if let Some(item) = parent.items.iter_mut().find(|i| i.path == item_path) {
1429 if let SettingControl::Map(map_state) = &mut item.control {
1430 if let Some(idx) = map_state
1431 .entries
1432 .iter()
1433 .position(|(k, _)| k == &dialog.entry_key)
1434 {
1435 map_state.remove_entry(idx);
1436 }
1437 }
1438 }
1439 }
1440 } else {
1441 if let Some(item) = self.current_item_mut() {
1443 if let SettingControl::Map(map_state) = &mut item.control {
1444 if let Some(idx) = map_state
1445 .entries
1446 .iter()
1447 .position(|(k, _)| k == &dialog.entry_key)
1448 {
1449 map_state.remove_entry(idx);
1450 }
1451 }
1452 }
1453 }
1454
1455 self.set_pending_change(&path, serde_json::Value::Null);
1457 }
1458
1459 pub fn max_scroll(&self) -> u16 {
1461 self.scroll_panel.scroll.max_offset()
1462 }
1463
1464 pub fn scroll_up(&mut self, delta: usize) -> bool {
1467 let old = self.scroll_panel.scroll.offset;
1468 self.scroll_panel.scroll_up(delta as u16);
1469 old != self.scroll_panel.scroll.offset
1470 }
1471
1472 pub fn scroll_down(&mut self, delta: usize) -> bool {
1475 let old = self.scroll_panel.scroll.offset;
1476 self.scroll_panel.scroll_down(delta as u16);
1477 old != self.scroll_panel.scroll.offset
1478 }
1479
1480 pub fn scroll_to_ratio(&mut self, ratio: f32) -> bool {
1483 let old = self.scroll_panel.scroll.offset;
1484 self.scroll_panel.scroll_to_ratio(ratio);
1485 old != self.scroll_panel.scroll.offset
1486 }
1487
1488 pub fn is_number_control(&self) -> bool {
1491 self.current_item()
1492 .is_some_and(|item| matches!(item.control, SettingControl::Number(_)))
1493 }
1494
1495 pub fn start_editing(&mut self) {
1496 if let Some(item) = self.current_item() {
1497 if matches!(
1498 item.control,
1499 SettingControl::TextList(_)
1500 | SettingControl::DualList(_)
1501 | SettingControl::Text(_)
1502 | SettingControl::Map(_)
1503 | SettingControl::Json(_)
1504 ) {
1505 self.editing_text = true;
1506 }
1507 }
1508 if let Some(item) = self.current_item_mut() {
1509 if let SettingControl::DualList(ref mut dl) = item.control {
1510 dl.editing = true;
1511 }
1512 }
1513 }
1514
1515 pub fn stop_editing(&mut self) {
1517 self.editing_text = false;
1518 if let Some(item) = self.current_item_mut() {
1519 if let SettingControl::DualList(ref mut dl) = item.control {
1520 dl.editing = false;
1521 }
1522 }
1523 }
1524
1525 pub fn is_editable_control(&self) -> bool {
1527 self.current_item().is_some_and(|item| {
1528 matches!(
1529 item.control,
1530 SettingControl::TextList(_)
1531 | SettingControl::DualList(_)
1532 | SettingControl::Text(_)
1533 | SettingControl::Map(_)
1534 | SettingControl::Json(_)
1535 )
1536 })
1537 }
1538
1539 pub fn is_editing_json(&self) -> bool {
1541 if !self.editing_text {
1542 return false;
1543 }
1544 self.current_item()
1545 .map(|item| matches!(&item.control, SettingControl::Json(_)))
1546 .unwrap_or(false)
1547 }
1548
1549 pub fn text_insert(&mut self, c: char) {
1551 if let Some(item) = self.current_item_mut() {
1552 match &mut item.control {
1553 SettingControl::TextList(state) => state.insert(c),
1554 SettingControl::Text(state) => {
1555 state.value.insert(state.cursor, c);
1556 state.cursor += c.len_utf8();
1557 }
1558 SettingControl::Map(state) => {
1559 state.new_key_text.insert(state.cursor, c);
1560 state.cursor += c.len_utf8();
1561 }
1562 SettingControl::Json(state) => state.insert(c),
1563 _ => {}
1564 }
1565 }
1566 }
1567
1568 pub fn text_backspace(&mut self) {
1570 if let Some(item) = self.current_item_mut() {
1571 match &mut item.control {
1572 SettingControl::TextList(state) => state.backspace(),
1573 SettingControl::Text(state) => {
1574 if state.cursor > 0 {
1575 let mut char_start = state.cursor - 1;
1576 while char_start > 0 && !state.value.is_char_boundary(char_start) {
1577 char_start -= 1;
1578 }
1579 state.value.remove(char_start);
1580 state.cursor = char_start;
1581 }
1582 }
1583 SettingControl::Map(state) => {
1584 if state.cursor > 0 {
1585 let mut char_start = state.cursor - 1;
1586 while char_start > 0 && !state.new_key_text.is_char_boundary(char_start) {
1587 char_start -= 1;
1588 }
1589 state.new_key_text.remove(char_start);
1590 state.cursor = char_start;
1591 }
1592 }
1593 SettingControl::Json(state) => state.backspace(),
1594 _ => {}
1595 }
1596 }
1597 }
1598
1599 pub fn text_move_left(&mut self) {
1601 if let Some(item) = self.current_item_mut() {
1602 match &mut item.control {
1603 SettingControl::TextList(state) => state.move_left(),
1604 SettingControl::Text(state) => {
1605 if state.cursor > 0 {
1606 let mut new_pos = state.cursor - 1;
1607 while new_pos > 0 && !state.value.is_char_boundary(new_pos) {
1608 new_pos -= 1;
1609 }
1610 state.cursor = new_pos;
1611 }
1612 }
1613 SettingControl::Map(state) => {
1614 if state.cursor > 0 {
1615 let mut new_pos = state.cursor - 1;
1616 while new_pos > 0 && !state.new_key_text.is_char_boundary(new_pos) {
1617 new_pos -= 1;
1618 }
1619 state.cursor = new_pos;
1620 }
1621 }
1622 SettingControl::Json(state) => state.move_left(),
1623 _ => {}
1624 }
1625 }
1626 }
1627
1628 pub fn text_move_right(&mut self) {
1630 if let Some(item) = self.current_item_mut() {
1631 match &mut item.control {
1632 SettingControl::TextList(state) => state.move_right(),
1633 SettingControl::Text(state) => {
1634 if state.cursor < state.value.len() {
1635 let mut new_pos = state.cursor + 1;
1636 while new_pos < state.value.len() && !state.value.is_char_boundary(new_pos)
1637 {
1638 new_pos += 1;
1639 }
1640 state.cursor = new_pos;
1641 }
1642 }
1643 SettingControl::Map(state) => {
1644 if state.cursor < state.new_key_text.len() {
1645 let mut new_pos = state.cursor + 1;
1646 while new_pos < state.new_key_text.len()
1647 && !state.new_key_text.is_char_boundary(new_pos)
1648 {
1649 new_pos += 1;
1650 }
1651 state.cursor = new_pos;
1652 }
1653 }
1654 SettingControl::Json(state) => state.move_right(),
1655 _ => {}
1656 }
1657 }
1658 }
1659
1660 pub fn text_focus_prev(&mut self) {
1662 if let Some(item) = self.current_item_mut() {
1663 match &mut item.control {
1664 SettingControl::TextList(state) => state.focus_prev(),
1665 SettingControl::Map(state) => {
1666 state.focus_prev();
1667 }
1668 _ => {}
1669 }
1670 }
1671 }
1672
1673 pub fn text_focus_next(&mut self) {
1675 if let Some(item) = self.current_item_mut() {
1676 match &mut item.control {
1677 SettingControl::TextList(state) => state.focus_next(),
1678 SettingControl::Map(state) => {
1679 state.focus_next();
1680 }
1681 _ => {}
1682 }
1683 }
1684 }
1685
1686 pub fn text_add_item(&mut self) {
1688 if let Some(item) = self.current_item_mut() {
1689 match &mut item.control {
1690 SettingControl::TextList(state) => state.add_item(),
1691 SettingControl::Map(state) => state.add_entry_from_input(),
1692 _ => {}
1693 }
1694 }
1695 self.on_value_changed();
1697 }
1698
1699 pub fn text_remove_focused(&mut self) {
1701 if let Some(item) = self.current_item_mut() {
1702 match &mut item.control {
1703 SettingControl::TextList(state) => {
1704 if let Some(idx) = state.focused_item {
1705 state.remove_item(idx);
1706 }
1707 }
1708 SettingControl::Map(state) => {
1709 if let Some(idx) = state.focused_entry {
1710 state.remove_entry(idx);
1711 }
1712 }
1713 _ => {}
1714 }
1715 }
1716 self.on_value_changed();
1718 }
1719
1720 pub fn is_editing_dual_list(&self) -> bool {
1722 if !self.editing_text {
1723 return false;
1724 }
1725 self.current_item()
1726 .map(|item| matches!(&item.control, SettingControl::DualList(_)))
1727 .unwrap_or(false)
1728 }
1729
1730 pub fn with_dual_list_mut<R>(
1735 &mut self,
1736 item_idx: usize,
1737 f: impl FnOnce(&mut crate::view::controls::DualListState) -> R,
1738 ) -> Option<R> {
1739 let page = self.pages.get_mut(self.selected_category)?;
1740 let item = page.items.get_mut(item_idx)?;
1741 if let SettingControl::DualList(ref mut state) = item.control {
1742 Some(f(state))
1743 } else {
1744 None
1745 }
1746 }
1747
1748 pub fn with_current_dual_list_mut<R>(
1751 &mut self,
1752 f: impl FnOnce(&mut crate::view::controls::DualListState) -> R,
1753 ) -> Option<R> {
1754 if let Some(item) = self.current_item_mut() {
1755 if let SettingControl::DualList(ref mut state) = item.control {
1756 return Some(f(state));
1757 }
1758 }
1759 None
1760 }
1761
1762 pub fn refresh_dual_list_sibling(&mut self) {
1769 let (new_included, sibling_path) = {
1770 let Some(item) = self.current_item() else {
1771 return;
1772 };
1773 let SettingControl::DualList(state) = &item.control else {
1774 return;
1775 };
1776 let Some(ref sib_path) = item.dual_list_sibling else {
1777 return;
1778 };
1779 (state.included.clone(), sib_path.clone())
1780 };
1781
1782 if let Some(page) = self.pages.get_mut(self.selected_category) {
1784 for other in page.items.iter_mut() {
1785 if other.path == sibling_path {
1786 if let SettingControl::DualList(ref mut sib_state) = other.control {
1787 sib_state.excluded = new_included;
1788 }
1789 break;
1790 }
1791 }
1792 }
1793 }
1794
1795 pub fn json_cursor_up(&mut self) {
1799 if let Some(item) = self.current_item_mut() {
1800 if let SettingControl::Json(state) = &mut item.control {
1801 state.move_up();
1802 }
1803 }
1804 }
1805
1806 pub fn json_cursor_down(&mut self) {
1808 if let Some(item) = self.current_item_mut() {
1809 if let SettingControl::Json(state) = &mut item.control {
1810 state.move_down();
1811 }
1812 }
1813 }
1814
1815 pub fn json_insert_newline(&mut self) {
1817 if let Some(item) = self.current_item_mut() {
1818 if let SettingControl::Json(state) = &mut item.control {
1819 state.insert('\n');
1820 }
1821 }
1822 }
1823
1824 pub fn json_delete(&mut self) {
1826 if let Some(item) = self.current_item_mut() {
1827 if let SettingControl::Json(state) = &mut item.control {
1828 state.delete();
1829 }
1830 }
1831 }
1832
1833 pub fn json_exit_editing(&mut self) {
1835 let is_valid = self
1836 .current_item()
1837 .map(|item| {
1838 if let SettingControl::Json(state) = &item.control {
1839 state.is_valid()
1840 } else {
1841 true
1842 }
1843 })
1844 .unwrap_or(true);
1845
1846 if is_valid {
1847 if let Some(item) = self.current_item_mut() {
1848 if let SettingControl::Json(state) = &mut item.control {
1849 state.commit();
1850 }
1851 }
1852 self.on_value_changed();
1853 } else if let Some(item) = self.current_item_mut() {
1854 if let SettingControl::Json(state) = &mut item.control {
1855 state.revert();
1856 }
1857 }
1858 self.editing_text = false;
1859 }
1860
1861 pub fn json_select_all(&mut self) {
1863 if let Some(item) = self.current_item_mut() {
1864 if let SettingControl::Json(state) = &mut item.control {
1865 state.select_all();
1866 }
1867 }
1868 }
1869
1870 pub fn json_selected_text(&self) -> Option<String> {
1872 if let Some(item) = self.current_item() {
1873 if let SettingControl::Json(state) = &item.control {
1874 return state.selected_text();
1875 }
1876 }
1877 None
1878 }
1879
1880 pub fn json_cursor_up_selecting(&mut self) {
1882 if let Some(item) = self.current_item_mut() {
1883 if let SettingControl::Json(state) = &mut item.control {
1884 state.editor.move_up_selecting();
1885 }
1886 }
1887 }
1888
1889 pub fn json_cursor_down_selecting(&mut self) {
1891 if let Some(item) = self.current_item_mut() {
1892 if let SettingControl::Json(state) = &mut item.control {
1893 state.editor.move_down_selecting();
1894 }
1895 }
1896 }
1897
1898 pub fn json_cursor_left_selecting(&mut self) {
1900 if let Some(item) = self.current_item_mut() {
1901 if let SettingControl::Json(state) = &mut item.control {
1902 state.editor.move_left_selecting();
1903 }
1904 }
1905 }
1906
1907 pub fn json_cursor_right_selecting(&mut self) {
1909 if let Some(item) = self.current_item_mut() {
1910 if let SettingControl::Json(state) = &mut item.control {
1911 state.editor.move_right_selecting();
1912 }
1913 }
1914 }
1915
1916 pub fn is_dropdown_open(&self) -> bool {
1920 self.current_item().is_some_and(|item| {
1921 if let SettingControl::Dropdown(ref d) = item.control {
1922 d.open
1923 } else {
1924 false
1925 }
1926 })
1927 }
1928
1929 pub fn dropdown_toggle(&mut self) {
1931 let mut opened = false;
1932 if let Some(item) = self.current_item_mut() {
1933 if let SettingControl::Dropdown(ref mut d) = item.control {
1934 d.toggle_open();
1935 opened = d.open;
1936 }
1937 }
1938
1939 if opened {
1941 self.update_layout_widths();
1943 let selected_item = self.selected_item;
1944 if let Some(page) = self.pages.get(self.selected_category) {
1945 self.scroll_panel.update_content_height(&page.items);
1946 self.scroll_panel
1948 .ensure_focused_visible(&page.items, selected_item, None);
1949 }
1950 }
1951 }
1952
1953 pub fn dropdown_prev(&mut self) {
1955 if let Some(item) = self.current_item_mut() {
1956 if let SettingControl::Dropdown(ref mut d) = item.control {
1957 d.select_prev();
1958 }
1959 }
1960 }
1961
1962 pub fn dropdown_next(&mut self) {
1964 if let Some(item) = self.current_item_mut() {
1965 if let SettingControl::Dropdown(ref mut d) = item.control {
1966 d.select_next();
1967 }
1968 }
1969 }
1970
1971 pub fn dropdown_home(&mut self) {
1973 if let Some(item) = self.current_item_mut() {
1974 if let SettingControl::Dropdown(ref mut d) = item.control {
1975 if !d.options.is_empty() {
1976 d.selected = 0;
1977 d.ensure_visible();
1978 }
1979 }
1980 }
1981 }
1982
1983 pub fn dropdown_end(&mut self) {
1985 if let Some(item) = self.current_item_mut() {
1986 if let SettingControl::Dropdown(ref mut d) = item.control {
1987 if !d.options.is_empty() {
1988 d.selected = d.options.len() - 1;
1989 d.ensure_visible();
1990 }
1991 }
1992 }
1993 }
1994
1995 pub fn dropdown_confirm(&mut self) {
1997 if let Some(item) = self.current_item_mut() {
1998 if let SettingControl::Dropdown(ref mut d) = item.control {
1999 d.confirm();
2000 }
2001 }
2002 self.on_value_changed();
2003 }
2004
2005 pub fn dropdown_cancel(&mut self) {
2007 if let Some(item) = self.current_item_mut() {
2008 if let SettingControl::Dropdown(ref mut d) = item.control {
2009 d.cancel();
2010 }
2011 }
2012 }
2013
2014 pub fn dropdown_select(&mut self, option_idx: usize) {
2016 if let Some(item) = self.current_item_mut() {
2017 if let SettingControl::Dropdown(ref mut d) = item.control {
2018 if option_idx < d.options.len() {
2019 d.selected = option_idx;
2020 d.confirm();
2021 }
2022 }
2023 }
2024 self.on_value_changed();
2025 }
2026
2027 pub fn set_dropdown_hover(&mut self, hover_idx: Option<usize>) -> bool {
2030 if let Some(item) = self.current_item_mut() {
2031 if let SettingControl::Dropdown(ref mut d) = item.control {
2032 if d.open && d.hover_index != hover_idx {
2033 d.hover_index = hover_idx;
2034 return true;
2035 }
2036 }
2037 }
2038 false
2039 }
2040
2041 pub fn dropdown_scroll(&mut self, delta: i32) {
2043 if let Some(item) = self.current_item_mut() {
2044 if let SettingControl::Dropdown(ref mut d) = item.control {
2045 if d.open {
2046 d.scroll_by(delta);
2047 }
2048 }
2049 }
2050 }
2051
2052 pub fn is_number_editing(&self) -> bool {
2056 self.current_item().is_some_and(|item| {
2057 if let SettingControl::Number(ref n) = item.control {
2058 n.editing()
2059 } else {
2060 false
2061 }
2062 })
2063 }
2064
2065 pub fn start_number_editing(&mut self) {
2067 if let Some(item) = self.current_item_mut() {
2068 if let SettingControl::Number(ref mut n) = item.control {
2069 n.start_editing();
2070 }
2071 }
2072 }
2073
2074 pub fn number_insert(&mut self, c: char) {
2076 if let Some(item) = self.current_item_mut() {
2077 if let SettingControl::Number(ref mut n) = item.control {
2078 n.insert_char(c);
2079 }
2080 }
2081 }
2082
2083 pub fn number_backspace(&mut self) {
2085 if let Some(item) = self.current_item_mut() {
2086 if let SettingControl::Number(ref mut n) = item.control {
2087 n.backspace();
2088 }
2089 }
2090 }
2091
2092 pub fn number_confirm(&mut self) {
2094 if let Some(item) = self.current_item_mut() {
2095 if let SettingControl::Number(ref mut n) = item.control {
2096 n.confirm_editing();
2097 }
2098 }
2099 self.on_value_changed();
2100 }
2101
2102 pub fn number_cancel(&mut self) {
2104 if let Some(item) = self.current_item_mut() {
2105 if let SettingControl::Number(ref mut n) = item.control {
2106 n.cancel_editing();
2107 }
2108 }
2109 }
2110
2111 pub fn number_delete(&mut self) {
2113 if let Some(item) = self.current_item_mut() {
2114 if let SettingControl::Number(ref mut n) = item.control {
2115 n.delete();
2116 }
2117 }
2118 }
2119
2120 pub fn number_move_left(&mut self) {
2122 if let Some(item) = self.current_item_mut() {
2123 if let SettingControl::Number(ref mut n) = item.control {
2124 n.move_left();
2125 }
2126 }
2127 }
2128
2129 pub fn number_move_right(&mut self) {
2131 if let Some(item) = self.current_item_mut() {
2132 if let SettingControl::Number(ref mut n) = item.control {
2133 n.move_right();
2134 }
2135 }
2136 }
2137
2138 pub fn number_move_home(&mut self) {
2140 if let Some(item) = self.current_item_mut() {
2141 if let SettingControl::Number(ref mut n) = item.control {
2142 n.move_home();
2143 }
2144 }
2145 }
2146
2147 pub fn number_move_end(&mut self) {
2149 if let Some(item) = self.current_item_mut() {
2150 if let SettingControl::Number(ref mut n) = item.control {
2151 n.move_end();
2152 }
2153 }
2154 }
2155
2156 pub fn number_move_left_selecting(&mut self) {
2158 if let Some(item) = self.current_item_mut() {
2159 if let SettingControl::Number(ref mut n) = item.control {
2160 n.move_left_selecting();
2161 }
2162 }
2163 }
2164
2165 pub fn number_move_right_selecting(&mut self) {
2167 if let Some(item) = self.current_item_mut() {
2168 if let SettingControl::Number(ref mut n) = item.control {
2169 n.move_right_selecting();
2170 }
2171 }
2172 }
2173
2174 pub fn number_move_home_selecting(&mut self) {
2176 if let Some(item) = self.current_item_mut() {
2177 if let SettingControl::Number(ref mut n) = item.control {
2178 n.move_home_selecting();
2179 }
2180 }
2181 }
2182
2183 pub fn number_move_end_selecting(&mut self) {
2185 if let Some(item) = self.current_item_mut() {
2186 if let SettingControl::Number(ref mut n) = item.control {
2187 n.move_end_selecting();
2188 }
2189 }
2190 }
2191
2192 pub fn number_move_word_left(&mut self) {
2194 if let Some(item) = self.current_item_mut() {
2195 if let SettingControl::Number(ref mut n) = item.control {
2196 n.move_word_left();
2197 }
2198 }
2199 }
2200
2201 pub fn number_move_word_right(&mut self) {
2203 if let Some(item) = self.current_item_mut() {
2204 if let SettingControl::Number(ref mut n) = item.control {
2205 n.move_word_right();
2206 }
2207 }
2208 }
2209
2210 pub fn number_move_word_left_selecting(&mut self) {
2212 if let Some(item) = self.current_item_mut() {
2213 if let SettingControl::Number(ref mut n) = item.control {
2214 n.move_word_left_selecting();
2215 }
2216 }
2217 }
2218
2219 pub fn number_move_word_right_selecting(&mut self) {
2221 if let Some(item) = self.current_item_mut() {
2222 if let SettingControl::Number(ref mut n) = item.control {
2223 n.move_word_right_selecting();
2224 }
2225 }
2226 }
2227
2228 pub fn number_select_all(&mut self) {
2230 if let Some(item) = self.current_item_mut() {
2231 if let SettingControl::Number(ref mut n) = item.control {
2232 n.select_all();
2233 }
2234 }
2235 }
2236
2237 pub fn number_delete_word_backward(&mut self) {
2239 if let Some(item) = self.current_item_mut() {
2240 if let SettingControl::Number(ref mut n) = item.control {
2241 n.delete_word_backward();
2242 }
2243 }
2244 }
2245
2246 pub fn number_delete_word_forward(&mut self) {
2248 if let Some(item) = self.current_item_mut() {
2249 if let SettingControl::Number(ref mut n) = item.control {
2250 n.delete_word_forward();
2251 }
2252 }
2253 }
2254
2255 pub fn get_change_descriptions(&self) -> Vec<String> {
2257 let mut descriptions: Vec<String> = self
2258 .pending_changes
2259 .iter()
2260 .map(|(path, value)| {
2261 let value_str = match value {
2262 serde_json::Value::Bool(b) => b.to_string(),
2263 serde_json::Value::Number(n) => n.to_string(),
2264 serde_json::Value::String(s) => format!("\"{}\"", s),
2265 _ => value.to_string(),
2266 };
2267 format!("{}: {}", path, value_str)
2268 })
2269 .collect();
2270 for path in &self.pending_deletions {
2272 descriptions.push(format!("{}: (reset to default)", path));
2273 }
2274 descriptions.sort();
2275 descriptions
2276 }
2277}
2278
2279fn update_control_from_value(control: &mut SettingControl, value: &serde_json::Value) {
2281 match control {
2282 SettingControl::Toggle(state) => {
2283 if let Some(b) = value.as_bool() {
2284 state.checked = b;
2285 }
2286 }
2287 SettingControl::Number(state) => {
2288 if let Some(n) = value.as_i64() {
2289 state.value = n;
2290 }
2291 }
2292 SettingControl::Dropdown(state) => {
2293 if let Some(s) = value.as_str() {
2294 if let Some(idx) = state.options.iter().position(|o| o == s) {
2295 state.selected = idx;
2296 }
2297 }
2298 }
2299 SettingControl::Text(state) => {
2300 if let Some(s) = value.as_str() {
2301 state.value = s.to_string();
2302 state.cursor = state.value.len();
2303 }
2304 }
2305 SettingControl::TextList(state) => {
2306 if let Some(arr) = value.as_array() {
2307 state.items = arr
2308 .iter()
2309 .filter_map(|v| {
2310 if state.is_integer {
2311 v.as_i64()
2312 .map(|n| n.to_string())
2313 .or_else(|| v.as_u64().map(|n| n.to_string()))
2314 .or_else(|| v.as_f64().map(|n| n.to_string()))
2315 } else {
2316 v.as_str().map(String::from)
2317 }
2318 })
2319 .collect();
2320 }
2321 }
2322 SettingControl::DualList(state) => {
2323 if let Some(arr) = value.as_array() {
2324 state.included = arr
2325 .iter()
2326 .filter_map(|v| v.as_str().map(String::from))
2327 .collect();
2328 }
2329 }
2330 SettingControl::Map(state) => {
2331 if let Some(obj) = value.as_object() {
2332 state.entries = obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
2333 state.entries.sort_by(|a, b| a.0.cmp(&b.0));
2334 }
2335 }
2336 SettingControl::ObjectArray(state) => {
2337 if let Some(arr) = value.as_array() {
2338 state.bindings = arr.clone();
2339 }
2340 }
2341 SettingControl::Json(state) => {
2342 let json_str =
2344 serde_json::to_string_pretty(value).unwrap_or_else(|_| "null".to_string());
2345 let json_str = if json_str.is_empty() {
2346 "null".to_string()
2347 } else {
2348 json_str
2349 };
2350 state.original_text = json_str.clone();
2351 state.editor.set_value(&json_str);
2352 state.scroll_offset = 0;
2353 }
2354 SettingControl::Complex { .. } => {}
2355 }
2356}
2357
2358#[cfg(test)]
2359mod tests {
2360 use super::*;
2361
2362 const TEST_SCHEMA: &str = r#"
2363{
2364 "type": "object",
2365 "properties": {
2366 "theme": {
2367 "type": "string",
2368 "default": "dark"
2369 },
2370 "line_numbers": {
2371 "type": "boolean",
2372 "default": true
2373 }
2374 },
2375 "$defs": {}
2376}
2377"#;
2378
2379 fn test_config() -> Config {
2380 Config::default()
2381 }
2382
2383 #[test]
2384 fn test_settings_state_creation() {
2385 let config = test_config();
2386 let state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2387
2388 assert!(!state.visible);
2389 assert_eq!(state.selected_category, 0);
2390 assert!(!state.has_changes());
2391 }
2392
2393 #[test]
2394 fn test_navigation() {
2395 let config = test_config();
2396 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2397
2398 assert_eq!(state.focus_panel(), FocusPanel::Categories);
2400
2401 state.toggle_focus();
2403 assert_eq!(state.focus_panel(), FocusPanel::Settings);
2404
2405 state.select_next();
2407 assert_eq!(state.selected_item, 1);
2408
2409 state.select_prev();
2410 assert_eq!(state.selected_item, 0);
2411 }
2412
2413 #[test]
2414 fn test_pending_changes() {
2415 let config = test_config();
2416 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2417
2418 assert!(!state.has_changes());
2419
2420 state.set_pending_change("/theme", serde_json::Value::String("light".to_string()));
2421 assert!(state.has_changes());
2422
2423 state.discard_changes();
2424 assert!(!state.has_changes());
2425 }
2426
2427 #[test]
2428 fn test_show_hide() {
2429 let config = test_config();
2430 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2431
2432 assert!(!state.visible);
2433
2434 state.show();
2435 assert!(state.visible);
2436 assert_eq!(state.focus_panel(), FocusPanel::Categories);
2437
2438 state.hide();
2439 assert!(!state.visible);
2440 }
2441
2442 const TEST_SCHEMA_CONTROLS: &str = r#"
2444{
2445 "type": "object",
2446 "properties": {
2447 "theme": {
2448 "type": "string",
2449 "enum": ["dark", "light", "high-contrast"],
2450 "default": "dark"
2451 },
2452 "tab_size": {
2453 "type": "integer",
2454 "minimum": 1,
2455 "maximum": 8,
2456 "default": 4
2457 },
2458 "line_numbers": {
2459 "type": "boolean",
2460 "default": true
2461 }
2462 },
2463 "$defs": {}
2464}
2465"#;
2466
2467 #[test]
2468 fn test_dropdown_toggle() {
2469 let config = test_config();
2470 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
2471 state.show();
2472 state.toggle_focus(); state.select_next();
2477 state.select_next();
2478 assert!(!state.is_dropdown_open());
2479
2480 state.dropdown_toggle();
2481 assert!(state.is_dropdown_open());
2482
2483 state.dropdown_toggle();
2484 assert!(!state.is_dropdown_open());
2485 }
2486
2487 #[test]
2488 fn test_dropdown_cancel_restores() {
2489 let config = test_config();
2490 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
2491 state.show();
2492 state.toggle_focus();
2493
2494 state.select_next();
2497 state.select_next();
2498
2499 state.dropdown_toggle();
2501 assert!(state.is_dropdown_open());
2502
2503 let initial = state.current_item().and_then(|item| {
2505 if let SettingControl::Dropdown(ref d) = item.control {
2506 Some(d.selected)
2507 } else {
2508 None
2509 }
2510 });
2511
2512 state.dropdown_next();
2514 let after_change = state.current_item().and_then(|item| {
2515 if let SettingControl::Dropdown(ref d) = item.control {
2516 Some(d.selected)
2517 } else {
2518 None
2519 }
2520 });
2521 assert_ne!(initial, after_change);
2522
2523 state.dropdown_cancel();
2525 assert!(!state.is_dropdown_open());
2526
2527 let after_cancel = state.current_item().and_then(|item| {
2528 if let SettingControl::Dropdown(ref d) = item.control {
2529 Some(d.selected)
2530 } else {
2531 None
2532 }
2533 });
2534 assert_eq!(initial, after_cancel);
2535 }
2536
2537 #[test]
2538 fn test_dropdown_confirm_keeps_selection() {
2539 let config = test_config();
2540 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
2541 state.show();
2542 state.toggle_focus();
2543
2544 state.dropdown_toggle();
2546
2547 state.dropdown_next();
2549 let after_change = state.current_item().and_then(|item| {
2550 if let SettingControl::Dropdown(ref d) = item.control {
2551 Some(d.selected)
2552 } else {
2553 None
2554 }
2555 });
2556
2557 state.dropdown_confirm();
2559 assert!(!state.is_dropdown_open());
2560
2561 let after_confirm = state.current_item().and_then(|item| {
2562 if let SettingControl::Dropdown(ref d) = item.control {
2563 Some(d.selected)
2564 } else {
2565 None
2566 }
2567 });
2568 assert_eq!(after_change, after_confirm);
2569 }
2570
2571 #[test]
2572 fn test_number_editing() {
2573 let config = test_config();
2574 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
2575 state.show();
2576 state.toggle_focus();
2577
2578 state.select_next();
2580
2581 assert!(!state.is_number_editing());
2583
2584 state.start_number_editing();
2586 assert!(state.is_number_editing());
2587
2588 state.number_insert('8');
2590
2591 state.number_confirm();
2593 assert!(!state.is_number_editing());
2594 }
2595
2596 #[test]
2597 fn test_number_cancel_editing() {
2598 let config = test_config();
2599 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
2600 state.show();
2601 state.toggle_focus();
2602
2603 state.select_next();
2605
2606 let initial_value = state.current_item().and_then(|item| {
2608 if let SettingControl::Number(ref n) = item.control {
2609 Some(n.value)
2610 } else {
2611 None
2612 }
2613 });
2614
2615 state.start_number_editing();
2617 state.number_backspace();
2618 state.number_insert('9');
2619 state.number_insert('9');
2620
2621 state.number_cancel();
2623 assert!(!state.is_number_editing());
2624
2625 let after_cancel = state.current_item().and_then(|item| {
2627 if let SettingControl::Number(ref n) = item.control {
2628 Some(n.value)
2629 } else {
2630 None
2631 }
2632 });
2633 assert_eq!(initial_value, after_cancel);
2634 }
2635
2636 #[test]
2637 fn test_number_backspace() {
2638 let config = test_config();
2639 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
2640 state.show();
2641 state.toggle_focus();
2642 state.select_next();
2643
2644 state.start_number_editing();
2645 state.number_backspace();
2646
2647 let display_text = state.current_item().and_then(|item| {
2649 if let SettingControl::Number(ref n) = item.control {
2650 Some(n.display_text())
2651 } else {
2652 None
2653 }
2654 });
2655 assert_eq!(display_text, Some(String::new()));
2657
2658 state.number_cancel();
2659 }
2660
2661 #[test]
2662 fn test_layer_selection() {
2663 let config = test_config();
2664 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2665
2666 assert_eq!(state.target_layer, ConfigLayer::User);
2668 assert_eq!(state.target_layer_name(), "User");
2669
2670 state.cycle_target_layer();
2672 assert_eq!(state.target_layer, ConfigLayer::Project);
2673 assert_eq!(state.target_layer_name(), "Project");
2674
2675 state.cycle_target_layer();
2676 assert_eq!(state.target_layer, ConfigLayer::Session);
2677 assert_eq!(state.target_layer_name(), "Session");
2678
2679 state.cycle_target_layer();
2680 assert_eq!(state.target_layer, ConfigLayer::User);
2681
2682 state.set_target_layer(ConfigLayer::Project);
2684 assert_eq!(state.target_layer, ConfigLayer::Project);
2685
2686 state.set_target_layer(ConfigLayer::System);
2688 assert_eq!(state.target_layer, ConfigLayer::Project);
2689 }
2690
2691 #[test]
2692 fn test_layer_switch_clears_pending_changes() {
2693 let config = test_config();
2694 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2695
2696 state.set_pending_change("/theme", serde_json::Value::String("light".to_string()));
2698 assert!(state.has_changes());
2699
2700 state.cycle_target_layer();
2702 assert!(!state.has_changes());
2703 }
2704
2705 #[test]
2724 fn nested_array_save_records_full_entry_path() {
2725 use crate::view::settings::schema::SettingType;
2728
2729 let config = test_config();
2730 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2731
2732 let item_schema = SettingSchema {
2734 path: "/item".to_string(),
2735 name: "Server".to_string(),
2736 description: None,
2737 setting_type: SettingType::Object {
2738 properties: vec![SettingSchema {
2739 path: "/enabled".to_string(),
2740 name: "Enabled".to_string(),
2741 description: None,
2742 setting_type: SettingType::Boolean,
2743 default: Some(serde_json::json!(false)),
2744 read_only: false,
2745 section: None,
2746 order: None,
2747 nullable: false,
2748 enum_from: None,
2749 dual_list_sibling: None,
2750 }],
2751 },
2752 default: None,
2753 read_only: false,
2754 section: None,
2755 order: None,
2756 nullable: false,
2757 enum_from: None,
2758 dual_list_sibling: None,
2759 };
2760
2761 let value_schema = SettingSchema {
2766 path: String::new(),
2767 name: "value".to_string(),
2768 description: None,
2769 setting_type: SettingType::ObjectArray {
2770 item_schema: Box::new(item_schema.clone()),
2771 display_field: None,
2772 },
2773 default: None,
2774 read_only: false,
2775 section: None,
2776 order: None,
2777 nullable: false,
2778 enum_from: None,
2779 dual_list_sibling: None,
2780 };
2781
2782 let parent = EntryDialogState::from_schema(
2786 "quicklsp".to_string(),
2787 &serde_json::json!([{ "enabled": true }]),
2788 &value_schema,
2789 "/universal_lsp",
2790 false, false,
2792 );
2793
2794 assert!(
2796 parent.is_single_value,
2797 "array value_schema should trigger is_single_value path"
2798 );
2799 assert_eq!(parent.entry_path(), "/universal_lsp/quicklsp");
2800
2801 state.entry_dialog_stack.push(parent);
2802
2803 state.open_nested_entry_dialog();
2808
2809 assert_eq!(
2811 state.entry_dialog_stack.len(),
2812 2,
2813 "open_nested_entry_dialog should have pushed a nested dialog"
2814 );
2815
2816 let nested_map_path = state
2819 .entry_dialog_stack
2820 .last()
2821 .map(|d| d.map_path.clone())
2822 .unwrap();
2823 assert_eq!(
2824 nested_map_path, "/universal_lsp/quicklsp",
2825 "BUG: nested dialog's map_path dropped the 'quicklsp' key segment"
2826 );
2827
2828 state.save_entry_dialog();
2830
2831 assert_eq!(state.entry_dialog_stack.len(), 1);
2833
2834 assert!(
2837 !state.pending_changes.contains_key("/universal_lsp/"),
2838 "regression: pending change recorded under empty-key path /universal_lsp/. \
2839 All keys: {:?}",
2840 state.pending_changes.keys().collect::<Vec<_>>()
2841 );
2842 assert!(
2843 !state
2844 .pending_changes
2845 .keys()
2846 .any(|k| k.starts_with("/universal_lsp") && k.ends_with('/')),
2847 "no /universal_lsp/* path should end in a trailing slash; got {:?}",
2848 state.pending_changes.keys().collect::<Vec<_>>()
2849 );
2850 assert!(
2851 state
2852 .pending_changes
2853 .contains_key("/universal_lsp/quicklsp"),
2854 "expected pending change at /universal_lsp/quicklsp, got {:?}",
2855 state.pending_changes.keys().collect::<Vec<_>>()
2856 );
2857 }
2858
2859 #[test]
2860 fn test_refresh_dual_list_sibling_updates_excluded() {
2861 use crate::view::controls::DualListState;
2862
2863 let schema = include_str!("../../../plugins/config-schema.json");
2866 let config = test_config();
2867 let mut state = SettingsState::new(schema, &config).unwrap();
2868
2869 let editor_page_idx = state
2871 .pages
2872 .iter()
2873 .position(|p| p.path == "/editor")
2874 .expect("editor page");
2875 state.selected_category = editor_page_idx;
2876
2877 let (left_idx, right_idx) = {
2878 let page = &state.pages[editor_page_idx];
2879 let l = page
2880 .items
2881 .iter()
2882 .position(|i| i.path == "/editor/status_bar/left")
2883 .expect("left item");
2884 let r = page
2885 .items
2886 .iter()
2887 .position(|i| i.path == "/editor/status_bar/right")
2888 .expect("right item");
2889 (l, r)
2890 };
2891
2892 assert!(matches!(
2894 &state.pages[editor_page_idx].items[left_idx].control,
2895 SettingControl::DualList(_)
2896 ));
2897
2898 let default_right_items: Vec<String> =
2900 match &state.pages[editor_page_idx].items[right_idx].control {
2901 SettingControl::DualList(dl) => dl.included.clone(),
2902 _ => panic!("right should be DualList"),
2903 };
2904 let initial_left_excluded: Vec<String> =
2905 match &state.pages[editor_page_idx].items[left_idx].control {
2906 SettingControl::DualList(dl) => dl.excluded.clone(),
2907 _ => panic!("left should be DualList"),
2908 };
2909 assert_eq!(
2910 initial_left_excluded, default_right_items,
2911 "left.excluded should mirror right's included on initial build"
2912 );
2913
2914 let new_element = "{chord}".to_string();
2916 state.selected_item = left_idx;
2917 state
2918 .with_current_dual_list_mut(|dl: &mut DualListState| {
2919 if !dl.included.contains(&new_element) {
2920 dl.included.push(new_element.clone());
2921 }
2922 })
2923 .expect("current item is a DualList");
2924
2925 state.refresh_dual_list_sibling();
2927
2928 match &state.pages[editor_page_idx].items[right_idx].control {
2929 SettingControl::DualList(dl) => {
2930 assert!(
2931 dl.excluded.contains(&new_element),
2932 "right.excluded should be updated to reflect left's new inclusion"
2933 );
2934 }
2935 _ => panic!("right should be DualList"),
2936 }
2937 }
2938
2939 #[test]
2940 fn test_with_dual_list_mut_returns_none_for_non_dual_list() {
2941 let config = test_config();
2942 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2943
2944 let result = state.with_dual_list_mut(0, |_| ());
2946 assert!(result.is_none());
2947 }
2948}