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) => {
303 state.focus = focus_state;
304 if !focused {
308 state.editing = false;
309 }
310 }
311 SettingControl::Json(_) | SettingControl::Complex { .. } => {} }
313 }
314 }
315
316 fn update_map_sub_focus(&mut self) {
319 self.sub_focus = self.current_item().and_then(|item| {
320 if let SettingControl::Map(ref map_state) = item.control {
321 Some(match map_state.focused_entry {
323 Some(i) => 1 + i,
324 None => 1 + map_state.entries.len(), })
326 } else {
327 None
328 }
329 });
330 }
331
332 pub fn select_prev(&mut self) {
334 match self.focus_panel() {
335 FocusPanel::Categories => {
336 if self.selected_category > 0 {
337 self.update_control_focus(false); self.selected_category -= 1;
339 self.selected_item = 0;
340 self.scroll_panel = ScrollablePanel::new();
341 self.sub_focus = None;
342 self.update_control_focus(true); }
344 }
345 FocusPanel::Settings => {
346 let handled = self
348 .current_item_mut()
349 .and_then(|item| match &mut item.control {
350 SettingControl::Map(map_state) => Some(map_state.focus_prev()),
351 _ => None,
352 })
353 .unwrap_or(false);
354
355 if handled {
356 self.update_map_sub_focus();
358 } else if self.selected_item > 0 {
359 self.update_control_focus(false); self.selected_item -= 1;
361 self.sub_focus = None;
362 self.init_map_focus(false); self.update_control_focus(true); }
365 self.ensure_visible();
366 }
367 FocusPanel::Footer => {
368 if self.footer_button_index > 0 {
370 self.footer_button_index -= 1;
371 }
372 }
373 }
374 }
375
376 pub fn select_next(&mut self) {
378 match self.focus_panel() {
379 FocusPanel::Categories => {
380 if self.selected_category + 1 < self.pages.len() {
381 self.update_control_focus(false); self.selected_category += 1;
383 self.selected_item = 0;
384 self.scroll_panel = ScrollablePanel::new();
385 self.sub_focus = None;
386 self.update_control_focus(true); }
388 }
389 FocusPanel::Settings => {
390 let handled = self
392 .current_item_mut()
393 .and_then(|item| match &mut item.control {
394 SettingControl::Map(map_state) => Some(map_state.focus_next()),
395 _ => None,
396 })
397 .unwrap_or(false);
398
399 if handled {
400 self.update_map_sub_focus();
402 } else {
403 let can_move = self
404 .current_page()
405 .is_some_and(|page| self.selected_item + 1 < page.items.len());
406 if can_move {
407 self.update_control_focus(false); self.selected_item += 1;
409 self.sub_focus = None;
410 self.init_map_focus(true); self.update_control_focus(true); }
413 }
414 self.ensure_visible();
415 }
416 FocusPanel::Footer => {
417 if self.footer_button_index < 2 {
419 self.footer_button_index += 1;
420 }
421 }
422 }
423 }
424
425 pub fn select_next_page(&mut self) {
427 let page_size = self.scroll_panel.viewport_height().max(1);
428 for _ in 0..page_size {
429 self.select_next();
430 }
431 }
432
433 pub fn select_prev_page(&mut self) {
435 let page_size = self.scroll_panel.viewport_height().max(1);
436 for _ in 0..page_size {
437 self.select_prev();
438 }
439 }
440
441 pub fn toggle_focus(&mut self) {
443 let old_panel = self.focus_panel();
444 self.focus.focus_next();
445 self.on_panel_changed(old_panel, true);
446 }
447
448 pub fn toggle_focus_backward(&mut self) {
450 let old_panel = self.focus_panel();
451 self.focus.focus_prev();
452 self.on_panel_changed(old_panel, false);
453 }
454
455 fn on_panel_changed(&mut self, old_panel: FocusPanel, forward: bool) {
457 if old_panel == FocusPanel::Settings {
459 self.update_control_focus(false);
460 }
461
462 if self.focus_panel() == FocusPanel::Settings
464 && self.selected_item >= self.current_page().map_or(0, |p| p.items.len())
465 {
466 self.selected_item = 0;
467 }
468 self.sub_focus = None;
469
470 if self.focus_panel() == FocusPanel::Settings {
471 self.init_map_focus(forward); self.update_control_focus(true); }
474
475 if self.focus_panel() == FocusPanel::Footer {
477 self.footer_button_index = if forward {
478 0 } else {
480 4 };
482 }
483
484 self.ensure_visible();
485 }
486
487 pub fn update_layout_widths(&mut self) {
491 let width = self.layout_width;
492 if width > 0 {
493 if let Some(page) = self.pages.get_mut(self.selected_category) {
494 for item in &mut page.items {
495 item.layout_width = width;
496 }
497 }
498 }
499 }
500
501 pub fn ensure_visible(&mut self) {
502 if self.focus_panel() != FocusPanel::Settings {
503 return;
504 }
505
506 self.update_layout_widths();
507
508 let selected_item = self.selected_item;
510 let sub_focus = self.sub_focus;
511 if let Some(page) = self.pages.get(self.selected_category) {
512 self.scroll_panel
513 .ensure_focused_visible(&page.items, selected_item, sub_focus);
514 }
515 }
516
517 pub fn set_pending_change(&mut self, path: &str, value: serde_json::Value) {
519 let original = self.original_config.pointer(path);
521 if original == Some(&value) {
522 self.pending_changes.remove(path);
523 } else {
524 self.pending_changes.insert(path.to_string(), value);
525 }
526 }
527
528 pub fn has_changes(&self) -> bool {
530 !self.pending_changes.is_empty() || !self.pending_deletions.is_empty()
531 }
532
533 pub fn apply_changes(&self, config: &Config) -> Result<Config, serde_json::Error> {
535 let mut config_value = serde_json::to_value(config)?;
536
537 for (path, value) in &self.pending_changes {
538 if let Some(target) = config_value.pointer_mut(path) {
539 *target = value.clone();
540 }
541 }
542
543 serde_json::from_value(config_value)
544 }
545
546 pub fn discard_changes(&mut self) {
548 self.pending_changes.clear();
549 self.pending_deletions.clear();
550 self.pages = super::items::build_pages(
552 &self.categories,
553 &self.original_config,
554 &self.layer_sources,
555 self.target_layer,
556 );
557 }
558
559 pub fn set_target_layer(&mut self, layer: ConfigLayer) {
561 if layer != ConfigLayer::System {
562 self.target_layer = layer;
564 self.pending_changes.clear();
566 self.pending_deletions.clear();
567 self.pages = super::items::build_pages(
569 &self.categories,
570 &self.original_config,
571 &self.layer_sources,
572 self.target_layer,
573 );
574 }
575 }
576
577 pub fn cycle_target_layer(&mut self) {
579 self.target_layer = match self.target_layer {
580 ConfigLayer::System => ConfigLayer::User, ConfigLayer::User => ConfigLayer::Project,
582 ConfigLayer::Project => ConfigLayer::Session,
583 ConfigLayer::Session => ConfigLayer::User,
584 };
585 self.pending_changes.clear();
587 self.pending_deletions.clear();
588 self.pages = super::items::build_pages(
590 &self.categories,
591 &self.original_config,
592 &self.layer_sources,
593 self.target_layer,
594 );
595 }
596
597 pub fn target_layer_name(&self) -> &'static str {
599 match self.target_layer {
600 ConfigLayer::System => "System (read-only)",
601 ConfigLayer::User => "User",
602 ConfigLayer::Project => "Project",
603 ConfigLayer::Session => "Session",
604 }
605 }
606
607 pub fn set_layer_sources(&mut self, sources: HashMap<String, ConfigLayer>) {
610 self.layer_sources = sources;
611 self.pages = super::items::build_pages(
613 &self.categories,
614 &self.original_config,
615 &self.layer_sources,
616 self.target_layer,
617 );
618 }
619
620 pub fn get_layer_source(&self, path: &str) -> ConfigLayer {
623 self.layer_sources
624 .get(path)
625 .copied()
626 .unwrap_or(ConfigLayer::System)
627 }
628
629 pub fn layer_source_label(layer: ConfigLayer) -> &'static str {
631 match layer {
632 ConfigLayer::System => "default",
633 ConfigLayer::User => "user",
634 ConfigLayer::Project => "project",
635 ConfigLayer::Session => "session",
636 }
637 }
638
639 pub fn reset_current_to_default(&mut self) {
647 let reset_info = self.current_item().and_then(|item| {
649 if !item.modified || item.is_auto_managed {
652 return None;
653 }
654 item.default
655 .as_ref()
656 .map(|default| (item.path.clone(), default.clone()))
657 });
658
659 if let Some((path, default)) = reset_info {
660 self.pending_deletions.insert(path.clone());
662 self.pending_changes.remove(&path);
664
665 if let Some(item) = self.current_item_mut() {
669 update_control_from_value(&mut item.control, &default);
670 item.modified = false;
671 item.layer_source = ConfigLayer::System; }
674 }
675 }
676
677 pub fn set_current_to_null(&mut self) {
683 let target_layer = self.target_layer;
684 let change_info = self.current_item().and_then(|item| {
685 if !item.nullable || item.is_null || item.read_only {
686 return None;
687 }
688 Some(item.path.clone())
689 });
690
691 if let Some(path) = change_info {
692 self.pending_changes
694 .insert(path.clone(), serde_json::Value::Null);
695 self.pending_deletions.remove(&path);
696
697 if let Some(item) = self.current_item_mut() {
699 item.is_null = true;
700 item.modified = true;
701 item.layer_source = target_layer;
702 }
703 }
704 }
705
706 pub fn clear_current_category(&mut self) {
712 let target_layer = self.target_layer;
713 let page = match self.current_page() {
714 Some(p) if p.nullable => p,
715 _ => return,
716 };
717 let page_path = page.path.clone();
718
719 self.pending_changes
721 .insert(page_path.clone(), serde_json::Value::Null);
722
723 let prefix = format!("{}/", page_path);
725 self.pending_changes
726 .retain(|path, _| !path.starts_with(&prefix));
727 self.pending_deletions
728 .retain(|path| !path.starts_with(&prefix));
729
730 if let Some(page) = self.current_page_mut() {
732 for item in &mut page.items {
733 if item.nullable {
734 item.is_null = true;
735 item.modified = false;
736 item.layer_source = target_layer;
737 }
738 }
739 }
740 }
741
742 pub fn current_category_has_values(&self) -> bool {
744 match self.current_page() {
745 Some(page) if page.nullable => {
746 page.items.iter().any(|item| !item.is_null && item.nullable)
747 || page.items.iter().any(|item| item.modified)
748 }
749 _ => false,
750 }
751 }
752
753 pub fn on_value_changed(&mut self) {
755 let target_layer = self.target_layer;
757
758 let change_info = self.current_item().map(|item| {
760 let value = control_to_value(&item.control);
761 (item.path.clone(), value)
762 });
763
764 if let Some((path, value)) = change_info {
765 self.pending_deletions.remove(&path);
768
769 if let Some(item) = self.current_item_mut() {
771 item.modified = true; item.layer_source = target_layer; item.is_null = false; }
775 self.set_pending_change(&path, value);
776 }
777 }
778
779 pub fn update_focus_states(&mut self) {
781 let current_focus = self.focus_panel();
782 for (page_idx, page) in self.pages.iter_mut().enumerate() {
783 for (item_idx, item) in page.items.iter_mut().enumerate() {
784 let is_focused = current_focus == FocusPanel::Settings
785 && page_idx == self.selected_category
786 && item_idx == self.selected_item;
787
788 let focus = if is_focused {
789 FocusState::Focused
790 } else {
791 FocusState::Normal
792 };
793
794 match &mut item.control {
795 SettingControl::Toggle(state) => state.focus = focus,
796 SettingControl::Number(state) => state.focus = focus,
797 SettingControl::Dropdown(state) => state.focus = focus,
798 SettingControl::Text(state) => state.focus = focus,
799 SettingControl::TextList(state) => state.focus = focus,
800 SettingControl::DualList(state) => state.focus = focus,
801 SettingControl::Map(state) => state.focus = focus,
802 SettingControl::ObjectArray(state) => state.focus = focus,
803 SettingControl::Json(state) => state.focus = focus,
804 SettingControl::Complex { .. } => {}
805 }
806 }
807 }
808 }
809
810 pub fn start_search(&mut self) {
812 self.search_active = true;
813 self.search_query.clear();
814 self.search_results.clear();
815 self.selected_search_result = 0;
816 self.search_scroll_offset = 0;
817 }
818
819 pub fn cancel_search(&mut self) {
821 self.search_active = false;
822 self.search_query.clear();
823 self.search_results.clear();
824 self.selected_search_result = 0;
825 self.search_scroll_offset = 0;
826 }
827
828 pub fn set_search_query(&mut self, query: String) {
830 self.search_query = query;
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_push_char(&mut self, c: char) {
838 self.search_query.push(c);
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_pop_char(&mut self) {
846 self.search_query.pop();
847 self.search_results = search_settings(&self.pages, &self.search_query);
848 self.selected_search_result = 0;
849 self.search_scroll_offset = 0;
850 }
851
852 pub fn search_prev(&mut self) {
854 if !self.search_results.is_empty() && self.selected_search_result > 0 {
855 self.selected_search_result -= 1;
856 if self.selected_search_result < self.search_scroll_offset {
858 self.search_scroll_offset = self.selected_search_result;
859 }
860 }
861 }
862
863 pub fn search_next(&mut self) {
865 if !self.search_results.is_empty()
866 && self.selected_search_result + 1 < self.search_results.len()
867 {
868 self.selected_search_result += 1;
869 if self.selected_search_result >= self.search_scroll_offset + self.search_max_visible {
871 self.search_scroll_offset =
872 self.selected_search_result - self.search_max_visible + 1;
873 }
874 }
875 }
876
877 pub fn search_scroll_up(&mut self, delta: usize) -> bool {
879 if self.search_results.is_empty() || self.search_scroll_offset == 0 {
880 return false;
881 }
882 self.search_scroll_offset = self.search_scroll_offset.saturating_sub(delta);
883 if self.selected_search_result >= self.search_scroll_offset + self.search_max_visible {
885 self.selected_search_result = self.search_scroll_offset + self.search_max_visible - 1;
886 }
887 true
888 }
889
890 pub fn search_scroll_down(&mut self, delta: usize) -> bool {
892 if self.search_results.is_empty() {
893 return false;
894 }
895 let max_offset = self
896 .search_results
897 .len()
898 .saturating_sub(self.search_max_visible);
899 if self.search_scroll_offset >= max_offset {
900 return false;
901 }
902 self.search_scroll_offset = (self.search_scroll_offset + delta).min(max_offset);
903 if self.selected_search_result < self.search_scroll_offset {
905 self.selected_search_result = self.search_scroll_offset;
906 }
907 true
908 }
909
910 pub fn search_scroll_to_ratio(&mut self, ratio: f32) -> bool {
912 if self.search_results.is_empty() {
913 return false;
914 }
915 let max_offset = self
916 .search_results
917 .len()
918 .saturating_sub(self.search_max_visible);
919 let new_offset = (ratio * max_offset as f32) as usize;
920 if new_offset != self.search_scroll_offset {
921 self.search_scroll_offset = new_offset.min(max_offset);
922 if self.selected_search_result < self.search_scroll_offset {
924 self.selected_search_result = self.search_scroll_offset;
925 } else if self.selected_search_result
926 >= self.search_scroll_offset + self.search_max_visible
927 {
928 self.selected_search_result =
929 self.search_scroll_offset + self.search_max_visible - 1;
930 }
931 return true;
932 }
933 false
934 }
935
936 pub fn jump_to_search_result(&mut self) {
938 let Some(result) = self
940 .search_results
941 .get(self.selected_search_result)
942 .cloned()
943 else {
944 return;
945 };
946 let page_index = result.page_index;
947 let item_index = result.item_index;
948
949 self.update_control_focus(false);
951 self.selected_category = page_index;
952 self.selected_item = item_index;
953 self.focus.set(FocusPanel::Settings);
954 self.scroll_panel.scroll.offset = 0;
956 self.update_layout_widths();
958 if let Some(page) = self.pages.get(self.selected_category) {
959 self.scroll_panel.update_content_height(&page.items);
960 }
961 self.sub_focus = None;
962 self.init_map_focus(true);
963
964 if let Some(ref deep_match) = result.deep_match {
966 self.jump_to_deep_match(deep_match);
967 }
968
969 self.update_control_focus(true); self.ensure_visible();
971 self.cancel_search();
972 }
973
974 fn jump_to_deep_match(&mut self, deep_match: &DeepMatch) {
976 match deep_match {
977 DeepMatch::MapKey { entry_index, .. } | DeepMatch::MapValue { entry_index, .. } => {
978 if let Some(item) = self.current_item_mut() {
979 if let SettingControl::Map(ref mut map_state) = item.control {
980 map_state.focused_entry = Some(*entry_index);
981 }
982 }
983 self.update_map_sub_focus();
984 }
985 DeepMatch::TextListItem { item_index, .. } => {
986 if let Some(item) = self.current_item_mut() {
987 if let SettingControl::TextList(ref mut list_state) = item.control {
988 list_state.focused_item = Some(*item_index);
989 }
990 }
991 self.sub_focus = Some(1 + *item_index);
993 }
994 }
995 }
996
997 pub fn current_search_result(&self) -> Option<&SearchResult> {
999 self.search_results.get(self.selected_search_result)
1000 }
1001
1002 pub fn show_confirm_dialog(&mut self) {
1004 self.showing_confirm_dialog = true;
1005 self.confirm_dialog_selection = 0; }
1007
1008 pub fn hide_confirm_dialog(&mut self) {
1010 self.showing_confirm_dialog = false;
1011 self.confirm_dialog_selection = 0;
1012 }
1013
1014 pub fn confirm_dialog_next(&mut self) {
1016 self.confirm_dialog_selection = (self.confirm_dialog_selection + 1) % 3;
1017 }
1018
1019 pub fn confirm_dialog_prev(&mut self) {
1021 self.confirm_dialog_selection = if self.confirm_dialog_selection == 0 {
1022 2
1023 } else {
1024 self.confirm_dialog_selection - 1
1025 };
1026 }
1027
1028 pub fn toggle_help(&mut self) {
1030 self.showing_help = !self.showing_help;
1031 }
1032
1033 pub fn hide_help(&mut self) {
1035 self.showing_help = false;
1036 }
1037
1038 pub fn showing_entry_dialog(&self) -> bool {
1040 self.has_entry_dialog()
1041 }
1042
1043 pub fn open_entry_dialog(&mut self) {
1045 let Some(item) = self.current_item() else {
1046 return;
1047 };
1048
1049 let path = item.path.as_str();
1051 let SettingControl::Map(map_state) = &item.control else {
1052 return;
1053 };
1054
1055 let Some(entry_idx) = map_state.focused_entry else {
1057 return;
1058 };
1059 let Some((key, value)) = map_state.entries.get(entry_idx) else {
1060 return;
1061 };
1062
1063 let Some(schema) = map_state.value_schema.as_ref() else {
1065 return; };
1067
1068 let no_delete = map_state.no_add;
1070
1071 let dialog =
1073 EntryDialogState::from_schema(key.clone(), value, schema, path, false, no_delete);
1074 self.entry_dialog_stack.push(dialog);
1075 }
1076
1077 pub fn open_add_entry_dialog(&mut self) {
1079 let Some(item) = self.current_item() else {
1080 return;
1081 };
1082 let SettingControl::Map(map_state) = &item.control else {
1083 return;
1084 };
1085 let Some(schema) = map_state.value_schema.as_ref() else {
1086 return;
1087 };
1088 let path = item.path.clone();
1089
1090 let dialog = EntryDialogState::from_schema(
1093 String::new(),
1094 &serde_json::json!({}),
1095 schema,
1096 &path,
1097 true,
1098 false,
1099 );
1100 self.entry_dialog_stack.push(dialog);
1101 }
1102
1103 pub fn open_add_array_item_dialog(&mut self) {
1105 let Some(item) = self.current_item() else {
1106 return;
1107 };
1108 let SettingControl::ObjectArray(array_state) = &item.control else {
1109 return;
1110 };
1111 let Some(schema) = array_state.item_schema.as_ref() else {
1112 return;
1113 };
1114 let path = item.path.clone();
1115
1116 let dialog =
1118 EntryDialogState::for_array_item(None, &serde_json::json!({}), schema, &path, true);
1119 self.entry_dialog_stack.push(dialog);
1120 }
1121
1122 pub fn open_edit_array_item_dialog(&mut self) {
1124 let Some(item) = self.current_item() else {
1125 return;
1126 };
1127 let SettingControl::ObjectArray(array_state) = &item.control else {
1128 return;
1129 };
1130 let Some(schema) = array_state.item_schema.as_ref() else {
1131 return;
1132 };
1133 let Some(index) = array_state.focused_index else {
1134 return;
1135 };
1136 let Some(value) = array_state.bindings.get(index) else {
1137 return;
1138 };
1139 let path = item.path.clone();
1140
1141 let dialog = EntryDialogState::for_array_item(Some(index), value, schema, &path, false);
1142 self.entry_dialog_stack.push(dialog);
1143 }
1144
1145 pub fn close_entry_dialog(&mut self) {
1147 self.entry_dialog_stack.pop();
1148 }
1149
1150 pub fn open_nested_entry_dialog(&mut self) {
1155 let nested_info = self.entry_dialog().and_then(|dialog| {
1157 let item = dialog.current_item()?;
1158 let base = dialog.entry_path();
1164 let relative = item.path.trim_start_matches('/');
1165 let path = if relative.is_empty() {
1166 base
1170 } else {
1171 format!("{}/{}", base, relative)
1172 };
1173
1174 match &item.control {
1175 SettingControl::Map(map_state) => {
1176 let schema = map_state.value_schema.as_ref()?;
1177 let no_delete = map_state.no_add; if let Some(entry_idx) = map_state.focused_entry {
1179 let (key, value) = map_state.entries.get(entry_idx)?;
1181 Some(NestedDialogInfo::MapEntry {
1182 key: key.clone(),
1183 value: value.clone(),
1184 schema: schema.as_ref().clone(),
1185 path,
1186 is_new: false,
1187 no_delete,
1188 })
1189 } else {
1190 Some(NestedDialogInfo::MapEntry {
1192 key: String::new(),
1193 value: serde_json::json!({}),
1194 schema: schema.as_ref().clone(),
1195 path,
1196 is_new: true,
1197 no_delete: false, })
1199 }
1200 }
1201 SettingControl::ObjectArray(array_state) => {
1202 let schema = array_state.item_schema.as_ref()?;
1203 if let Some(index) = array_state.focused_index {
1204 let value = array_state.bindings.get(index)?;
1206 Some(NestedDialogInfo::ArrayItem {
1207 index: Some(index),
1208 value: value.clone(),
1209 schema: schema.as_ref().clone(),
1210 path,
1211 is_new: false,
1212 })
1213 } else {
1214 Some(NestedDialogInfo::ArrayItem {
1216 index: None,
1217 value: serde_json::json!({}),
1218 schema: schema.as_ref().clone(),
1219 path,
1220 is_new: true,
1221 })
1222 }
1223 }
1224 _ => None,
1225 }
1226 });
1227
1228 if let Some(info) = nested_info {
1230 let dialog = match info {
1231 NestedDialogInfo::MapEntry {
1232 key,
1233 value,
1234 schema,
1235 path,
1236 is_new,
1237 no_delete,
1238 } => EntryDialogState::from_schema(key, &value, &schema, &path, is_new, no_delete),
1239 NestedDialogInfo::ArrayItem {
1240 index,
1241 value,
1242 schema,
1243 path,
1244 is_new,
1245 } => EntryDialogState::for_array_item(index, &value, &schema, &path, is_new),
1246 };
1247 self.entry_dialog_stack.push(dialog);
1248 }
1249 }
1250
1251 pub fn save_entry_dialog(&mut self) {
1256 let is_array = if self.entry_dialog_stack.len() > 1 {
1260 self.entry_dialog_stack
1262 .get(self.entry_dialog_stack.len() - 2)
1263 .and_then(|parent| parent.current_item())
1264 .map(|item| matches!(item.control, SettingControl::ObjectArray(_)))
1265 .unwrap_or(false)
1266 } else {
1267 self.current_item()
1269 .map(|item| matches!(item.control, SettingControl::ObjectArray(_)))
1270 .unwrap_or(false)
1271 };
1272
1273 if is_array {
1274 self.save_array_item_dialog_inner();
1275 } else {
1276 self.save_map_entry_dialog_inner();
1277 }
1278 }
1279
1280 fn save_map_entry_dialog_inner(&mut self) {
1282 let Some(dialog) = self.entry_dialog_stack.pop() else {
1283 return;
1284 };
1285
1286 let key = dialog.get_key();
1288 if key.is_empty() {
1289 return; }
1291
1292 let value = dialog.to_value();
1293 let map_path = dialog.map_path.clone();
1294 let original_key = dialog.entry_key.clone();
1295 let is_new = dialog.is_new;
1296 let key_changed = !is_new && key != original_key;
1297
1298 if let Some(item) = self.current_item_mut() {
1300 if let SettingControl::Map(map_state) = &mut item.control {
1301 if key_changed {
1303 if let Some(idx) = map_state
1304 .entries
1305 .iter()
1306 .position(|(k, _)| k == &original_key)
1307 {
1308 map_state.entries.remove(idx);
1309 }
1310 }
1311
1312 if let Some(entry) = map_state.entries.iter_mut().find(|(k, _)| k == &key) {
1314 entry.1 = value.clone();
1315 } else {
1316 map_state.entries.push((key.clone(), value.clone()));
1317 map_state.entries.sort_by(|a, b| a.0.cmp(&b.0));
1318 }
1319 }
1320 }
1321
1322 if key_changed {
1324 let old_path = format!("{}/{}", map_path, original_key);
1325 self.pending_changes
1326 .insert(old_path, serde_json::Value::Null);
1327 }
1328
1329 let path = format!("{}/{}", map_path, key);
1331 self.set_pending_change(&path, value);
1332 }
1333
1334 fn save_array_item_dialog_inner(&mut self) {
1336 let Some(dialog) = self.entry_dialog_stack.pop() else {
1337 return;
1338 };
1339
1340 let value = dialog.to_value();
1341 let array_path = dialog.map_path.clone();
1342 let is_new = dialog.is_new;
1343 let entry_key = dialog.entry_key.clone();
1344
1345 let is_nested = !self.entry_dialog_stack.is_empty();
1347
1348 if is_nested {
1349 let parent_entry_path = self
1357 .entry_dialog_stack
1358 .last()
1359 .map(|p| p.entry_path())
1360 .unwrap_or_default();
1361 let item_path = array_path
1362 .strip_prefix(parent_entry_path.as_str())
1363 .unwrap_or(&array_path)
1364 .trim_end_matches('/')
1365 .to_string();
1366
1367 if let Some(parent) = self.entry_dialog_stack.last_mut() {
1369 if let Some(item) = parent.items.iter_mut().find(|i| i.path == item_path) {
1370 if let SettingControl::ObjectArray(array_state) = &mut item.control {
1371 if is_new {
1372 array_state.bindings.push(value.clone());
1373 } else if let Ok(index) = entry_key.parse::<usize>() {
1374 if index < array_state.bindings.len() {
1375 array_state.bindings[index] = value.clone();
1376 }
1377 }
1378 }
1379 }
1380 }
1381
1382 if let Some(parent) = self.entry_dialog_stack.last() {
1385 if let Some(item) = parent.items.iter().find(|i| i.path == item_path) {
1386 if let SettingControl::ObjectArray(array_state) = &item.control {
1387 let array_value = serde_json::Value::Array(array_state.bindings.clone());
1388 self.set_pending_change(&array_path, array_value);
1389 }
1390 }
1391 }
1392 } else {
1393 if let Some(item) = self.current_item_mut() {
1395 if let SettingControl::ObjectArray(array_state) = &mut item.control {
1396 if is_new {
1397 array_state.bindings.push(value.clone());
1398 } else if let Ok(index) = entry_key.parse::<usize>() {
1399 if index < array_state.bindings.len() {
1400 array_state.bindings[index] = value.clone();
1401 }
1402 }
1403 }
1404 }
1405
1406 if let Some(item) = self.current_item() {
1408 if let SettingControl::ObjectArray(array_state) = &item.control {
1409 let array_value = serde_json::Value::Array(array_state.bindings.clone());
1410 self.set_pending_change(&array_path, array_value);
1411 }
1412 }
1413 }
1414 }
1415
1416 pub fn delete_entry_dialog(&mut self) {
1418 let is_nested = self.entry_dialog_stack.len() > 1;
1420
1421 let Some(dialog) = self.entry_dialog_stack.pop() else {
1422 return;
1423 };
1424
1425 let path = format!("{}/{}", dialog.map_path, dialog.entry_key);
1426
1427 if is_nested {
1429 let map_field = dialog.map_path.rsplit('/').next().unwrap_or("").to_string();
1432 let item_path = format!("/{}", map_field);
1433
1434 if let Some(parent) = self.entry_dialog_stack.last_mut() {
1436 if let Some(item) = parent.items.iter_mut().find(|i| i.path == item_path) {
1437 if let SettingControl::Map(map_state) = &mut item.control {
1438 if let Some(idx) = map_state
1439 .entries
1440 .iter()
1441 .position(|(k, _)| k == &dialog.entry_key)
1442 {
1443 map_state.remove_entry(idx);
1444 }
1445 }
1446 }
1447 }
1448 } else {
1449 if let Some(item) = self.current_item_mut() {
1451 if let SettingControl::Map(map_state) = &mut item.control {
1452 if let Some(idx) = map_state
1453 .entries
1454 .iter()
1455 .position(|(k, _)| k == &dialog.entry_key)
1456 {
1457 map_state.remove_entry(idx);
1458 }
1459 }
1460 }
1461 }
1462
1463 self.set_pending_change(&path, serde_json::Value::Null);
1465 }
1466
1467 pub fn max_scroll(&self) -> u16 {
1469 self.scroll_panel.scroll.max_offset()
1470 }
1471
1472 pub fn scroll_up(&mut self, delta: usize) -> bool {
1475 let old = self.scroll_panel.scroll.offset;
1476 self.scroll_panel.scroll_up(delta as u16);
1477 old != self.scroll_panel.scroll.offset
1478 }
1479
1480 pub fn scroll_down(&mut self, delta: usize) -> bool {
1483 let old = self.scroll_panel.scroll.offset;
1484 self.scroll_panel.scroll_down(delta as u16);
1485 old != self.scroll_panel.scroll.offset
1486 }
1487
1488 pub fn scroll_to_ratio(&mut self, ratio: f32) -> bool {
1491 let old = self.scroll_panel.scroll.offset;
1492 self.scroll_panel.scroll_to_ratio(ratio);
1493 old != self.scroll_panel.scroll.offset
1494 }
1495
1496 pub fn is_number_control(&self) -> bool {
1499 self.current_item()
1500 .is_some_and(|item| matches!(item.control, SettingControl::Number(_)))
1501 }
1502
1503 pub fn start_editing(&mut self) {
1504 if let Some(item) = self.current_item() {
1505 if matches!(
1506 item.control,
1507 SettingControl::TextList(_)
1508 | SettingControl::DualList(_)
1509 | SettingControl::Text(_)
1510 | SettingControl::Map(_)
1511 | SettingControl::Json(_)
1512 ) {
1513 self.editing_text = true;
1514 }
1515 }
1516 if let Some(item) = self.current_item_mut() {
1517 match item.control {
1518 SettingControl::DualList(ref mut dl) => {
1519 dl.editing = true;
1520 }
1521 SettingControl::Text(ref mut state) => {
1522 state.editing = true;
1523 state.arm_replace_on_type();
1528 }
1529 _ => {}
1530 }
1531 }
1532 }
1533
1534 pub fn stop_editing(&mut self) {
1536 self.editing_text = false;
1537 if let Some(item) = self.current_item_mut() {
1538 match item.control {
1539 SettingControl::DualList(ref mut dl) => {
1540 dl.editing = false;
1541 }
1542 SettingControl::Text(ref mut state) => {
1543 state.editing = false;
1544 }
1545 _ => {}
1546 }
1547 }
1548 }
1549
1550 pub fn is_editable_control(&self) -> bool {
1552 self.current_item().is_some_and(|item| {
1553 matches!(
1554 item.control,
1555 SettingControl::TextList(_)
1556 | SettingControl::DualList(_)
1557 | SettingControl::Text(_)
1558 | SettingControl::Map(_)
1559 | SettingControl::Json(_)
1560 )
1561 })
1562 }
1563
1564 pub fn is_editing_json(&self) -> bool {
1566 if !self.editing_text {
1567 return false;
1568 }
1569 self.current_item()
1570 .map(|item| matches!(&item.control, SettingControl::Json(_)))
1571 .unwrap_or(false)
1572 }
1573
1574 pub fn text_insert(&mut self, c: char) {
1576 if let Some(item) = self.current_item_mut() {
1577 match &mut item.control {
1578 SettingControl::TextList(state) => state.insert(c),
1579 SettingControl::Text(state) => state.insert(c),
1580 SettingControl::Map(state) => {
1581 state.new_key_text.insert(state.cursor, c);
1582 state.cursor += c.len_utf8();
1583 }
1584 SettingControl::Json(state) => state.insert(c),
1585 _ => {}
1586 }
1587 }
1588 }
1589
1590 pub fn text_backspace(&mut self) {
1592 if let Some(item) = self.current_item_mut() {
1593 match &mut item.control {
1594 SettingControl::TextList(state) => state.backspace(),
1595 SettingControl::Text(state) => state.backspace(),
1596 SettingControl::Map(state) => {
1597 if state.cursor > 0 {
1598 let mut char_start = state.cursor - 1;
1599 while char_start > 0 && !state.new_key_text.is_char_boundary(char_start) {
1600 char_start -= 1;
1601 }
1602 state.new_key_text.remove(char_start);
1603 state.cursor = char_start;
1604 }
1605 }
1606 SettingControl::Json(state) => state.backspace(),
1607 _ => {}
1608 }
1609 }
1610 }
1611
1612 pub fn text_move_left(&mut self) {
1614 if let Some(item) = self.current_item_mut() {
1615 match &mut item.control {
1616 SettingControl::TextList(state) => state.move_left(),
1617 SettingControl::Text(state) => state.move_left(),
1618 SettingControl::Map(state) => {
1619 if state.cursor > 0 {
1620 let mut new_pos = state.cursor - 1;
1621 while new_pos > 0 && !state.new_key_text.is_char_boundary(new_pos) {
1622 new_pos -= 1;
1623 }
1624 state.cursor = new_pos;
1625 }
1626 }
1627 SettingControl::Json(state) => state.move_left(),
1628 _ => {}
1629 }
1630 }
1631 }
1632
1633 pub fn text_move_right(&mut self) {
1635 if let Some(item) = self.current_item_mut() {
1636 match &mut item.control {
1637 SettingControl::TextList(state) => state.move_right(),
1638 SettingControl::Text(state) => state.move_right(),
1639 SettingControl::Map(state) => {
1640 if state.cursor < state.new_key_text.len() {
1641 let mut new_pos = state.cursor + 1;
1642 while new_pos < state.new_key_text.len()
1643 && !state.new_key_text.is_char_boundary(new_pos)
1644 {
1645 new_pos += 1;
1646 }
1647 state.cursor = new_pos;
1648 }
1649 }
1650 SettingControl::Json(state) => state.move_right(),
1651 _ => {}
1652 }
1653 }
1654 }
1655
1656 pub fn text_focus_prev(&mut self) {
1658 if let Some(item) = self.current_item_mut() {
1659 match &mut item.control {
1660 SettingControl::TextList(state) => state.focus_prev(),
1661 SettingControl::Map(state) => {
1662 state.focus_prev();
1663 }
1664 _ => {}
1665 }
1666 }
1667 }
1668
1669 pub fn text_focus_next(&mut self) {
1671 if let Some(item) = self.current_item_mut() {
1672 match &mut item.control {
1673 SettingControl::TextList(state) => state.focus_next(),
1674 SettingControl::Map(state) => {
1675 state.focus_next();
1676 }
1677 _ => {}
1678 }
1679 }
1680 }
1681
1682 pub fn text_add_item(&mut self) {
1684 if let Some(item) = self.current_item_mut() {
1685 match &mut item.control {
1686 SettingControl::TextList(state) => state.add_item(),
1687 SettingControl::Map(state) => state.add_entry_from_input(),
1688 _ => {}
1689 }
1690 }
1691 self.on_value_changed();
1693 }
1694
1695 pub fn text_remove_focused(&mut self) {
1697 if let Some(item) = self.current_item_mut() {
1698 match &mut item.control {
1699 SettingControl::TextList(state) => {
1700 if let Some(idx) = state.focused_item {
1701 state.remove_item(idx);
1702 }
1703 }
1704 SettingControl::Map(state) => {
1705 if let Some(idx) = state.focused_entry {
1706 state.remove_entry(idx);
1707 }
1708 }
1709 _ => {}
1710 }
1711 }
1712 self.on_value_changed();
1714 }
1715
1716 pub fn is_editing_dual_list(&self) -> bool {
1718 if !self.editing_text {
1719 return false;
1720 }
1721 self.current_item()
1722 .map(|item| matches!(&item.control, SettingControl::DualList(_)))
1723 .unwrap_or(false)
1724 }
1725
1726 pub fn with_dual_list_mut<R>(
1731 &mut self,
1732 item_idx: usize,
1733 f: impl FnOnce(&mut crate::view::controls::DualListState) -> R,
1734 ) -> Option<R> {
1735 let page = self.pages.get_mut(self.selected_category)?;
1736 let item = page.items.get_mut(item_idx)?;
1737 if let SettingControl::DualList(ref mut state) = item.control {
1738 Some(f(state))
1739 } else {
1740 None
1741 }
1742 }
1743
1744 pub fn with_current_dual_list_mut<R>(
1747 &mut self,
1748 f: impl FnOnce(&mut crate::view::controls::DualListState) -> R,
1749 ) -> Option<R> {
1750 if let Some(item) = self.current_item_mut() {
1751 if let SettingControl::DualList(ref mut state) = item.control {
1752 return Some(f(state));
1753 }
1754 }
1755 None
1756 }
1757
1758 pub fn refresh_dual_list_sibling(&mut self) {
1765 let (new_included, sibling_path) = {
1766 let Some(item) = self.current_item() else {
1767 return;
1768 };
1769 let SettingControl::DualList(state) = &item.control else {
1770 return;
1771 };
1772 let Some(ref sib_path) = item.dual_list_sibling else {
1773 return;
1774 };
1775 (state.included.clone(), sib_path.clone())
1776 };
1777
1778 if let Some(page) = self.pages.get_mut(self.selected_category) {
1780 for other in page.items.iter_mut() {
1781 if other.path == sibling_path {
1782 if let SettingControl::DualList(ref mut sib_state) = other.control {
1783 sib_state.excluded = new_included;
1784 }
1785 break;
1786 }
1787 }
1788 }
1789 }
1790
1791 pub fn json_cursor_up(&mut self) {
1795 if let Some(item) = self.current_item_mut() {
1796 if let SettingControl::Json(state) = &mut item.control {
1797 state.move_up();
1798 }
1799 }
1800 }
1801
1802 pub fn json_cursor_down(&mut self) {
1804 if let Some(item) = self.current_item_mut() {
1805 if let SettingControl::Json(state) = &mut item.control {
1806 state.move_down();
1807 }
1808 }
1809 }
1810
1811 pub fn json_insert_newline(&mut self) {
1813 if let Some(item) = self.current_item_mut() {
1814 if let SettingControl::Json(state) = &mut item.control {
1815 state.insert('\n');
1816 }
1817 }
1818 }
1819
1820 pub fn json_delete(&mut self) {
1822 if let Some(item) = self.current_item_mut() {
1823 if let SettingControl::Json(state) = &mut item.control {
1824 state.delete();
1825 }
1826 }
1827 }
1828
1829 pub fn json_exit_editing(&mut self) {
1831 let is_valid = self
1832 .current_item()
1833 .map(|item| {
1834 if let SettingControl::Json(state) = &item.control {
1835 state.is_valid()
1836 } else {
1837 true
1838 }
1839 })
1840 .unwrap_or(true);
1841
1842 if is_valid {
1843 if let Some(item) = self.current_item_mut() {
1844 if let SettingControl::Json(state) = &mut item.control {
1845 state.commit();
1846 }
1847 }
1848 self.on_value_changed();
1849 } else if let Some(item) = self.current_item_mut() {
1850 if let SettingControl::Json(state) = &mut item.control {
1851 state.revert();
1852 }
1853 }
1854 self.editing_text = false;
1855 }
1856
1857 pub fn json_select_all(&mut self) {
1859 if let Some(item) = self.current_item_mut() {
1860 if let SettingControl::Json(state) = &mut item.control {
1861 state.select_all();
1862 }
1863 }
1864 }
1865
1866 pub fn json_selected_text(&self) -> Option<String> {
1868 if let Some(item) = self.current_item() {
1869 if let SettingControl::Json(state) = &item.control {
1870 return state.selected_text();
1871 }
1872 }
1873 None
1874 }
1875
1876 pub fn json_cursor_up_selecting(&mut self) {
1878 if let Some(item) = self.current_item_mut() {
1879 if let SettingControl::Json(state) = &mut item.control {
1880 state.editor.move_up_selecting();
1881 }
1882 }
1883 }
1884
1885 pub fn json_cursor_down_selecting(&mut self) {
1887 if let Some(item) = self.current_item_mut() {
1888 if let SettingControl::Json(state) = &mut item.control {
1889 state.editor.move_down_selecting();
1890 }
1891 }
1892 }
1893
1894 pub fn json_cursor_left_selecting(&mut self) {
1896 if let Some(item) = self.current_item_mut() {
1897 if let SettingControl::Json(state) = &mut item.control {
1898 state.editor.move_left_selecting();
1899 }
1900 }
1901 }
1902
1903 pub fn json_cursor_right_selecting(&mut self) {
1905 if let Some(item) = self.current_item_mut() {
1906 if let SettingControl::Json(state) = &mut item.control {
1907 state.editor.move_right_selecting();
1908 }
1909 }
1910 }
1911
1912 pub fn is_dropdown_open(&self) -> bool {
1916 self.current_item().is_some_and(|item| {
1917 if let SettingControl::Dropdown(ref d) = item.control {
1918 d.open
1919 } else {
1920 false
1921 }
1922 })
1923 }
1924
1925 pub fn dropdown_toggle(&mut self) {
1927 let mut opened = false;
1928 if let Some(item) = self.current_item_mut() {
1929 if let SettingControl::Dropdown(ref mut d) = item.control {
1930 d.toggle_open();
1931 opened = d.open;
1932 }
1933 }
1934
1935 if opened {
1937 self.update_layout_widths();
1939 let selected_item = self.selected_item;
1940 if let Some(page) = self.pages.get(self.selected_category) {
1941 self.scroll_panel.update_content_height(&page.items);
1942 self.scroll_panel
1944 .ensure_focused_visible(&page.items, selected_item, None);
1945 }
1946 }
1947 }
1948
1949 pub fn dropdown_prev(&mut self) {
1951 if let Some(item) = self.current_item_mut() {
1952 if let SettingControl::Dropdown(ref mut d) = item.control {
1953 d.select_prev();
1954 }
1955 }
1956 }
1957
1958 pub fn dropdown_next(&mut self) {
1960 if let Some(item) = self.current_item_mut() {
1961 if let SettingControl::Dropdown(ref mut d) = item.control {
1962 d.select_next();
1963 }
1964 }
1965 }
1966
1967 pub fn dropdown_home(&mut self) {
1969 if let Some(item) = self.current_item_mut() {
1970 if let SettingControl::Dropdown(ref mut d) = item.control {
1971 if !d.options.is_empty() {
1972 d.selected = 0;
1973 d.ensure_visible();
1974 }
1975 }
1976 }
1977 }
1978
1979 pub fn dropdown_end(&mut self) {
1981 if let Some(item) = self.current_item_mut() {
1982 if let SettingControl::Dropdown(ref mut d) = item.control {
1983 if !d.options.is_empty() {
1984 d.selected = d.options.len() - 1;
1985 d.ensure_visible();
1986 }
1987 }
1988 }
1989 }
1990
1991 pub fn dropdown_confirm(&mut self) {
1993 if let Some(item) = self.current_item_mut() {
1994 if let SettingControl::Dropdown(ref mut d) = item.control {
1995 d.confirm();
1996 }
1997 }
1998 self.on_value_changed();
1999 }
2000
2001 pub fn dropdown_cancel(&mut self) {
2003 if let Some(item) = self.current_item_mut() {
2004 if let SettingControl::Dropdown(ref mut d) = item.control {
2005 d.cancel();
2006 }
2007 }
2008 }
2009
2010 pub fn dropdown_select(&mut self, option_idx: usize) {
2012 if let Some(item) = self.current_item_mut() {
2013 if let SettingControl::Dropdown(ref mut d) = item.control {
2014 if option_idx < d.options.len() {
2015 d.selected = option_idx;
2016 d.confirm();
2017 }
2018 }
2019 }
2020 self.on_value_changed();
2021 }
2022
2023 pub fn set_dropdown_hover(&mut self, hover_idx: Option<usize>) -> bool {
2026 if let Some(item) = self.current_item_mut() {
2027 if let SettingControl::Dropdown(ref mut d) = item.control {
2028 if d.open && d.hover_index != hover_idx {
2029 d.hover_index = hover_idx;
2030 return true;
2031 }
2032 }
2033 }
2034 false
2035 }
2036
2037 pub fn dropdown_scroll(&mut self, delta: i32) {
2039 if let Some(item) = self.current_item_mut() {
2040 if let SettingControl::Dropdown(ref mut d) = item.control {
2041 if d.open {
2042 d.scroll_by(delta);
2043 }
2044 }
2045 }
2046 }
2047
2048 pub fn is_number_editing(&self) -> bool {
2052 self.current_item().is_some_and(|item| {
2053 if let SettingControl::Number(ref n) = item.control {
2054 n.editing()
2055 } else {
2056 false
2057 }
2058 })
2059 }
2060
2061 pub fn start_number_editing(&mut self) {
2063 if let Some(item) = self.current_item_mut() {
2064 if let SettingControl::Number(ref mut n) = item.control {
2065 n.start_editing();
2066 }
2067 }
2068 }
2069
2070 pub fn number_insert(&mut self, c: char) {
2072 if let Some(item) = self.current_item_mut() {
2073 if let SettingControl::Number(ref mut n) = item.control {
2074 n.insert_char(c);
2075 }
2076 }
2077 }
2078
2079 pub fn number_backspace(&mut self) {
2081 if let Some(item) = self.current_item_mut() {
2082 if let SettingControl::Number(ref mut n) = item.control {
2083 n.backspace();
2084 }
2085 }
2086 }
2087
2088 pub fn number_confirm(&mut self) {
2090 if let Some(item) = self.current_item_mut() {
2091 if let SettingControl::Number(ref mut n) = item.control {
2092 n.confirm_editing();
2093 }
2094 }
2095 self.on_value_changed();
2096 }
2097
2098 pub fn number_cancel(&mut self) {
2100 if let Some(item) = self.current_item_mut() {
2101 if let SettingControl::Number(ref mut n) = item.control {
2102 n.cancel_editing();
2103 }
2104 }
2105 }
2106
2107 pub fn number_delete(&mut self) {
2109 if let Some(item) = self.current_item_mut() {
2110 if let SettingControl::Number(ref mut n) = item.control {
2111 n.delete();
2112 }
2113 }
2114 }
2115
2116 pub fn number_move_left(&mut self) {
2118 if let Some(item) = self.current_item_mut() {
2119 if let SettingControl::Number(ref mut n) = item.control {
2120 n.move_left();
2121 }
2122 }
2123 }
2124
2125 pub fn number_move_right(&mut self) {
2127 if let Some(item) = self.current_item_mut() {
2128 if let SettingControl::Number(ref mut n) = item.control {
2129 n.move_right();
2130 }
2131 }
2132 }
2133
2134 pub fn number_move_home(&mut self) {
2136 if let Some(item) = self.current_item_mut() {
2137 if let SettingControl::Number(ref mut n) = item.control {
2138 n.move_home();
2139 }
2140 }
2141 }
2142
2143 pub fn number_move_end(&mut self) {
2145 if let Some(item) = self.current_item_mut() {
2146 if let SettingControl::Number(ref mut n) = item.control {
2147 n.move_end();
2148 }
2149 }
2150 }
2151
2152 pub fn number_move_left_selecting(&mut self) {
2154 if let Some(item) = self.current_item_mut() {
2155 if let SettingControl::Number(ref mut n) = item.control {
2156 n.move_left_selecting();
2157 }
2158 }
2159 }
2160
2161 pub fn number_move_right_selecting(&mut self) {
2163 if let Some(item) = self.current_item_mut() {
2164 if let SettingControl::Number(ref mut n) = item.control {
2165 n.move_right_selecting();
2166 }
2167 }
2168 }
2169
2170 pub fn number_move_home_selecting(&mut self) {
2172 if let Some(item) = self.current_item_mut() {
2173 if let SettingControl::Number(ref mut n) = item.control {
2174 n.move_home_selecting();
2175 }
2176 }
2177 }
2178
2179 pub fn number_move_end_selecting(&mut self) {
2181 if let Some(item) = self.current_item_mut() {
2182 if let SettingControl::Number(ref mut n) = item.control {
2183 n.move_end_selecting();
2184 }
2185 }
2186 }
2187
2188 pub fn number_move_word_left(&mut self) {
2190 if let Some(item) = self.current_item_mut() {
2191 if let SettingControl::Number(ref mut n) = item.control {
2192 n.move_word_left();
2193 }
2194 }
2195 }
2196
2197 pub fn number_move_word_right(&mut self) {
2199 if let Some(item) = self.current_item_mut() {
2200 if let SettingControl::Number(ref mut n) = item.control {
2201 n.move_word_right();
2202 }
2203 }
2204 }
2205
2206 pub fn number_move_word_left_selecting(&mut self) {
2208 if let Some(item) = self.current_item_mut() {
2209 if let SettingControl::Number(ref mut n) = item.control {
2210 n.move_word_left_selecting();
2211 }
2212 }
2213 }
2214
2215 pub fn number_move_word_right_selecting(&mut self) {
2217 if let Some(item) = self.current_item_mut() {
2218 if let SettingControl::Number(ref mut n) = item.control {
2219 n.move_word_right_selecting();
2220 }
2221 }
2222 }
2223
2224 pub fn number_select_all(&mut self) {
2226 if let Some(item) = self.current_item_mut() {
2227 if let SettingControl::Number(ref mut n) = item.control {
2228 n.select_all();
2229 }
2230 }
2231 }
2232
2233 pub fn number_delete_word_backward(&mut self) {
2235 if let Some(item) = self.current_item_mut() {
2236 if let SettingControl::Number(ref mut n) = item.control {
2237 n.delete_word_backward();
2238 }
2239 }
2240 }
2241
2242 pub fn number_delete_word_forward(&mut self) {
2244 if let Some(item) = self.current_item_mut() {
2245 if let SettingControl::Number(ref mut n) = item.control {
2246 n.delete_word_forward();
2247 }
2248 }
2249 }
2250
2251 pub fn get_change_descriptions(&self) -> Vec<String> {
2253 let mut descriptions: Vec<String> = self
2254 .pending_changes
2255 .iter()
2256 .map(|(path, value)| {
2257 let value_str = match value {
2258 serde_json::Value::Bool(b) => b.to_string(),
2259 serde_json::Value::Number(n) => n.to_string(),
2260 serde_json::Value::String(s) => format!("\"{}\"", s),
2261 _ => value.to_string(),
2262 };
2263 format!("{}: {}", path, value_str)
2264 })
2265 .collect();
2266 for path in &self.pending_deletions {
2268 descriptions.push(format!("{}: (reset to default)", path));
2269 }
2270 descriptions.sort();
2271 descriptions
2272 }
2273}
2274
2275fn update_control_from_value(control: &mut SettingControl, value: &serde_json::Value) {
2277 match control {
2278 SettingControl::Toggle(state) => {
2279 if let Some(b) = value.as_bool() {
2280 state.checked = b;
2281 }
2282 }
2283 SettingControl::Number(state) => {
2284 if let Some(n) = value.as_i64() {
2285 state.value = n;
2286 }
2287 }
2288 SettingControl::Dropdown(state) => {
2289 if let Some(s) = value.as_str() {
2290 if let Some(idx) = state.options.iter().position(|o| o == s) {
2291 state.selected = idx;
2292 }
2293 }
2294 }
2295 SettingControl::Text(state) => {
2296 if let Some(s) = value.as_str() {
2297 state.value = s.to_string();
2298 state.cursor = state.value.len();
2299 }
2300 }
2301 SettingControl::TextList(state) => {
2302 if let Some(arr) = value.as_array() {
2303 state.items = arr
2304 .iter()
2305 .filter_map(|v| {
2306 if state.is_integer {
2307 v.as_i64()
2308 .map(|n| n.to_string())
2309 .or_else(|| v.as_u64().map(|n| n.to_string()))
2310 .or_else(|| v.as_f64().map(|n| n.to_string()))
2311 } else {
2312 v.as_str().map(String::from)
2313 }
2314 })
2315 .collect();
2316 }
2317 }
2318 SettingControl::DualList(state) => {
2319 if let Some(arr) = value.as_array() {
2320 state.included = arr
2321 .iter()
2322 .filter_map(|v| v.as_str().map(String::from))
2323 .collect();
2324 }
2325 }
2326 SettingControl::Map(state) => {
2327 if let Some(obj) = value.as_object() {
2328 state.entries = obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
2329 state.entries.sort_by(|a, b| a.0.cmp(&b.0));
2330 }
2331 }
2332 SettingControl::ObjectArray(state) => {
2333 if let Some(arr) = value.as_array() {
2334 state.bindings = arr.clone();
2335 }
2336 }
2337 SettingControl::Json(state) => {
2338 let json_str =
2340 serde_json::to_string_pretty(value).unwrap_or_else(|_| "null".to_string());
2341 let json_str = if json_str.is_empty() {
2342 "null".to_string()
2343 } else {
2344 json_str
2345 };
2346 state.original_text = json_str.clone();
2347 state.editor.set_value(&json_str);
2348 state.scroll_offset = 0;
2349 }
2350 SettingControl::Complex { .. } => {}
2351 }
2352}
2353
2354#[cfg(test)]
2355mod tests {
2356 use super::*;
2357
2358 const TEST_SCHEMA: &str = r#"
2359{
2360 "type": "object",
2361 "properties": {
2362 "theme": {
2363 "type": "string",
2364 "default": "dark"
2365 },
2366 "line_numbers": {
2367 "type": "boolean",
2368 "default": true
2369 }
2370 },
2371 "$defs": {}
2372}
2373"#;
2374
2375 fn test_config() -> Config {
2376 Config::default()
2377 }
2378
2379 #[test]
2380 fn test_settings_state_creation() {
2381 let config = test_config();
2382 let state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2383
2384 assert!(!state.visible);
2385 assert_eq!(state.selected_category, 0);
2386 assert!(!state.has_changes());
2387 }
2388
2389 #[test]
2390 fn test_navigation() {
2391 let config = test_config();
2392 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2393
2394 assert_eq!(state.focus_panel(), FocusPanel::Categories);
2396
2397 state.toggle_focus();
2399 assert_eq!(state.focus_panel(), FocusPanel::Settings);
2400
2401 state.select_next();
2403 assert_eq!(state.selected_item, 1);
2404
2405 state.select_prev();
2406 assert_eq!(state.selected_item, 0);
2407 }
2408
2409 #[test]
2410 fn test_pending_changes() {
2411 let config = test_config();
2412 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2413
2414 assert!(!state.has_changes());
2415
2416 state.set_pending_change("/theme", serde_json::Value::String("light".to_string()));
2417 assert!(state.has_changes());
2418
2419 state.discard_changes();
2420 assert!(!state.has_changes());
2421 }
2422
2423 #[test]
2424 fn test_show_hide() {
2425 let config = test_config();
2426 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2427
2428 assert!(!state.visible);
2429
2430 state.show();
2431 assert!(state.visible);
2432 assert_eq!(state.focus_panel(), FocusPanel::Categories);
2433
2434 state.hide();
2435 assert!(!state.visible);
2436 }
2437
2438 const TEST_SCHEMA_CONTROLS: &str = r#"
2440{
2441 "type": "object",
2442 "properties": {
2443 "theme": {
2444 "type": "string",
2445 "enum": ["dark", "light", "high-contrast"],
2446 "default": "dark"
2447 },
2448 "tab_size": {
2449 "type": "integer",
2450 "minimum": 1,
2451 "maximum": 8,
2452 "default": 4
2453 },
2454 "line_numbers": {
2455 "type": "boolean",
2456 "default": true
2457 }
2458 },
2459 "$defs": {}
2460}
2461"#;
2462
2463 #[test]
2464 fn test_dropdown_toggle() {
2465 let config = test_config();
2466 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
2467 state.show();
2468 state.toggle_focus(); state.select_next();
2473 state.select_next();
2474 assert!(!state.is_dropdown_open());
2475
2476 state.dropdown_toggle();
2477 assert!(state.is_dropdown_open());
2478
2479 state.dropdown_toggle();
2480 assert!(!state.is_dropdown_open());
2481 }
2482
2483 #[test]
2484 fn test_dropdown_cancel_restores() {
2485 let config = test_config();
2486 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
2487 state.show();
2488 state.toggle_focus();
2489
2490 state.select_next();
2493 state.select_next();
2494
2495 state.dropdown_toggle();
2497 assert!(state.is_dropdown_open());
2498
2499 let initial = state.current_item().and_then(|item| {
2501 if let SettingControl::Dropdown(ref d) = item.control {
2502 Some(d.selected)
2503 } else {
2504 None
2505 }
2506 });
2507
2508 state.dropdown_next();
2510 let after_change = state.current_item().and_then(|item| {
2511 if let SettingControl::Dropdown(ref d) = item.control {
2512 Some(d.selected)
2513 } else {
2514 None
2515 }
2516 });
2517 assert_ne!(initial, after_change);
2518
2519 state.dropdown_cancel();
2521 assert!(!state.is_dropdown_open());
2522
2523 let after_cancel = state.current_item().and_then(|item| {
2524 if let SettingControl::Dropdown(ref d) = item.control {
2525 Some(d.selected)
2526 } else {
2527 None
2528 }
2529 });
2530 assert_eq!(initial, after_cancel);
2531 }
2532
2533 #[test]
2534 fn test_dropdown_confirm_keeps_selection() {
2535 let config = test_config();
2536 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
2537 state.show();
2538 state.toggle_focus();
2539
2540 state.dropdown_toggle();
2542
2543 state.dropdown_next();
2545 let after_change = state.current_item().and_then(|item| {
2546 if let SettingControl::Dropdown(ref d) = item.control {
2547 Some(d.selected)
2548 } else {
2549 None
2550 }
2551 });
2552
2553 state.dropdown_confirm();
2555 assert!(!state.is_dropdown_open());
2556
2557 let after_confirm = state.current_item().and_then(|item| {
2558 if let SettingControl::Dropdown(ref d) = item.control {
2559 Some(d.selected)
2560 } else {
2561 None
2562 }
2563 });
2564 assert_eq!(after_change, after_confirm);
2565 }
2566
2567 #[test]
2568 fn test_number_editing() {
2569 let config = test_config();
2570 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
2571 state.show();
2572 state.toggle_focus();
2573
2574 state.select_next();
2576
2577 assert!(!state.is_number_editing());
2579
2580 state.start_number_editing();
2582 assert!(state.is_number_editing());
2583
2584 state.number_insert('8');
2586
2587 state.number_confirm();
2589 assert!(!state.is_number_editing());
2590 }
2591
2592 #[test]
2593 fn test_number_cancel_editing() {
2594 let config = test_config();
2595 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
2596 state.show();
2597 state.toggle_focus();
2598
2599 state.select_next();
2601
2602 let initial_value = state.current_item().and_then(|item| {
2604 if let SettingControl::Number(ref n) = item.control {
2605 Some(n.value)
2606 } else {
2607 None
2608 }
2609 });
2610
2611 state.start_number_editing();
2613 state.number_backspace();
2614 state.number_insert('9');
2615 state.number_insert('9');
2616
2617 state.number_cancel();
2619 assert!(!state.is_number_editing());
2620
2621 let after_cancel = state.current_item().and_then(|item| {
2623 if let SettingControl::Number(ref n) = item.control {
2624 Some(n.value)
2625 } else {
2626 None
2627 }
2628 });
2629 assert_eq!(initial_value, after_cancel);
2630 }
2631
2632 #[test]
2633 fn test_number_backspace() {
2634 let config = test_config();
2635 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
2636 state.show();
2637 state.toggle_focus();
2638 state.select_next();
2639
2640 state.start_number_editing();
2641 state.number_backspace();
2642
2643 let display_text = state.current_item().and_then(|item| {
2645 if let SettingControl::Number(ref n) = item.control {
2646 Some(n.display_text())
2647 } else {
2648 None
2649 }
2650 });
2651 assert_eq!(display_text, Some(String::new()));
2653
2654 state.number_cancel();
2655 }
2656
2657 #[test]
2658 fn test_layer_selection() {
2659 let config = test_config();
2660 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2661
2662 assert_eq!(state.target_layer, ConfigLayer::User);
2664 assert_eq!(state.target_layer_name(), "User");
2665
2666 state.cycle_target_layer();
2668 assert_eq!(state.target_layer, ConfigLayer::Project);
2669 assert_eq!(state.target_layer_name(), "Project");
2670
2671 state.cycle_target_layer();
2672 assert_eq!(state.target_layer, ConfigLayer::Session);
2673 assert_eq!(state.target_layer_name(), "Session");
2674
2675 state.cycle_target_layer();
2676 assert_eq!(state.target_layer, ConfigLayer::User);
2677
2678 state.set_target_layer(ConfigLayer::Project);
2680 assert_eq!(state.target_layer, ConfigLayer::Project);
2681
2682 state.set_target_layer(ConfigLayer::System);
2684 assert_eq!(state.target_layer, ConfigLayer::Project);
2685 }
2686
2687 #[test]
2688 fn test_layer_switch_clears_pending_changes() {
2689 let config = test_config();
2690 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2691
2692 state.set_pending_change("/theme", serde_json::Value::String("light".to_string()));
2694 assert!(state.has_changes());
2695
2696 state.cycle_target_layer();
2698 assert!(!state.has_changes());
2699 }
2700
2701 #[test]
2720 fn nested_array_save_records_full_entry_path() {
2721 use crate::view::settings::schema::SettingType;
2724
2725 let config = test_config();
2726 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2727
2728 let item_schema = SettingSchema {
2730 path: "/item".to_string(),
2731 name: "Server".to_string(),
2732 description: None,
2733 setting_type: SettingType::Object {
2734 properties: vec![SettingSchema {
2735 path: "/enabled".to_string(),
2736 name: "Enabled".to_string(),
2737 description: None,
2738 setting_type: SettingType::Boolean,
2739 default: Some(serde_json::json!(false)),
2740 read_only: false,
2741 section: None,
2742 order: None,
2743 nullable: false,
2744 enum_from: None,
2745 dual_list_sibling: None,
2746 }],
2747 },
2748 default: None,
2749 read_only: false,
2750 section: None,
2751 order: None,
2752 nullable: false,
2753 enum_from: None,
2754 dual_list_sibling: None,
2755 };
2756
2757 let value_schema = SettingSchema {
2762 path: String::new(),
2763 name: "value".to_string(),
2764 description: None,
2765 setting_type: SettingType::ObjectArray {
2766 item_schema: Box::new(item_schema.clone()),
2767 display_field: None,
2768 },
2769 default: None,
2770 read_only: false,
2771 section: None,
2772 order: None,
2773 nullable: false,
2774 enum_from: None,
2775 dual_list_sibling: None,
2776 };
2777
2778 let parent = EntryDialogState::from_schema(
2782 "quicklsp".to_string(),
2783 &serde_json::json!([{ "enabled": true }]),
2784 &value_schema,
2785 "/universal_lsp",
2786 false, false,
2788 );
2789
2790 assert!(
2792 parent.is_single_value,
2793 "array value_schema should trigger is_single_value path"
2794 );
2795 assert_eq!(parent.entry_path(), "/universal_lsp/quicklsp");
2796
2797 state.entry_dialog_stack.push(parent);
2798
2799 state.open_nested_entry_dialog();
2804
2805 assert_eq!(
2807 state.entry_dialog_stack.len(),
2808 2,
2809 "open_nested_entry_dialog should have pushed a nested dialog"
2810 );
2811
2812 let nested_map_path = state
2815 .entry_dialog_stack
2816 .last()
2817 .map(|d| d.map_path.clone())
2818 .unwrap();
2819 assert_eq!(
2820 nested_map_path, "/universal_lsp/quicklsp",
2821 "BUG: nested dialog's map_path dropped the 'quicklsp' key segment"
2822 );
2823
2824 state.save_entry_dialog();
2826
2827 assert_eq!(state.entry_dialog_stack.len(), 1);
2829
2830 assert!(
2833 !state.pending_changes.contains_key("/universal_lsp/"),
2834 "regression: pending change recorded under empty-key path /universal_lsp/. \
2835 All keys: {:?}",
2836 state.pending_changes.keys().collect::<Vec<_>>()
2837 );
2838 assert!(
2839 !state
2840 .pending_changes
2841 .keys()
2842 .any(|k| k.starts_with("/universal_lsp") && k.ends_with('/')),
2843 "no /universal_lsp/* path should end in a trailing slash; got {:?}",
2844 state.pending_changes.keys().collect::<Vec<_>>()
2845 );
2846 assert!(
2847 state
2848 .pending_changes
2849 .contains_key("/universal_lsp/quicklsp"),
2850 "expected pending change at /universal_lsp/quicklsp, got {:?}",
2851 state.pending_changes.keys().collect::<Vec<_>>()
2852 );
2853 }
2854
2855 #[test]
2856 fn test_refresh_dual_list_sibling_updates_excluded() {
2857 use crate::view::controls::DualListState;
2858
2859 let schema = include_str!("../../../plugins/config-schema.json");
2862 let config = test_config();
2863 let mut state = SettingsState::new(schema, &config).unwrap();
2864
2865 let editor_page_idx = state
2867 .pages
2868 .iter()
2869 .position(|p| p.path == "/editor")
2870 .expect("editor page");
2871 state.selected_category = editor_page_idx;
2872
2873 let (left_idx, right_idx) = {
2874 let page = &state.pages[editor_page_idx];
2875 let l = page
2876 .items
2877 .iter()
2878 .position(|i| i.path == "/editor/status_bar/left")
2879 .expect("left item");
2880 let r = page
2881 .items
2882 .iter()
2883 .position(|i| i.path == "/editor/status_bar/right")
2884 .expect("right item");
2885 (l, r)
2886 };
2887
2888 assert!(matches!(
2890 &state.pages[editor_page_idx].items[left_idx].control,
2891 SettingControl::DualList(_)
2892 ));
2893
2894 let default_right_items: Vec<String> =
2896 match &state.pages[editor_page_idx].items[right_idx].control {
2897 SettingControl::DualList(dl) => dl.included.clone(),
2898 _ => panic!("right should be DualList"),
2899 };
2900 let initial_left_excluded: Vec<String> =
2901 match &state.pages[editor_page_idx].items[left_idx].control {
2902 SettingControl::DualList(dl) => dl.excluded.clone(),
2903 _ => panic!("left should be DualList"),
2904 };
2905 assert_eq!(
2906 initial_left_excluded, default_right_items,
2907 "left.excluded should mirror right's included on initial build"
2908 );
2909
2910 let new_element = "{chord}".to_string();
2912 state.selected_item = left_idx;
2913 state
2914 .with_current_dual_list_mut(|dl: &mut DualListState| {
2915 if !dl.included.contains(&new_element) {
2916 dl.included.push(new_element.clone());
2917 }
2918 })
2919 .expect("current item is a DualList");
2920
2921 state.refresh_dual_list_sibling();
2923
2924 match &state.pages[editor_page_idx].items[right_idx].control {
2925 SettingControl::DualList(dl) => {
2926 assert!(
2927 dl.excluded.contains(&new_element),
2928 "right.excluded should be updated to reflect left's new inclusion"
2929 );
2930 }
2931 _ => panic!("right should be DualList"),
2932 }
2933 }
2934
2935 #[test]
2936 fn test_with_dual_list_mut_returns_none_for_non_dual_list() {
2937 let config = test_config();
2938 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2939
2940 let result = state.with_dual_list_mut(0, |_| ());
2942 assert!(result.is_none());
2943 }
2944}