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, ScrollItem, ScrollablePanel};
15use std::collections::HashMap;
16
17fn set_json_pointer_create(root: &mut serde_json::Value, pointer: &str, value: serde_json::Value) {
20 if pointer.is_empty() || pointer == "/" {
21 *root = value;
22 return;
23 }
24 let parts: Vec<&str> = pointer.trim_start_matches('/').split('/').collect();
25 let mut current = root;
26 for (i, part) in parts.iter().enumerate() {
27 if i == parts.len() - 1 {
28 if let serde_json::Value::Object(map) = current {
29 map.insert(part.to_string(), value);
30 }
31 return;
32 }
33 if let serde_json::Value::Object(map) = current {
34 if !map.contains_key(*part) {
35 map.insert(
36 part.to_string(),
37 serde_json::Value::Object(Default::default()),
38 );
39 }
40 current = map.get_mut(*part).unwrap();
41 } else {
42 return;
43 }
44 }
45}
46
47enum NestedDialogInfo {
49 MapEntry {
50 key: String,
51 value: serde_json::Value,
52 schema: SettingSchema,
53 path: String,
54 is_new: bool,
55 no_delete: bool,
56 },
57 ArrayItem {
58 index: Option<usize>,
59 value: serde_json::Value,
60 schema: SettingSchema,
61 path: String,
62 is_new: bool,
63 },
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
68pub enum FocusPanel {
69 #[default]
71 Categories,
72 Settings,
74 Footer,
76}
77
78#[derive(Debug)]
80pub struct SettingsState {
81 categories: Vec<SettingCategory>,
83 pub pages: Vec<SettingsPage>,
85 pub selected_category: usize,
87 pub selected_item: usize,
89 pub focus: FocusManager<FocusPanel>,
91 pub footer_button_index: usize,
93 pub pending_changes: HashMap<String, serde_json::Value>,
95 original_config: serde_json::Value,
97 pub visible: bool,
99 pub search_query: String,
101 pub search_active: bool,
103 pub search_results: Vec<SearchResult>,
105 pub selected_search_result: usize,
107 pub search_scroll_offset: usize,
109 pub search_max_visible: usize,
111 pub showing_confirm_dialog: bool,
113 pub confirm_dialog_selection: usize,
115 pub confirm_dialog_hover: Option<usize>,
117 pub showing_reset_dialog: bool,
119 pub reset_dialog_selection: usize,
121 pub reset_dialog_hover: Option<usize>,
123 pub showing_entry_discard_confirm: bool,
128 pub entry_discard_confirm_selection: usize,
131 pub showing_entry_delete_confirm: bool,
135 pub entry_delete_confirm_selection: usize,
138 pub entry_delete_target_name: String,
140 pub entry_delete_target_is_array_item: bool,
145 pub showing_help: bool,
147 pub scroll_panel: ScrollablePanel,
149 pub sub_focus: Option<usize>,
151 pub editing_text: bool,
153 pub hover_position: Option<(u16, u16)>,
155 pub hover_hit: Option<SettingsHit>,
157 pub entry_dialog_stack: Vec<EntryDialogState>,
160 pub target_layer: ConfigLayer,
164 available_status_bar_tokens: HashMap<String, String>,
167 pub layer_sources: HashMap<String, ConfigLayer>,
171 pub pending_deletions: std::collections::HashSet<String>,
175 pub layout_width: u16,
179 pub item_style: super::items::ItemBoxStyle,
182 pub expanded_categories: std::collections::HashSet<usize>,
186 pub categories_scroll: ScrollablePanel,
189 pub tree_cursor_section: Option<usize>,
201}
202
203#[derive(Debug, Clone, Copy)]
210pub enum TreeRow {
211 Category {
212 idx: usize,
213 expandable: bool,
214 expanded: bool,
215 },
216 Section {
217 cat_idx: usize,
218 section_idx: usize,
219 },
220}
221
222impl crate::view::ui::ScrollItem for TreeRow {
223 fn height(&self, _width: u16) -> u16 {
224 1
225 }
226}
227
228impl SettingsState {
229 pub fn new(schema_json: &str, config: &Config) -> Result<Self, serde_json::Error> {
231 Self::new_with_plugin_schemas(schema_json, config, &HashMap::new())
232 }
233
234 pub fn new_with_plugin_schemas(
238 schema_json: &str,
239 config: &Config,
240 plugin_schemas: &HashMap<String, serde_json::Value>,
241 ) -> Result<Self, serde_json::Error> {
242 let mut categories = parse_schema(schema_json)?;
243
244 let mut enabled_with_schema: Vec<String> = config
246 .plugins
247 .iter()
248 .filter_map(|(name, cfg)| {
249 if cfg.enabled && plugin_schemas.contains_key(name) {
250 Some(name.clone())
251 } else {
252 None
253 }
254 })
255 .collect();
256 enabled_with_schema.sort();
257 tracing::trace!(
258 "SettingsState built: total plugin_schemas={}, enabled_with_schema={:?}",
259 plugin_schemas.len(),
260 enabled_with_schema
261 );
262 super::schema::append_plugin_settings_category(
263 &mut categories,
264 plugin_schemas,
265 &enabled_with_schema,
266 );
267
268 let config_value = serde_json::to_value(config)?;
269 let layer_sources = HashMap::new(); let target_layer = ConfigLayer::User; let available_status_bar_tokens: HashMap<String, String> = HashMap::new();
272 let pages = super::items::build_pages(
273 &categories,
274 &config_value,
275 &layer_sources,
276 target_layer,
277 &available_status_bar_tokens,
278 );
279
280 Ok(Self {
281 categories,
282 pages,
283 selected_category: 0,
284 selected_item: 0,
285 focus: FocusManager::new(vec![
286 FocusPanel::Categories,
287 FocusPanel::Settings,
288 FocusPanel::Footer,
289 ]),
290 footer_button_index: 2, pending_changes: HashMap::new(),
292 original_config: config_value,
293 visible: false,
294 search_query: String::new(),
295 search_active: false,
296 search_results: Vec::new(),
297 selected_search_result: 0,
298 search_scroll_offset: 0,
299 search_max_visible: 5, showing_confirm_dialog: false,
301 confirm_dialog_selection: 0,
302 confirm_dialog_hover: None,
303 showing_reset_dialog: false,
304 reset_dialog_selection: 0,
305 reset_dialog_hover: None,
306 showing_entry_discard_confirm: false,
307 entry_discard_confirm_selection: 0,
308 showing_entry_delete_confirm: false,
309 entry_delete_confirm_selection: 0,
310 entry_delete_target_name: String::new(),
311 entry_delete_target_is_array_item: false,
312 showing_help: false,
313 scroll_panel: ScrollablePanel::new(),
314 sub_focus: None,
315 editing_text: false,
316 available_status_bar_tokens,
317 hover_position: None,
318 hover_hit: None,
319 entry_dialog_stack: Vec::new(),
320 target_layer,
321 layer_sources,
322 pending_deletions: std::collections::HashSet::new(),
323 layout_width: 0,
324 item_style: super::items::ItemBoxStyle::default(),
325 expanded_categories: std::collections::HashSet::new(),
326 categories_scroll: ScrollablePanel::new(),
327 tree_cursor_section: None,
328 })
329 }
330
331 #[inline]
333 pub fn focus_panel(&self) -> FocusPanel {
334 self.focus.current().unwrap_or_default()
335 }
336
337 pub fn show(&mut self) {
339 self.visible = true;
340 self.focus.set(FocusPanel::Categories);
341 self.footer_button_index = 2; self.selected_category = 0;
343 self.selected_item = 0;
344 self.scroll_panel = ScrollablePanel::new();
345 self.sub_focus = None;
346 self.showing_confirm_dialog = false;
348 self.confirm_dialog_selection = 0;
349 self.confirm_dialog_hover = None;
350 self.showing_reset_dialog = false;
351 self.reset_dialog_selection = 0;
352 self.reset_dialog_hover = None;
353 self.showing_help = false;
354 }
355
356 fn rebuild_pages(&mut self) {
358 self.pages = super::items::build_pages(
359 &self.categories,
360 &self.original_config,
361 &self.layer_sources,
362 self.target_layer,
363 &self.available_status_bar_tokens,
364 );
365 }
366
367 pub fn hide(&mut self) {
369 self.visible = false;
370 self.search_active = false;
371 self.search_query.clear();
372 }
373
374 pub fn entry_dialog(&self) -> Option<&EntryDialogState> {
376 self.entry_dialog_stack.last()
377 }
378
379 pub fn entry_dialog_mut(&mut self) -> Option<&mut EntryDialogState> {
381 self.entry_dialog_stack.last_mut()
382 }
383
384 pub fn has_entry_dialog(&self) -> bool {
386 !self.entry_dialog_stack.is_empty()
387 }
388
389 pub fn current_page(&self) -> Option<&SettingsPage> {
391 self.pages.get(self.selected_category)
392 }
393
394 pub fn current_page_mut(&mut self) -> Option<&mut SettingsPage> {
396 self.pages.get_mut(self.selected_category)
397 }
398
399 pub fn topmost_visible_item_index(&self) -> Option<usize> {
404 let page = self.pages.get(self.selected_category)?;
405 if page.items.is_empty() {
406 return None;
407 }
408 let target = self.scroll_panel.scroll.offset;
409 let width = self.layout_width;
410 let mut y: u16 = 0;
411 for (idx, item) in page.items.iter().enumerate() {
412 let h = <SettingItem as ScrollItem>::height(item, width);
413 if y + h > target {
414 return Some(idx);
415 }
416 y += h;
417 }
418 Some(page.items.len() - 1)
419 }
420
421 pub fn current_section_index(&self) -> Option<usize> {
426 let page = self.pages.get(self.selected_category)?;
427 if page.sections.is_empty() {
428 return None;
429 }
430 let item_idx = self
439 .topmost_visible_item_index()
440 .unwrap_or(self.selected_item);
441 let mut current: Option<usize> = None;
443 for (s_idx, section) in page.sections.iter().enumerate() {
444 if section.first_item_index <= item_idx {
445 current = Some(s_idx);
446 } else {
447 break;
448 }
449 }
450 current
451 }
452
453 pub fn is_category_expandable(&self, cat_idx: usize) -> bool {
457 self.pages
458 .get(cat_idx)
459 .is_some_and(|p| p.sections.len() > 1)
460 }
461
462 pub fn tree_step(&mut self, delta: i32) {
472 let rows = self.visible_tree();
473 if rows.is_empty() {
474 return;
475 }
476 let cur = self.tree_cursor_index(&rows);
477 let len = rows.len() as i32;
478 let target = (cur as i32 + delta).clamp(0, len - 1) as usize;
479 if target == cur {
480 return;
481 }
482 let prev_category = self.selected_category;
483 self.update_control_focus(false);
484 match rows[target] {
485 TreeRow::Category { idx, .. } => {
486 self.selected_category = idx;
490 self.selected_item = 0;
491 self.tree_cursor_section = None;
492 if idx != prev_category {
493 self.scroll_panel = ScrollablePanel::new();
494 }
495 self.sub_focus = None;
496 self.update_control_focus(true);
497 }
498 TreeRow::Section {
499 cat_idx,
500 section_idx,
501 } => {
502 let first = self.pages[cat_idx].sections[section_idx].first_item_index;
503 self.selected_category = cat_idx;
504 self.selected_item = first;
505 self.tree_cursor_section = Some(section_idx);
506 if cat_idx != prev_category {
507 self.scroll_panel = ScrollablePanel::new();
508 }
509 self.sub_focus = None;
510 self.init_map_focus(true);
511 self.update_control_focus(true);
512 }
513 }
514 let width = self.layout_width;
520 if let Some(page) = self.pages.get(self.selected_category) {
521 if matches!(rows[target], TreeRow::Section { .. }) {
529 self.scroll_panel.update_content_height(&page.items, width);
530 let content_width = self.scroll_panel.content_width(width);
531 let item_y =
532 self.scroll_panel
533 .item_y_offset(&page.items, self.selected_item, content_width);
534 self.scroll_panel.scroll.offset = item_y;
535 } else {
536 let selected_item = self.selected_item;
537 let sub_focus = self.sub_focus;
538 self.scroll_panel.ensure_focused_visible(
539 &page.items,
540 selected_item,
541 sub_focus,
542 width,
543 );
544 }
545 }
546 let new_rows = self.visible_tree();
547 let new_cur = self.tree_cursor_index(&new_rows);
548 self.categories_scroll
549 .ensure_focused_visible(&new_rows, new_cur, None, width);
550 }
551
552 pub(super) fn tree_cursor_index(&self, rows: &[TreeRow]) -> usize {
560 let cat = self.selected_category;
561 if let Some(s_idx) = self.tree_cursor_section {
562 for (i, row) in rows.iter().enumerate() {
563 if let TreeRow::Section {
564 cat_idx,
565 section_idx,
566 } = *row
567 {
568 if cat_idx == cat && section_idx == s_idx {
569 return i;
570 }
571 }
572 }
573 }
574 for (i, row) in rows.iter().enumerate() {
575 if let TreeRow::Category { idx, .. } = *row {
576 if idx == cat {
577 return i;
578 }
579 }
580 }
581 0
582 }
583
584 pub fn auto_expand_current_category(&mut self) {
594 let idx = self.selected_category;
595 if self.is_category_expandable(idx) {
596 self.expanded_categories.insert(idx);
597 }
598 }
599
600 pub fn toggle_category_expanded(&mut self, cat_idx: usize) {
601 if !self.is_category_expandable(cat_idx) {
602 return;
603 }
604 if !self.expanded_categories.insert(cat_idx) {
605 self.expanded_categories.remove(&cat_idx);
606 }
607 }
608
609 pub fn jump_to_section(&mut self, cat_idx: usize, section_idx: usize) {
613 let Some(page) = self.pages.get(cat_idx) else {
614 return;
615 };
616 let Some(section) = page.sections.get(section_idx) else {
617 return;
618 };
619 let target_item = section.first_item_index;
620 self.update_control_focus(false);
621 self.selected_category = cat_idx;
622 self.selected_item = target_item;
623 self.tree_cursor_section = Some(section_idx);
624 self.focus.set(FocusPanel::Settings);
625 let width = self.layout_width;
626 if let Some(page) = self.pages.get(self.selected_category) {
627 self.scroll_panel.update_content_height(&page.items, width);
628 let content_width = self.scroll_panel.content_width(width);
629 let item_y = self
636 .scroll_panel
637 .item_y_offset(&page.items, target_item, content_width);
638 self.scroll_panel.scroll.offset = item_y;
639 }
640 self.sub_focus = None;
641 self.init_map_focus(true);
642 self.update_control_focus(true);
643 self.auto_expand_current_category();
644 }
645
646 pub fn visible_tree(&self) -> Vec<TreeRow> {
650 let mut rows = Vec::with_capacity(self.pages.len());
651 for (idx, page) in self.pages.iter().enumerate() {
652 let expandable = page.sections.len() > 1;
653 let expanded = expandable && self.expanded_categories.contains(&idx);
654 rows.push(TreeRow::Category {
655 idx,
656 expandable,
657 expanded,
658 });
659 if expanded {
660 for section_idx in 0..page.sections.len() {
661 rows.push(TreeRow::Section {
662 cat_idx: idx,
663 section_idx,
664 });
665 }
666 }
667 }
668 rows
669 }
670
671 pub fn current_item(&self) -> Option<&SettingItem> {
673 self.current_page()
674 .and_then(|page| page.items.get(self.selected_item))
675 }
676
677 pub fn current_item_mut(&mut self) -> Option<&mut SettingItem> {
679 self.pages
680 .get_mut(self.selected_category)
681 .and_then(|page| page.items.get_mut(self.selected_item))
682 }
683
684 pub fn can_exit_text_editing(&self) -> bool {
686 self.current_item()
687 .map(|item| {
688 if let SettingControl::Text(state) = &item.control {
689 state.is_valid()
690 } else {
691 true
692 }
693 })
694 .unwrap_or(true)
695 }
696
697 pub fn entry_dialog_can_exit_text_editing(&self) -> bool {
699 self.entry_dialog()
700 .and_then(|dialog| dialog.current_item())
701 .map(|item| {
702 if let SettingControl::Text(state) = &item.control {
703 state.is_valid()
704 } else {
705 true
706 }
707 })
708 .unwrap_or(true)
709 }
710
711 fn init_map_focus(&mut self, from_above: bool) {
714 if let Some(item) = self.current_item_mut() {
715 if let SettingControl::Map(ref mut map_state) = item.control {
716 map_state.init_focus(from_above);
717 }
718 }
719 self.update_map_sub_focus();
721 }
722
723 pub(super) fn update_control_focus(&mut self, focused: bool) {
727 let focus_state = if focused {
728 FocusState::Focused
729 } else {
730 FocusState::Normal
731 };
732 if let Some(item) = self.current_item_mut() {
733 match &mut item.control {
734 SettingControl::Map(ref mut state) => state.focus = focus_state,
735 SettingControl::TextList(ref mut state) => state.focus = focus_state,
736 SettingControl::DualList(ref mut state) => state.focus = focus_state,
737 SettingControl::ObjectArray(ref mut state) => state.focus = focus_state,
738 SettingControl::Toggle(ref mut state) => state.focus = focus_state,
739 SettingControl::Number(ref mut state) => state.focus = focus_state,
740 SettingControl::Dropdown(ref mut state) => state.focus = focus_state,
741 SettingControl::Text(ref mut state) => {
742 state.focus = focus_state;
743 if !focused {
747 state.editing = false;
748 }
749 }
750 SettingControl::Json(_) | SettingControl::Complex { .. } => {} }
752 }
753 }
754
755 fn update_map_sub_focus(&mut self) {
758 self.sub_focus = self.current_item().and_then(|item| {
759 if let SettingControl::Map(ref map_state) = item.control {
760 Some(match map_state.focused_entry {
762 Some(i) => 1 + i,
763 None => 1 + map_state.entries.len(), })
765 } else {
766 None
767 }
768 });
769 }
770
771 pub fn select_prev(&mut self) {
773 match self.focus_panel() {
774 FocusPanel::Categories => {
775 self.tree_step(-1);
776 }
777 FocusPanel::Settings => {
778 let handled = self
780 .current_item_mut()
781 .and_then(|item| match &mut item.control {
782 SettingControl::Map(map_state) => Some(map_state.focus_prev()),
783 _ => None,
784 })
785 .unwrap_or(false);
786
787 if handled {
788 self.update_map_sub_focus();
790 } else if self.selected_item > 0 {
791 self.update_control_focus(false); self.selected_item -= 1;
793 self.sub_focus = None;
794 self.init_map_focus(false); self.update_control_focus(true); }
797 self.ensure_visible();
798 }
799 FocusPanel::Footer => {
800 if self.footer_button_index > 0 {
802 self.footer_button_index -= 1;
803 }
804 }
805 }
806 }
807
808 pub fn select_next(&mut self) {
810 match self.focus_panel() {
811 FocusPanel::Categories => {
812 self.tree_step(1);
813 }
814 FocusPanel::Settings => {
815 let handled = self
817 .current_item_mut()
818 .and_then(|item| match &mut item.control {
819 SettingControl::Map(map_state) => Some(map_state.focus_next()),
820 _ => None,
821 })
822 .unwrap_or(false);
823
824 if handled {
825 self.update_map_sub_focus();
827 } else {
828 let can_move = self
829 .current_page()
830 .is_some_and(|page| self.selected_item + 1 < page.items.len());
831 if can_move {
832 self.update_control_focus(false); self.selected_item += 1;
834 self.sub_focus = None;
835 self.init_map_focus(true); self.update_control_focus(true); }
838 }
839 self.ensure_visible();
840 }
841 FocusPanel::Footer => {
842 if self.footer_button_index < 2 {
844 self.footer_button_index += 1;
845 }
846 }
847 }
848 }
849
850 pub fn select_next_page(&mut self) {
852 let page_size = self.scroll_panel.viewport_height().max(1);
853 for _ in 0..page_size {
854 self.select_next();
855 }
856 }
857
858 pub fn select_prev_page(&mut self) {
860 let page_size = self.scroll_panel.viewport_height().max(1);
861 for _ in 0..page_size {
862 self.select_prev();
863 }
864 }
865
866 pub fn toggle_focus(&mut self) {
868 let old_panel = self.focus_panel();
869 self.focus.focus_next();
870 self.on_panel_changed(old_panel, true);
871 }
872
873 pub fn toggle_focus_backward(&mut self) {
875 let old_panel = self.focus_panel();
876 self.focus.focus_prev();
877 self.on_panel_changed(old_panel, false);
878 }
879
880 fn on_panel_changed(&mut self, old_panel: FocusPanel, forward: bool) {
882 if old_panel == FocusPanel::Settings {
884 self.update_control_focus(false);
885 }
886
887 if self.focus_panel() == FocusPanel::Settings
889 && self.selected_item >= self.current_page().map_or(0, |p| p.items.len())
890 {
891 self.selected_item = 0;
892 }
893 self.sub_focus = None;
894
895 if self.focus_panel() == FocusPanel::Settings {
896 self.init_map_focus(forward); self.update_control_focus(true); }
899
900 if self.focus_panel() == FocusPanel::Footer {
902 self.footer_button_index = if forward {
903 0 } else {
905 4 };
907 }
908
909 self.ensure_visible();
910 }
911
912 pub fn set_item_style(&mut self, style: super::items::ItemBoxStyle) {
920 if self.item_style == style {
921 return;
922 }
923 self.item_style = style;
924 for page in &mut self.pages {
925 for item in &mut page.items {
926 item.style = style;
927 }
928 }
929 }
930
931 pub fn ensure_visible(&mut self) {
933 if self.focus_panel() != FocusPanel::Settings {
934 return;
935 }
936
937 let selected_item = self.selected_item;
939 let sub_focus = self.sub_focus;
940 let width = self.layout_width;
941 let prev_offset = self.scroll_panel.scroll.offset;
942 if let Some(page) = self.pages.get(self.selected_category) {
943 self.scroll_panel
944 .ensure_focused_visible(&page.items, selected_item, sub_focus, width);
945 }
946 if self.scroll_panel.scroll.offset != prev_offset {
950 self.sync_tree_cursor_to_body_scroll();
951 }
952 }
953
954 pub fn set_pending_change(&mut self, path: &str, value: serde_json::Value) {
956 let original = self.original_config.pointer(path);
958 if original == Some(&value) {
959 self.pending_changes.remove(path);
960 } else {
961 self.pending_changes.insert(path.to_string(), value);
962 }
963 }
964
965 pub fn has_changes(&self) -> bool {
967 !self.pending_changes.is_empty() || !self.pending_deletions.is_empty()
968 }
969
970 pub fn apply_changes(&self, config: &Config) -> Result<Config, serde_json::Error> {
972 let mut config_value = serde_json::to_value(config)?;
973
974 for path in &self.pending_deletions {
984 crate::config_io::remove_json_pointer(&mut config_value, path);
985 }
986
987 for (path, value) in &self.pending_changes {
988 if let Some(target) = config_value.pointer_mut(path) {
995 *target = value.clone();
996 } else {
997 set_json_pointer_create(&mut config_value, path, value.clone());
998 }
999 }
1000
1001 serde_json::from_value(config_value)
1002 }
1003
1004 pub fn discard_changes(&mut self) {
1006 self.pending_changes.clear();
1007 self.pending_deletions.clear();
1008 self.rebuild_pages();
1010 }
1011
1012 pub fn set_target_layer(&mut self, layer: ConfigLayer) {
1014 if layer != ConfigLayer::System {
1015 self.target_layer = layer;
1017 self.pending_changes.clear();
1019 self.pending_deletions.clear();
1020 self.rebuild_pages();
1022 }
1023 }
1024
1025 pub fn cycle_target_layer(&mut self) {
1027 self.target_layer = match self.target_layer {
1028 ConfigLayer::System => ConfigLayer::User, ConfigLayer::User => ConfigLayer::Project,
1030 ConfigLayer::Project => ConfigLayer::Session,
1031 ConfigLayer::Session => ConfigLayer::User,
1032 };
1033 self.pending_changes.clear();
1035 self.pending_deletions.clear();
1036 self.rebuild_pages();
1038 }
1039
1040 pub fn target_layer_name(&self) -> &'static str {
1042 match self.target_layer {
1043 ConfigLayer::System => "System (read-only)",
1044 ConfigLayer::User => "User",
1045 ConfigLayer::Project => "Project",
1046 ConfigLayer::Session => "Session",
1047 }
1048 }
1049
1050 pub fn set_layer_sources(&mut self, sources: HashMap<String, ConfigLayer>) {
1053 self.layer_sources = sources;
1054 self.rebuild_pages();
1056 }
1057
1058 pub fn set_status_bar_tokens(&mut self, tokens: HashMap<String, String>) {
1061 self.available_status_bar_tokens = tokens;
1062 self.rebuild_pages();
1063 }
1064
1065 pub fn get_layer_source(&self, path: &str) -> ConfigLayer {
1068 self.layer_sources
1069 .get(path)
1070 .copied()
1071 .unwrap_or(ConfigLayer::System)
1072 }
1073
1074 pub fn layer_source_label(layer: ConfigLayer) -> &'static str {
1076 match layer {
1077 ConfigLayer::System => "default",
1078 ConfigLayer::User => "user",
1079 ConfigLayer::Project => "project",
1080 ConfigLayer::Session => "session",
1081 }
1082 }
1083
1084 pub fn entry_dialog_activate_focused_field_button(&mut self) -> bool {
1102 match self.entry_dialog_mut() {
1103 Some(dialog) => dialog.activate_focused_field_button(),
1104 None => false,
1105 }
1106 }
1107
1108 pub fn reset_current_to_default(&mut self) {
1109 let reset_info = self.current_item().and_then(|item| {
1111 if !item.modified || item.is_auto_managed {
1114 return None;
1115 }
1116 item.default
1117 .as_ref()
1118 .map(|default| (item.path.clone(), default.clone()))
1119 });
1120
1121 if let Some((path, default)) = reset_info {
1122 self.pending_deletions.insert(path.clone());
1124 self.pending_changes.remove(&path);
1126
1127 if let Some(item) = self.current_item_mut() {
1131 update_control_from_value(&mut item.control, &default);
1132 item.modified = false;
1133 item.layer_source = ConfigLayer::System; }
1136 }
1137 }
1138
1139 pub fn set_current_to_null(&mut self) {
1145 let target_layer = self.target_layer;
1146 let change_info = self.current_item().and_then(|item| {
1147 if !item.nullable || item.is_null || item.read_only {
1148 return None;
1149 }
1150 Some(item.path.clone())
1151 });
1152
1153 if let Some(path) = change_info {
1154 self.pending_changes
1156 .insert(path.clone(), serde_json::Value::Null);
1157 self.pending_deletions.remove(&path);
1158
1159 if let Some(item) = self.current_item_mut() {
1161 item.is_null = true;
1162 item.modified = true;
1163 item.layer_source = target_layer;
1164 }
1165 }
1166 }
1167
1168 pub fn clear_current_category(&mut self) {
1174 let target_layer = self.target_layer;
1175 let page = match self.current_page() {
1176 Some(p) if p.nullable => p,
1177 _ => return,
1178 };
1179 let page_path = page.path.clone();
1180
1181 self.pending_changes
1183 .insert(page_path.clone(), serde_json::Value::Null);
1184
1185 let prefix = format!("{}/", page_path);
1187 self.pending_changes
1188 .retain(|path, _| !path.starts_with(&prefix));
1189 self.pending_deletions
1190 .retain(|path| !path.starts_with(&prefix));
1191
1192 if let Some(page) = self.current_page_mut() {
1194 for item in &mut page.items {
1195 if item.nullable {
1196 item.is_null = true;
1197 item.modified = false;
1198 item.layer_source = target_layer;
1199 }
1200 }
1201 }
1202 }
1203
1204 pub fn current_category_has_values(&self) -> bool {
1206 match self.current_page() {
1207 Some(page) if page.nullable => {
1208 page.items.iter().any(|item| !item.is_null && item.nullable)
1209 || page.items.iter().any(|item| item.modified)
1210 }
1211 _ => false,
1212 }
1213 }
1214
1215 pub fn on_value_changed(&mut self) {
1217 let target_layer = self.target_layer;
1219
1220 let change_info = self.current_item().map(|item| {
1222 let value = control_to_value(&item.control);
1223 (item.path.clone(), value)
1224 });
1225
1226 if let Some((path, value)) = change_info {
1227 self.pending_deletions.remove(&path);
1230
1231 if let Some(item) = self.current_item_mut() {
1233 item.modified = true; item.layer_source = target_layer; item.is_null = false; }
1237 self.set_pending_change(&path, value);
1238 }
1239 }
1240
1241 pub fn update_focus_states(&mut self) {
1243 let current_focus = self.focus_panel();
1244 for (page_idx, page) in self.pages.iter_mut().enumerate() {
1245 for (item_idx, item) in page.items.iter_mut().enumerate() {
1246 let is_focused = current_focus == FocusPanel::Settings
1247 && page_idx == self.selected_category
1248 && item_idx == self.selected_item;
1249
1250 let focus = if is_focused {
1251 FocusState::Focused
1252 } else {
1253 FocusState::Normal
1254 };
1255
1256 match &mut item.control {
1257 SettingControl::Toggle(state) => state.focus = focus,
1258 SettingControl::Number(state) => state.focus = focus,
1259 SettingControl::Dropdown(state) => state.focus = focus,
1260 SettingControl::Text(state) => state.focus = focus,
1261 SettingControl::TextList(state) => state.focus = focus,
1262 SettingControl::DualList(state) => state.focus = focus,
1263 SettingControl::Map(state) => state.focus = focus,
1264 SettingControl::ObjectArray(state) => state.focus = focus,
1265 SettingControl::Json(state) => state.focus = focus,
1266 SettingControl::Complex { .. } => {}
1267 }
1268 }
1269 }
1270 }
1271
1272 pub fn start_search(&mut self) {
1274 self.search_active = true;
1275 self.search_query.clear();
1276 self.search_results.clear();
1277 self.selected_search_result = 0;
1278 self.search_scroll_offset = 0;
1279 }
1280
1281 pub fn cancel_search(&mut self) {
1283 self.search_active = false;
1284 self.search_query.clear();
1285 self.search_results.clear();
1286 self.selected_search_result = 0;
1287 self.search_scroll_offset = 0;
1288 }
1289
1290 pub fn set_search_query(&mut self, query: String) {
1292 self.search_query = query;
1293 self.search_results = search_settings(&self.pages, &self.search_query);
1294 self.selected_search_result = 0;
1295 self.search_scroll_offset = 0;
1296 }
1297
1298 pub fn search_push_char(&mut self, c: char) {
1300 self.search_query.push(c);
1301 self.search_results = search_settings(&self.pages, &self.search_query);
1302 self.selected_search_result = 0;
1303 self.search_scroll_offset = 0;
1304 }
1305
1306 pub fn search_pop_char(&mut self) {
1308 self.search_query.pop();
1309 self.search_results = search_settings(&self.pages, &self.search_query);
1310 self.selected_search_result = 0;
1311 self.search_scroll_offset = 0;
1312 }
1313
1314 pub fn search_prev(&mut self) {
1316 if !self.search_results.is_empty() && self.selected_search_result > 0 {
1317 self.selected_search_result -= 1;
1318 if self.selected_search_result < self.search_scroll_offset {
1320 self.search_scroll_offset = self.selected_search_result;
1321 }
1322 }
1323 }
1324
1325 pub fn search_next(&mut self) {
1327 if !self.search_results.is_empty()
1328 && self.selected_search_result + 1 < self.search_results.len()
1329 {
1330 self.selected_search_result += 1;
1331 if self.selected_search_result >= self.search_scroll_offset + self.search_max_visible {
1333 self.search_scroll_offset =
1334 self.selected_search_result - self.search_max_visible + 1;
1335 }
1336 }
1337 }
1338
1339 pub fn search_scroll_up(&mut self, delta: usize) -> bool {
1341 if self.search_results.is_empty() || self.search_scroll_offset == 0 {
1342 return false;
1343 }
1344 self.search_scroll_offset = self.search_scroll_offset.saturating_sub(delta);
1345 if self.selected_search_result >= self.search_scroll_offset + self.search_max_visible {
1347 self.selected_search_result = self.search_scroll_offset + self.search_max_visible - 1;
1348 }
1349 true
1350 }
1351
1352 pub fn search_scroll_down(&mut self, delta: usize) -> bool {
1354 if self.search_results.is_empty() {
1355 return false;
1356 }
1357 let max_offset = self
1358 .search_results
1359 .len()
1360 .saturating_sub(self.search_max_visible);
1361 if self.search_scroll_offset >= max_offset {
1362 return false;
1363 }
1364 self.search_scroll_offset = (self.search_scroll_offset + delta).min(max_offset);
1365 if self.selected_search_result < self.search_scroll_offset {
1367 self.selected_search_result = self.search_scroll_offset;
1368 }
1369 true
1370 }
1371
1372 pub fn search_scroll_to_ratio(&mut self, ratio: f32) -> bool {
1374 if self.search_results.is_empty() {
1375 return false;
1376 }
1377 let max_offset = self
1378 .search_results
1379 .len()
1380 .saturating_sub(self.search_max_visible);
1381 let new_offset = (ratio * max_offset as f32) as usize;
1382 if new_offset != self.search_scroll_offset {
1383 self.search_scroll_offset = new_offset.min(max_offset);
1384 if self.selected_search_result < self.search_scroll_offset {
1386 self.selected_search_result = self.search_scroll_offset;
1387 } else if self.selected_search_result
1388 >= self.search_scroll_offset + self.search_max_visible
1389 {
1390 self.selected_search_result =
1391 self.search_scroll_offset + self.search_max_visible - 1;
1392 }
1393 return true;
1394 }
1395 false
1396 }
1397
1398 pub fn jump_to_search_result(&mut self) {
1400 let Some(result) = self
1402 .search_results
1403 .get(self.selected_search_result)
1404 .cloned()
1405 else {
1406 return;
1407 };
1408 let page_index = result.page_index;
1409 let item_index = result.item_index;
1410
1411 self.update_control_focus(false);
1413 self.selected_category = page_index;
1414 self.selected_item = item_index;
1415 self.focus.set(FocusPanel::Settings);
1416 self.scroll_panel.scroll.offset = 0;
1418 self.sub_focus = None;
1419 self.init_map_focus(true);
1420
1421 if let Some(ref deep_match) = result.deep_match {
1423 self.jump_to_deep_match(deep_match);
1424 }
1425
1426 self.update_control_focus(true); self.auto_expand_current_category();
1428 self.tree_cursor_section = self.current_section_index();
1432 self.ensure_visible();
1433 self.cancel_search();
1434 }
1435
1436 fn jump_to_deep_match(&mut self, deep_match: &DeepMatch) {
1438 match deep_match {
1439 DeepMatch::MapKey { entry_index, .. } | DeepMatch::MapValue { entry_index, .. } => {
1440 if let Some(item) = self.current_item_mut() {
1441 if let SettingControl::Map(ref mut map_state) = item.control {
1442 map_state.focused_entry = Some(*entry_index);
1443 }
1444 }
1445 self.update_map_sub_focus();
1446 }
1447 DeepMatch::TextListItem { item_index, .. } => {
1448 if let Some(item) = self.current_item_mut() {
1449 if let SettingControl::TextList(ref mut list_state) = item.control {
1450 list_state.focused_item = Some(*item_index);
1451 }
1452 }
1453 self.sub_focus = Some(1 + *item_index);
1455 }
1456 }
1457 }
1458
1459 pub fn current_search_result(&self) -> Option<&SearchResult> {
1461 self.search_results.get(self.selected_search_result)
1462 }
1463
1464 pub fn show_confirm_dialog(&mut self) {
1466 self.showing_confirm_dialog = true;
1467 self.confirm_dialog_selection = 0; }
1469
1470 pub fn hide_confirm_dialog(&mut self) {
1472 self.showing_confirm_dialog = false;
1473 self.confirm_dialog_selection = 0;
1474 }
1475
1476 pub fn confirm_dialog_next(&mut self) {
1478 self.confirm_dialog_selection = (self.confirm_dialog_selection + 1) % 3;
1479 }
1480
1481 pub fn confirm_dialog_prev(&mut self) {
1483 self.confirm_dialog_selection = if self.confirm_dialog_selection == 0 {
1484 2
1485 } else {
1486 self.confirm_dialog_selection - 1
1487 };
1488 }
1489
1490 pub fn toggle_help(&mut self) {
1492 self.showing_help = !self.showing_help;
1493 }
1494
1495 pub fn hide_help(&mut self) {
1497 self.showing_help = false;
1498 }
1499
1500 pub fn showing_entry_dialog(&self) -> bool {
1502 self.has_entry_dialog()
1503 }
1504
1505 pub fn open_entry_dialog(&mut self) {
1507 let Some(item) = self.current_item() else {
1508 return;
1509 };
1510
1511 let path = item.path.as_str();
1513 let SettingControl::Map(map_state) = &item.control else {
1514 return;
1515 };
1516
1517 let Some(entry_idx) = map_state.focused_entry else {
1519 return;
1520 };
1521 let Some((key, value)) = map_state.entries.get(entry_idx) else {
1522 return;
1523 };
1524
1525 let Some(schema) = map_state.value_schema.as_ref() else {
1527 return; };
1529
1530 let no_delete = map_state.no_add;
1532
1533 let entry_pointer = format!("{}/{}", path, key);
1538 let key = key.clone();
1539 let value = value.clone();
1540
1541 let mut dialog = EntryDialogState::from_schema(
1543 key,
1544 &value,
1545 schema,
1546 path,
1547 false,
1548 no_delete,
1549 &self.available_status_bar_tokens,
1550 );
1551 apply_builtin_defaults(&mut dialog, &entry_pointer);
1552 dialog.inheritable_fields = inheritable_fields_for(path);
1553 self.entry_dialog_stack.push(dialog);
1554 }
1555
1556 pub fn open_add_entry_dialog(&mut self) {
1558 let Some(item) = self.current_item() else {
1559 return;
1560 };
1561 let SettingControl::Map(map_state) = &item.control else {
1562 return;
1563 };
1564 let Some(schema) = map_state.value_schema.as_ref() else {
1565 return;
1566 };
1567 let path = item.path.clone();
1568
1569 let dialog = EntryDialogState::from_schema(
1572 String::new(),
1573 &serde_json::json!({}),
1574 schema,
1575 &path,
1576 true,
1577 false,
1578 &self.available_status_bar_tokens,
1579 );
1580 self.entry_dialog_stack.push(dialog);
1581 }
1582
1583 pub fn open_add_array_item_dialog(&mut self) {
1585 let Some(item) = self.current_item() else {
1586 return;
1587 };
1588 let SettingControl::ObjectArray(array_state) = &item.control else {
1589 return;
1590 };
1591 let Some(schema) = array_state.item_schema.as_ref() else {
1592 return;
1593 };
1594 let path = item.path.clone();
1595
1596 let dialog = EntryDialogState::for_array_item(
1598 None,
1599 &serde_json::json!({}),
1600 schema,
1601 &path,
1602 true,
1603 &self.available_status_bar_tokens,
1604 );
1605 self.entry_dialog_stack.push(dialog);
1606 }
1607
1608 pub fn open_edit_array_item_dialog(&mut self) {
1610 let Some(item) = self.current_item() else {
1611 return;
1612 };
1613 let SettingControl::ObjectArray(array_state) = &item.control else {
1614 return;
1615 };
1616 let Some(schema) = array_state.item_schema.as_ref() else {
1617 return;
1618 };
1619 let Some(index) = array_state.focused_index else {
1620 return;
1621 };
1622 let Some(value) = array_state.bindings.get(index) else {
1623 return;
1624 };
1625 let path = item.path.clone();
1626
1627 let dialog = EntryDialogState::for_array_item(
1628 Some(index),
1629 value,
1630 schema,
1631 &path,
1632 false,
1633 &self.available_status_bar_tokens,
1634 );
1635 self.entry_dialog_stack.push(dialog);
1636 }
1637
1638 pub fn close_entry_dialog(&mut self) {
1640 self.entry_dialog_stack.pop();
1641 }
1642
1643 pub fn open_nested_entry_dialog(&mut self) {
1648 let nested_info = self.entry_dialog().and_then(|dialog| {
1650 let item = dialog.current_item()?;
1651 let base = dialog.entry_path();
1657 let relative = item.path.trim_start_matches('/');
1658 let path = if relative.is_empty() {
1659 base
1663 } else {
1664 format!("{}/{}", base, relative)
1665 };
1666
1667 match &item.control {
1668 SettingControl::Map(map_state) => {
1669 let schema = map_state.value_schema.as_ref()?;
1670 let no_delete = map_state.no_add; if let Some(entry_idx) = map_state.focused_entry {
1672 let (key, value) = map_state.entries.get(entry_idx)?;
1674 Some(NestedDialogInfo::MapEntry {
1675 key: key.clone(),
1676 value: value.clone(),
1677 schema: schema.as_ref().clone(),
1678 path,
1679 is_new: false,
1680 no_delete,
1681 })
1682 } else {
1683 Some(NestedDialogInfo::MapEntry {
1685 key: String::new(),
1686 value: serde_json::json!({}),
1687 schema: schema.as_ref().clone(),
1688 path,
1689 is_new: true,
1690 no_delete: false, })
1692 }
1693 }
1694 SettingControl::ObjectArray(array_state) => {
1695 let schema = array_state.item_schema.as_ref()?;
1696 if let Some(index) = array_state.focused_index {
1697 let value = array_state.bindings.get(index)?;
1699 Some(NestedDialogInfo::ArrayItem {
1700 index: Some(index),
1701 value: value.clone(),
1702 schema: schema.as_ref().clone(),
1703 path,
1704 is_new: false,
1705 })
1706 } else {
1707 Some(NestedDialogInfo::ArrayItem {
1709 index: None,
1710 value: serde_json::json!({}),
1711 schema: schema.as_ref().clone(),
1712 path,
1713 is_new: true,
1714 })
1715 }
1716 }
1717 _ => None,
1718 }
1719 });
1720
1721 if let Some(info) = nested_info {
1723 let dialog = match info {
1724 NestedDialogInfo::MapEntry {
1725 key,
1726 value,
1727 schema,
1728 path,
1729 is_new,
1730 no_delete,
1731 } => EntryDialogState::from_schema(
1732 key,
1733 &value,
1734 &schema,
1735 &path,
1736 is_new,
1737 no_delete,
1738 &self.available_status_bar_tokens,
1739 ),
1740 NestedDialogInfo::ArrayItem {
1741 index,
1742 value,
1743 schema,
1744 path,
1745 is_new,
1746 } => EntryDialogState::for_array_item(
1747 index,
1748 &value,
1749 &schema,
1750 &path,
1751 is_new,
1752 &self.available_status_bar_tokens,
1753 ),
1754 };
1755 self.entry_dialog_stack.push(dialog);
1756 }
1757 }
1758
1759 pub fn save_entry_dialog(&mut self) {
1764 let is_array = if self.entry_dialog_stack.len() > 1 {
1768 self.entry_dialog_stack
1770 .get(self.entry_dialog_stack.len() - 2)
1771 .and_then(|parent| parent.current_item())
1772 .map(|item| matches!(item.control, SettingControl::ObjectArray(_)))
1773 .unwrap_or(false)
1774 } else {
1775 self.current_item()
1777 .map(|item| matches!(item.control, SettingControl::ObjectArray(_)))
1778 .unwrap_or(false)
1779 };
1780
1781 if is_array {
1782 self.save_array_item_dialog_inner();
1783 } else {
1784 self.save_map_entry_dialog_inner();
1785 }
1786 }
1787
1788 fn save_map_entry_dialog_inner(&mut self) {
1790 let Some(mut dialog) = self.entry_dialog_stack.pop() else {
1791 return;
1792 };
1793 dialog.commit_pending_list_drafts();
1797
1798 let key = dialog.get_key();
1800 if key.is_empty() {
1801 return; }
1803
1804 let value = dialog.to_value();
1805 let map_path = dialog.map_path.clone();
1806 let original_key = dialog.entry_key.clone();
1807 let is_new = dialog.is_new;
1808 let key_changed = !is_new && key != original_key;
1809
1810 if let Some(item) = self.current_item_mut() {
1812 if let SettingControl::Map(map_state) = &mut item.control {
1813 if key_changed {
1815 if let Some(idx) = map_state
1816 .entries
1817 .iter()
1818 .position(|(k, _)| k == &original_key)
1819 {
1820 map_state.entries.remove(idx);
1821 }
1822 }
1823
1824 if let Some(entry) = map_state.entries.iter_mut().find(|(k, _)| k == &key) {
1826 entry.1 = value.clone();
1827 } else {
1828 map_state.entries.push((key.clone(), value.clone()));
1829 map_state.entries.sort_by(|a, b| a.0.cmp(&b.0));
1830 }
1831 }
1832 }
1833
1834 if key_changed {
1836 let old_path = format!("{}/{}", map_path, original_key);
1837 self.pending_changes
1838 .insert(old_path, serde_json::Value::Null);
1839 }
1840
1841 let path = format!("{}/{}", map_path, key);
1843 self.set_pending_change(&path, value);
1844 }
1845
1846 fn save_array_item_dialog_inner(&mut self) {
1848 let Some(mut dialog) = self.entry_dialog_stack.pop() else {
1849 return;
1850 };
1851 dialog.commit_pending_list_drafts();
1853
1854 let value = dialog.to_value();
1855 let array_path = dialog.map_path.clone();
1856 let is_new = dialog.is_new;
1857 let entry_key = dialog.entry_key.clone();
1858
1859 let is_nested = !self.entry_dialog_stack.is_empty();
1861
1862 if is_nested {
1863 let parent_entry_path = self
1871 .entry_dialog_stack
1872 .last()
1873 .map(|p| p.entry_path())
1874 .unwrap_or_default();
1875 let item_path = array_path
1876 .strip_prefix(parent_entry_path.as_str())
1877 .unwrap_or(&array_path)
1878 .trim_end_matches('/')
1879 .to_string();
1880
1881 if let Some(parent) = self.entry_dialog_stack.last_mut() {
1887 if let Some(item) = parent.items.iter_mut().find(|i| i.path == item_path) {
1888 if let SettingControl::ObjectArray(array_state) = &mut item.control {
1889 if is_new {
1890 array_state.bindings.push(value.clone());
1891 } else if let Ok(index) = entry_key.parse::<usize>() {
1892 if index < array_state.bindings.len() {
1893 array_state.bindings[index] = value.clone();
1894 }
1895 }
1896 parent.user_edited = true;
1897 }
1898 }
1899 }
1900
1901 if let Some(parent) = self.entry_dialog_stack.last() {
1904 if let Some(item) = parent.items.iter().find(|i| i.path == item_path) {
1905 if let SettingControl::ObjectArray(array_state) = &item.control {
1906 let array_value = serde_json::Value::Array(array_state.bindings.clone());
1907 self.set_pending_change(&array_path, array_value);
1908 }
1909 }
1910 }
1911 } else {
1912 if let Some(item) = self.current_item_mut() {
1914 if let SettingControl::ObjectArray(array_state) = &mut item.control {
1915 if is_new {
1916 array_state.bindings.push(value.clone());
1917 } else if let Ok(index) = entry_key.parse::<usize>() {
1918 if index < array_state.bindings.len() {
1919 array_state.bindings[index] = value.clone();
1920 }
1921 }
1922 }
1923 }
1924
1925 if let Some(item) = self.current_item() {
1927 if let SettingControl::ObjectArray(array_state) = &item.control {
1928 let array_value = serde_json::Value::Array(array_state.bindings.clone());
1929 self.set_pending_change(&array_path, array_value);
1930 }
1931 }
1932 }
1933 }
1934
1935 pub fn request_entry_delete_confirm(&mut self) {
1941 let (name, is_array_item) = self
1942 .entry_dialog()
1943 .map(|d| (d.entry_key.clone(), d.is_array_item))
1944 .unwrap_or_default();
1945 self.entry_delete_target_name = if is_array_item { String::new() } else { name };
1949 self.entry_delete_target_is_array_item = is_array_item;
1950 self.entry_delete_confirm_selection = 0;
1951 self.showing_entry_delete_confirm = true;
1952 }
1953
1954 pub fn delete_entry_dialog(&mut self) {
1955 let is_nested = self.entry_dialog_stack.len() > 1;
1957
1958 let Some(dialog) = self.entry_dialog_stack.pop() else {
1959 return;
1960 };
1961
1962 let path = format!("{}/{}", dialog.map_path, dialog.entry_key);
1963
1964 if is_nested {
1966 let map_field = dialog.map_path.rsplit('/').next().unwrap_or("").to_string();
1969 let item_path = format!("/{}", map_field);
1970
1971 if let Some(parent) = self.entry_dialog_stack.last_mut() {
1973 if let Some(item) = parent.items.iter_mut().find(|i| i.path == item_path) {
1974 if let SettingControl::Map(map_state) = &mut item.control {
1975 if let Some(idx) = map_state
1976 .entries
1977 .iter()
1978 .position(|(k, _)| k == &dialog.entry_key)
1979 {
1980 map_state.remove_entry(idx);
1981 }
1982 }
1983 }
1984 }
1985 } else {
1986 if let Some(item) = self.current_item_mut() {
1988 if let SettingControl::Map(map_state) = &mut item.control {
1989 if let Some(idx) = map_state
1990 .entries
1991 .iter()
1992 .position(|(k, _)| k == &dialog.entry_key)
1993 {
1994 map_state.remove_entry(idx);
1995 }
1996 }
1997 }
1998 }
1999
2000 self.pending_changes.remove(&path);
2008 self.pending_deletions.insert(path);
2009 }
2010
2011 pub fn max_scroll(&self) -> u16 {
2013 self.scroll_panel.scroll.max_offset()
2014 }
2015
2016 pub fn scroll_up(&mut self, delta: usize) -> bool {
2019 let old = self.scroll_panel.scroll.offset;
2020 self.scroll_panel.scroll_up(delta as u16);
2021 let changed = old != self.scroll_panel.scroll.offset;
2022 if changed {
2023 self.sync_tree_cursor_to_body_scroll();
2024 }
2025 changed
2026 }
2027
2028 pub fn scroll_down(&mut self, delta: usize) -> bool {
2031 let old = self.scroll_panel.scroll.offset;
2032 self.scroll_panel.scroll_down(delta as u16);
2033 let changed = old != self.scroll_panel.scroll.offset;
2034 if changed {
2035 self.sync_tree_cursor_to_body_scroll();
2036 }
2037 changed
2038 }
2039
2040 pub fn scroll_to_ratio(&mut self, ratio: f32) -> bool {
2043 let old = self.scroll_panel.scroll.offset;
2044 self.scroll_panel.scroll_to_ratio(ratio);
2045 let changed = old != self.scroll_panel.scroll.offset;
2046 if changed {
2047 self.sync_tree_cursor_to_body_scroll();
2048 }
2049 changed
2050 }
2051
2052 pub(super) fn sync_tree_cursor_to_body_scroll(&mut self) {
2058 if let Some(section_idx) = self.current_section_index() {
2059 self.tree_cursor_section = Some(section_idx);
2060 }
2061 }
2066
2067 pub fn is_number_control(&self) -> bool {
2070 self.current_item()
2071 .is_some_and(|item| matches!(item.control, SettingControl::Number(_)))
2072 }
2073
2074 pub fn start_editing(&mut self) {
2075 if let Some(item) = self.current_item() {
2076 if matches!(
2077 item.control,
2078 SettingControl::TextList(_)
2079 | SettingControl::DualList(_)
2080 | SettingControl::Text(_)
2081 | SettingControl::Map(_)
2082 | SettingControl::Json(_)
2083 ) {
2084 self.editing_text = true;
2085 }
2086 }
2087 if let Some(item) = self.current_item_mut() {
2088 match item.control {
2089 SettingControl::DualList(ref mut dl) => {
2090 dl.editing = true;
2091 }
2092 SettingControl::Text(ref mut state) => {
2093 state.editing = true;
2094 state.arm_replace_on_type();
2099 }
2100 _ => {}
2101 }
2102 }
2103 }
2104
2105 pub fn stop_editing(&mut self) {
2107 self.editing_text = false;
2108 if let Some(item) = self.current_item_mut() {
2109 match item.control {
2110 SettingControl::DualList(ref mut dl) => {
2111 dl.editing = false;
2112 }
2113 SettingControl::Text(ref mut state) => {
2114 state.editing = false;
2115 }
2116 _ => {}
2117 }
2118 }
2119 }
2120
2121 pub fn is_editable_control(&self) -> bool {
2123 self.current_item().is_some_and(|item| {
2124 matches!(
2125 item.control,
2126 SettingControl::TextList(_)
2127 | SettingControl::DualList(_)
2128 | SettingControl::Text(_)
2129 | SettingControl::Map(_)
2130 | SettingControl::Json(_)
2131 )
2132 })
2133 }
2134
2135 pub fn is_editing_json(&self) -> bool {
2137 if !self.editing_text {
2138 return false;
2139 }
2140 self.current_item()
2141 .map(|item| matches!(&item.control, SettingControl::Json(_)))
2142 .unwrap_or(false)
2143 }
2144
2145 pub fn text_insert(&mut self, c: char) {
2147 if let Some(item) = self.current_item_mut() {
2148 match &mut item.control {
2149 SettingControl::TextList(state) => state.insert(c),
2150 SettingControl::Text(state) => state.insert(c),
2151 SettingControl::Map(state) => {
2152 state.new_key_text.insert(state.cursor, c);
2153 state.cursor += c.len_utf8();
2154 }
2155 SettingControl::Json(state) => state.insert(c),
2156 _ => {}
2157 }
2158 }
2159 }
2160
2161 pub fn text_insert_str(&mut self, s: &str) {
2165 if let Some(item) = self.current_item_mut() {
2166 match &mut item.control {
2167 SettingControl::TextList(state) => state.insert_str(s),
2168 SettingControl::Text(state) => state.insert_str(s),
2169 SettingControl::Map(state) => {
2170 for c in s.chars() {
2171 state.new_key_text.insert(state.cursor, c);
2172 state.cursor += c.len_utf8();
2173 }
2174 }
2175 SettingControl::Json(state) => state.insert_str(s),
2176 _ => {}
2177 }
2178 }
2179 }
2180
2181 pub fn paste_into_focused_text(&mut self, text: &str) -> bool {
2188 if let Some(dialog) = self.entry_dialog_mut() {
2189 if dialog.editing_text {
2190 dialog.insert_str(text);
2191 return true;
2192 }
2193 return false;
2194 }
2195 if self.editing_text {
2196 self.text_insert_str(text);
2197 return true;
2198 }
2199 false
2200 }
2201
2202 pub fn text_backspace(&mut self) {
2204 if let Some(item) = self.current_item_mut() {
2205 match &mut item.control {
2206 SettingControl::TextList(state) => state.backspace(),
2207 SettingControl::Text(state) => state.backspace(),
2208 SettingControl::Map(state) => {
2209 if state.cursor > 0 {
2210 let mut char_start = state.cursor - 1;
2211 while char_start > 0 && !state.new_key_text.is_char_boundary(char_start) {
2212 char_start -= 1;
2213 }
2214 state.new_key_text.remove(char_start);
2215 state.cursor = char_start;
2216 }
2217 }
2218 SettingControl::Json(state) => state.backspace(),
2219 _ => {}
2220 }
2221 }
2222 }
2223
2224 pub fn text_move_left(&mut self) {
2226 if let Some(item) = self.current_item_mut() {
2227 match &mut item.control {
2228 SettingControl::TextList(state) => state.move_left(),
2229 SettingControl::Text(state) => state.move_left(),
2230 SettingControl::Map(state) => {
2231 if state.cursor > 0 {
2232 let mut new_pos = state.cursor - 1;
2233 while new_pos > 0 && !state.new_key_text.is_char_boundary(new_pos) {
2234 new_pos -= 1;
2235 }
2236 state.cursor = new_pos;
2237 }
2238 }
2239 SettingControl::Json(state) => state.move_left(),
2240 _ => {}
2241 }
2242 }
2243 }
2244
2245 pub fn text_move_right(&mut self) {
2247 if let Some(item) = self.current_item_mut() {
2248 match &mut item.control {
2249 SettingControl::TextList(state) => state.move_right(),
2250 SettingControl::Text(state) => state.move_right(),
2251 SettingControl::Map(state) => {
2252 if state.cursor < state.new_key_text.len() {
2253 let mut new_pos = state.cursor + 1;
2254 while new_pos < state.new_key_text.len()
2255 && !state.new_key_text.is_char_boundary(new_pos)
2256 {
2257 new_pos += 1;
2258 }
2259 state.cursor = new_pos;
2260 }
2261 }
2262 SettingControl::Json(state) => state.move_right(),
2263 _ => {}
2264 }
2265 }
2266 }
2267
2268 pub fn text_focus_prev(&mut self) {
2270 if let Some(item) = self.current_item_mut() {
2271 match &mut item.control {
2272 SettingControl::TextList(state) => state.focus_prev(),
2273 SettingControl::Map(state) => {
2274 state.focus_prev();
2275 }
2276 _ => {}
2277 }
2278 }
2279 }
2280
2281 pub fn text_focus_next(&mut self) {
2283 if let Some(item) = self.current_item_mut() {
2284 match &mut item.control {
2285 SettingControl::TextList(state) => state.focus_next(),
2286 SettingControl::Map(state) => {
2287 state.focus_next();
2288 }
2289 _ => {}
2290 }
2291 }
2292 }
2293
2294 pub fn text_add_item(&mut self) {
2296 if let Some(item) = self.current_item_mut() {
2297 match &mut item.control {
2298 SettingControl::TextList(state) => state.add_item(),
2299 SettingControl::Map(state) => state.add_entry_from_input(),
2300 _ => {}
2301 }
2302 }
2303 self.on_value_changed();
2305 }
2306
2307 pub fn text_remove_focused(&mut self) {
2309 if let Some(item) = self.current_item_mut() {
2310 match &mut item.control {
2311 SettingControl::TextList(state) => {
2312 if let Some(idx) = state.focused_item {
2313 state.remove_item(idx);
2314 }
2315 }
2316 SettingControl::Map(state) => {
2317 if let Some(idx) = state.focused_entry {
2318 state.remove_entry(idx);
2319 }
2320 }
2321 _ => {}
2322 }
2323 }
2324 self.on_value_changed();
2326 }
2327
2328 pub fn is_editing_dual_list(&self) -> bool {
2330 if !self.editing_text {
2331 return false;
2332 }
2333 self.current_item()
2334 .map(|item| matches!(&item.control, SettingControl::DualList(_)))
2335 .unwrap_or(false)
2336 }
2337
2338 pub fn with_dual_list_mut<R>(
2343 &mut self,
2344 item_idx: usize,
2345 f: impl FnOnce(&mut crate::view::controls::DualListState) -> R,
2346 ) -> Option<R> {
2347 let page = self.pages.get_mut(self.selected_category)?;
2348 let item = page.items.get_mut(item_idx)?;
2349 if let SettingControl::DualList(ref mut state) = item.control {
2350 Some(f(state))
2351 } else {
2352 None
2353 }
2354 }
2355
2356 pub fn with_current_dual_list_mut<R>(
2359 &mut self,
2360 f: impl FnOnce(&mut crate::view::controls::DualListState) -> R,
2361 ) -> Option<R> {
2362 if let Some(item) = self.current_item_mut() {
2363 if let SettingControl::DualList(ref mut state) = item.control {
2364 return Some(f(state));
2365 }
2366 }
2367 None
2368 }
2369
2370 pub fn refresh_dual_list_sibling(&mut self) {
2377 let (new_included, sibling_path) = {
2378 let Some(item) = self.current_item() else {
2379 return;
2380 };
2381 let SettingControl::DualList(state) = &item.control else {
2382 return;
2383 };
2384 let Some(ref sib_path) = item.dual_list_sibling else {
2385 return;
2386 };
2387 (state.included.clone(), sib_path.clone())
2388 };
2389
2390 if let Some(page) = self.pages.get_mut(self.selected_category) {
2392 for other in page.items.iter_mut() {
2393 if other.path == sibling_path {
2394 if let SettingControl::DualList(ref mut sib_state) = other.control {
2395 sib_state.excluded = new_included;
2396 }
2397 break;
2398 }
2399 }
2400 }
2401 }
2402
2403 pub fn json_cursor_up(&mut self) {
2407 if let Some(item) = self.current_item_mut() {
2408 if let SettingControl::Json(state) = &mut item.control {
2409 state.move_up();
2410 }
2411 }
2412 }
2413
2414 pub fn json_cursor_down(&mut self) {
2416 if let Some(item) = self.current_item_mut() {
2417 if let SettingControl::Json(state) = &mut item.control {
2418 state.move_down();
2419 }
2420 }
2421 }
2422
2423 pub fn json_insert_newline(&mut self) {
2425 if let Some(item) = self.current_item_mut() {
2426 if let SettingControl::Json(state) = &mut item.control {
2427 state.insert('\n');
2428 }
2429 }
2430 }
2431
2432 pub fn json_delete(&mut self) {
2434 if let Some(item) = self.current_item_mut() {
2435 if let SettingControl::Json(state) = &mut item.control {
2436 state.delete();
2437 }
2438 }
2439 }
2440
2441 pub fn json_exit_editing(&mut self) {
2443 let is_valid = self
2444 .current_item()
2445 .map(|item| {
2446 if let SettingControl::Json(state) = &item.control {
2447 state.is_valid()
2448 } else {
2449 true
2450 }
2451 })
2452 .unwrap_or(true);
2453
2454 if is_valid {
2455 if let Some(item) = self.current_item_mut() {
2456 if let SettingControl::Json(state) = &mut item.control {
2457 state.commit();
2458 }
2459 }
2460 self.on_value_changed();
2461 } else if let Some(item) = self.current_item_mut() {
2462 if let SettingControl::Json(state) = &mut item.control {
2463 state.revert();
2464 }
2465 }
2466 self.editing_text = false;
2467 }
2468
2469 pub fn json_select_all(&mut self) {
2471 if let Some(item) = self.current_item_mut() {
2472 if let SettingControl::Json(state) = &mut item.control {
2473 state.select_all();
2474 }
2475 }
2476 }
2477
2478 pub fn json_selected_text(&self) -> Option<String> {
2480 if let Some(item) = self.current_item() {
2481 if let SettingControl::Json(state) = &item.control {
2482 return state.selected_text();
2483 }
2484 }
2485 None
2486 }
2487
2488 pub fn json_cursor_up_selecting(&mut self) {
2490 if let Some(item) = self.current_item_mut() {
2491 if let SettingControl::Json(state) = &mut item.control {
2492 state.editor.move_up_selecting();
2493 }
2494 }
2495 }
2496
2497 pub fn json_cursor_down_selecting(&mut self) {
2499 if let Some(item) = self.current_item_mut() {
2500 if let SettingControl::Json(state) = &mut item.control {
2501 state.editor.move_down_selecting();
2502 }
2503 }
2504 }
2505
2506 pub fn json_cursor_left_selecting(&mut self) {
2508 if let Some(item) = self.current_item_mut() {
2509 if let SettingControl::Json(state) = &mut item.control {
2510 state.editor.move_left_selecting();
2511 }
2512 }
2513 }
2514
2515 pub fn json_cursor_right_selecting(&mut self) {
2517 if let Some(item) = self.current_item_mut() {
2518 if let SettingControl::Json(state) = &mut item.control {
2519 state.editor.move_right_selecting();
2520 }
2521 }
2522 }
2523
2524 pub fn is_dropdown_open(&self) -> bool {
2528 self.current_item().is_some_and(|item| {
2529 if let SettingControl::Dropdown(ref d) = item.control {
2530 d.open
2531 } else {
2532 false
2533 }
2534 })
2535 }
2536
2537 pub fn dropdown_toggle(&mut self) {
2539 let mut opened = false;
2540 if let Some(item) = self.current_item_mut() {
2541 if let SettingControl::Dropdown(ref mut d) = item.control {
2542 d.toggle_open();
2543 opened = d.open;
2544 }
2545 }
2546
2547 if opened {
2549 let selected_item = self.selected_item;
2551 let width = self.layout_width;
2552 if let Some(page) = self.pages.get(self.selected_category) {
2553 self.scroll_panel
2555 .ensure_focused_visible(&page.items, selected_item, None, width);
2556 }
2557 }
2558 }
2559
2560 pub fn dropdown_prev(&mut self) {
2562 if let Some(item) = self.current_item_mut() {
2563 if let SettingControl::Dropdown(ref mut d) = item.control {
2564 d.select_prev();
2565 }
2566 }
2567 }
2568
2569 pub fn dropdown_next(&mut self) {
2571 if let Some(item) = self.current_item_mut() {
2572 if let SettingControl::Dropdown(ref mut d) = item.control {
2573 d.select_next();
2574 }
2575 }
2576 }
2577
2578 pub fn dropdown_home(&mut self) {
2580 if let Some(item) = self.current_item_mut() {
2581 if let SettingControl::Dropdown(ref mut d) = item.control {
2582 if !d.options.is_empty() {
2583 d.selected = 0;
2584 d.ensure_visible();
2585 }
2586 }
2587 }
2588 }
2589
2590 pub fn dropdown_end(&mut self) {
2592 if let Some(item) = self.current_item_mut() {
2593 if let SettingControl::Dropdown(ref mut d) = item.control {
2594 if !d.options.is_empty() {
2595 d.selected = d.options.len() - 1;
2596 d.ensure_visible();
2597 }
2598 }
2599 }
2600 }
2601
2602 pub fn dropdown_confirm(&mut self) {
2604 if let Some(item) = self.current_item_mut() {
2605 if let SettingControl::Dropdown(ref mut d) = item.control {
2606 d.confirm();
2607 }
2608 }
2609 self.on_value_changed();
2610 }
2611
2612 pub fn dropdown_cancel(&mut self) {
2614 if let Some(item) = self.current_item_mut() {
2615 if let SettingControl::Dropdown(ref mut d) = item.control {
2616 d.cancel();
2617 }
2618 }
2619 }
2620
2621 pub fn dropdown_select(&mut self, option_idx: usize) {
2623 if let Some(item) = self.current_item_mut() {
2624 if let SettingControl::Dropdown(ref mut d) = item.control {
2625 if option_idx < d.options.len() {
2626 d.selected = option_idx;
2627 d.confirm();
2628 }
2629 }
2630 }
2631 self.on_value_changed();
2632 }
2633
2634 pub fn set_dropdown_hover(&mut self, hover_idx: Option<usize>) -> bool {
2637 if let Some(item) = self.current_item_mut() {
2638 if let SettingControl::Dropdown(ref mut d) = item.control {
2639 if d.open && d.hover_index != hover_idx {
2640 d.hover_index = hover_idx;
2641 return true;
2642 }
2643 }
2644 }
2645 false
2646 }
2647
2648 pub fn dropdown_scroll(&mut self, delta: i32) {
2650 if let Some(item) = self.current_item_mut() {
2651 if let SettingControl::Dropdown(ref mut d) = item.control {
2652 if d.open {
2653 d.scroll_by(delta);
2654 }
2655 }
2656 }
2657 }
2658
2659 pub fn is_number_editing(&self) -> bool {
2663 self.current_item().is_some_and(|item| {
2664 if let SettingControl::Number(ref n) = item.control {
2665 n.editing()
2666 } else {
2667 false
2668 }
2669 })
2670 }
2671
2672 pub fn start_number_editing(&mut self) {
2674 if let Some(item) = self.current_item_mut() {
2675 if let SettingControl::Number(ref mut n) = item.control {
2676 n.start_editing();
2677 }
2678 }
2679 }
2680
2681 pub fn number_insert(&mut self, c: char) {
2683 if let Some(item) = self.current_item_mut() {
2684 if let SettingControl::Number(ref mut n) = item.control {
2685 n.insert_char(c);
2686 }
2687 }
2688 }
2689
2690 pub fn number_backspace(&mut self) {
2692 if let Some(item) = self.current_item_mut() {
2693 if let SettingControl::Number(ref mut n) = item.control {
2694 n.backspace();
2695 }
2696 }
2697 }
2698
2699 pub fn number_confirm(&mut self) {
2701 if let Some(item) = self.current_item_mut() {
2702 if let SettingControl::Number(ref mut n) = item.control {
2703 n.confirm_editing();
2704 }
2705 }
2706 self.on_value_changed();
2707 }
2708
2709 pub fn number_cancel(&mut self) {
2711 if let Some(item) = self.current_item_mut() {
2712 if let SettingControl::Number(ref mut n) = item.control {
2713 n.cancel_editing();
2714 }
2715 }
2716 }
2717
2718 pub fn number_delete(&mut self) {
2720 if let Some(item) = self.current_item_mut() {
2721 if let SettingControl::Number(ref mut n) = item.control {
2722 n.delete();
2723 }
2724 }
2725 }
2726
2727 pub fn number_move_left(&mut self) {
2729 if let Some(item) = self.current_item_mut() {
2730 if let SettingControl::Number(ref mut n) = item.control {
2731 n.move_left();
2732 }
2733 }
2734 }
2735
2736 pub fn number_move_right(&mut self) {
2738 if let Some(item) = self.current_item_mut() {
2739 if let SettingControl::Number(ref mut n) = item.control {
2740 n.move_right();
2741 }
2742 }
2743 }
2744
2745 pub fn number_move_home(&mut self) {
2747 if let Some(item) = self.current_item_mut() {
2748 if let SettingControl::Number(ref mut n) = item.control {
2749 n.move_home();
2750 }
2751 }
2752 }
2753
2754 pub fn number_move_end(&mut self) {
2756 if let Some(item) = self.current_item_mut() {
2757 if let SettingControl::Number(ref mut n) = item.control {
2758 n.move_end();
2759 }
2760 }
2761 }
2762
2763 pub fn number_move_left_selecting(&mut self) {
2765 if let Some(item) = self.current_item_mut() {
2766 if let SettingControl::Number(ref mut n) = item.control {
2767 n.move_left_selecting();
2768 }
2769 }
2770 }
2771
2772 pub fn number_move_right_selecting(&mut self) {
2774 if let Some(item) = self.current_item_mut() {
2775 if let SettingControl::Number(ref mut n) = item.control {
2776 n.move_right_selecting();
2777 }
2778 }
2779 }
2780
2781 pub fn number_move_home_selecting(&mut self) {
2783 if let Some(item) = self.current_item_mut() {
2784 if let SettingControl::Number(ref mut n) = item.control {
2785 n.move_home_selecting();
2786 }
2787 }
2788 }
2789
2790 pub fn number_move_end_selecting(&mut self) {
2792 if let Some(item) = self.current_item_mut() {
2793 if let SettingControl::Number(ref mut n) = item.control {
2794 n.move_end_selecting();
2795 }
2796 }
2797 }
2798
2799 pub fn number_move_word_left(&mut self) {
2801 if let Some(item) = self.current_item_mut() {
2802 if let SettingControl::Number(ref mut n) = item.control {
2803 n.move_word_left();
2804 }
2805 }
2806 }
2807
2808 pub fn number_move_word_right(&mut self) {
2810 if let Some(item) = self.current_item_mut() {
2811 if let SettingControl::Number(ref mut n) = item.control {
2812 n.move_word_right();
2813 }
2814 }
2815 }
2816
2817 pub fn number_move_word_left_selecting(&mut self) {
2819 if let Some(item) = self.current_item_mut() {
2820 if let SettingControl::Number(ref mut n) = item.control {
2821 n.move_word_left_selecting();
2822 }
2823 }
2824 }
2825
2826 pub fn number_move_word_right_selecting(&mut self) {
2828 if let Some(item) = self.current_item_mut() {
2829 if let SettingControl::Number(ref mut n) = item.control {
2830 n.move_word_right_selecting();
2831 }
2832 }
2833 }
2834
2835 pub fn number_select_all(&mut self) {
2837 if let Some(item) = self.current_item_mut() {
2838 if let SettingControl::Number(ref mut n) = item.control {
2839 n.select_all();
2840 }
2841 }
2842 }
2843
2844 pub fn number_delete_word_backward(&mut self) {
2846 if let Some(item) = self.current_item_mut() {
2847 if let SettingControl::Number(ref mut n) = item.control {
2848 n.delete_word_backward();
2849 }
2850 }
2851 }
2852
2853 pub fn number_delete_word_forward(&mut self) {
2855 if let Some(item) = self.current_item_mut() {
2856 if let SettingControl::Number(ref mut n) = item.control {
2857 n.delete_word_forward();
2858 }
2859 }
2860 }
2861
2862 pub fn get_change_descriptions(&self) -> Vec<String> {
2864 let mut descriptions: Vec<String> = self
2865 .pending_changes
2866 .iter()
2867 .map(|(path, value)| {
2868 let value_str = match value {
2869 serde_json::Value::Bool(b) => b.to_string(),
2870 serde_json::Value::Number(n) => n.to_string(),
2871 serde_json::Value::String(s) => format!("\"{}\"", s),
2872 _ => value.to_string(),
2873 };
2874 format!("{}: {}", path, value_str)
2875 })
2876 .collect();
2877 for path in &self.pending_deletions {
2879 descriptions.push(format!("{}: (reset to default)", path));
2880 }
2881 descriptions.sort();
2882 descriptions
2883 }
2884}
2885
2886fn inheritable_fields_for(map_path: &str) -> std::collections::HashSet<String> {
2894 if map_path == "/languages" {
2895 serde_json::to_value(crate::config::EditorConfig::default())
2896 .ok()
2897 .and_then(|v| {
2898 v.as_object().map(|o| {
2899 o.keys()
2900 .cloned()
2901 .collect::<std::collections::HashSet<String>>()
2902 })
2903 })
2904 .unwrap_or_default()
2905 } else {
2906 std::collections::HashSet::new()
2907 }
2908}
2909
2910fn apply_builtin_defaults(dialog: &mut EntryDialogState, entry_pointer: &str) {
2916 let Ok(default_cfg) = serde_json::to_value(Config::default()) else {
2917 return;
2918 };
2919 let Some(entry) = default_cfg.pointer(entry_pointer) else {
2920 return;
2921 };
2922 for item in &mut dialog.items {
2923 if item.path == "__key__" {
2924 continue;
2925 }
2926 let field = item.path.trim_start_matches('/');
2927 if let Some(v) = entry.get(field) {
2928 item.default = Some(v.clone());
2929 }
2930 }
2931}
2932
2933pub(crate) fn update_control_from_value(control: &mut SettingControl, value: &serde_json::Value) {
2934 match control {
2935 SettingControl::Toggle(state) => {
2936 if let Some(b) = value.as_bool() {
2937 state.checked = b;
2938 }
2939 }
2940 SettingControl::Number(state) => {
2941 if let Some(n) = value.as_i64() {
2942 state.value = n;
2943 }
2944 }
2945 SettingControl::Dropdown(state) => {
2946 if let Some(s) = value.as_str() {
2947 if let Some(idx) = state.options.iter().position(|o| o == s) {
2948 state.selected = idx;
2949 }
2950 }
2951 }
2952 SettingControl::Text(state) => {
2953 if let Some(s) = value.as_str() {
2954 state.value = s.to_string();
2955 state.cursor = state.value.len();
2956 }
2957 }
2958 SettingControl::TextList(state) => {
2959 if let Some(arr) = value.as_array() {
2960 state.items = arr
2961 .iter()
2962 .filter_map(|v| {
2963 if state.is_integer {
2964 v.as_i64()
2965 .map(|n| n.to_string())
2966 .or_else(|| v.as_u64().map(|n| n.to_string()))
2967 .or_else(|| v.as_f64().map(|n| n.to_string()))
2968 } else {
2969 v.as_str().map(String::from)
2970 }
2971 })
2972 .collect();
2973 }
2974 }
2975 SettingControl::DualList(state) => {
2976 if let Some(arr) = value.as_array() {
2977 state.included = arr
2978 .iter()
2979 .filter_map(|v| v.as_str().map(String::from))
2980 .collect();
2981 }
2982 }
2983 SettingControl::Map(state) => {
2984 if let Some(obj) = value.as_object() {
2985 state.entries = obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
2986 state.entries.sort_by(|a, b| a.0.cmp(&b.0));
2987 }
2988 }
2989 SettingControl::ObjectArray(state) => {
2990 if let Some(arr) = value.as_array() {
2991 state.bindings = arr.clone();
2992 }
2993 }
2994 SettingControl::Json(state) => {
2995 let json_str =
2997 serde_json::to_string_pretty(value).unwrap_or_else(|_| "null".to_string());
2998 let json_str = if json_str.is_empty() {
2999 "null".to_string()
3000 } else {
3001 json_str
3002 };
3003 state.original_text = json_str.clone();
3004 state.editor.set_value(&json_str);
3005 state.scroll_offset = 0;
3006 }
3007 SettingControl::Complex { .. } => {}
3008 }
3009}
3010
3011#[cfg(test)]
3012mod tests {
3013 use super::*;
3014
3015 const TEST_SCHEMA: &str = r#"
3016{
3017 "type": "object",
3018 "properties": {
3019 "theme": {
3020 "type": "string",
3021 "default": "dark"
3022 },
3023 "line_numbers": {
3024 "type": "boolean",
3025 "default": true
3026 }
3027 },
3028 "$defs": {}
3029}
3030"#;
3031
3032 fn test_config() -> Config {
3033 Config::default()
3034 }
3035
3036 #[test]
3037 fn test_settings_state_creation() {
3038 let config = test_config();
3039 let state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3040
3041 assert!(!state.visible);
3042 assert_eq!(state.selected_category, 0);
3043 assert!(!state.has_changes());
3044 }
3045
3046 #[test]
3047 fn test_navigation() {
3048 let config = test_config();
3049 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3050
3051 assert_eq!(state.focus_panel(), FocusPanel::Categories);
3053
3054 state.toggle_focus();
3056 assert_eq!(state.focus_panel(), FocusPanel::Settings);
3057
3058 state.select_next();
3060 assert_eq!(state.selected_item, 1);
3061
3062 state.select_prev();
3063 assert_eq!(state.selected_item, 0);
3064 }
3065
3066 #[test]
3067 fn test_pending_changes() {
3068 let config = test_config();
3069 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3070
3071 assert!(!state.has_changes());
3072
3073 state.set_pending_change("/theme", serde_json::Value::String("light".to_string()));
3074 assert!(state.has_changes());
3075
3076 state.discard_changes();
3077 assert!(!state.has_changes());
3078 }
3079
3080 #[test]
3081 fn test_show_hide() {
3082 let config = test_config();
3083 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3084
3085 assert!(!state.visible);
3086
3087 state.show();
3088 assert!(state.visible);
3089 assert_eq!(state.focus_panel(), FocusPanel::Categories);
3090
3091 state.hide();
3092 assert!(!state.visible);
3093 }
3094
3095 const TEST_SCHEMA_CONTROLS: &str = r#"
3097{
3098 "type": "object",
3099 "properties": {
3100 "theme": {
3101 "type": "string",
3102 "enum": ["dark", "light", "high-contrast"],
3103 "default": "dark"
3104 },
3105 "tab_size": {
3106 "type": "integer",
3107 "minimum": 1,
3108 "maximum": 8,
3109 "default": 4
3110 },
3111 "line_numbers": {
3112 "type": "boolean",
3113 "default": true
3114 }
3115 },
3116 "$defs": {}
3117}
3118"#;
3119
3120 #[test]
3121 fn test_dropdown_toggle() {
3122 let config = test_config();
3123 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3124 state.show();
3125 state.toggle_focus(); state.select_next();
3130 state.select_next();
3131 assert!(!state.is_dropdown_open());
3132
3133 state.dropdown_toggle();
3134 assert!(state.is_dropdown_open());
3135
3136 state.dropdown_toggle();
3137 assert!(!state.is_dropdown_open());
3138 }
3139
3140 #[test]
3141 fn test_dropdown_cancel_restores() {
3142 let config = test_config();
3143 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3144 state.show();
3145 state.toggle_focus();
3146
3147 state.select_next();
3150 state.select_next();
3151
3152 state.dropdown_toggle();
3154 assert!(state.is_dropdown_open());
3155
3156 let initial = state.current_item().and_then(|item| {
3158 if let SettingControl::Dropdown(ref d) = item.control {
3159 Some(d.selected)
3160 } else {
3161 None
3162 }
3163 });
3164
3165 state.dropdown_next();
3167 let after_change = state.current_item().and_then(|item| {
3168 if let SettingControl::Dropdown(ref d) = item.control {
3169 Some(d.selected)
3170 } else {
3171 None
3172 }
3173 });
3174 assert_ne!(initial, after_change);
3175
3176 state.dropdown_cancel();
3178 assert!(!state.is_dropdown_open());
3179
3180 let after_cancel = state.current_item().and_then(|item| {
3181 if let SettingControl::Dropdown(ref d) = item.control {
3182 Some(d.selected)
3183 } else {
3184 None
3185 }
3186 });
3187 assert_eq!(initial, after_cancel);
3188 }
3189
3190 #[test]
3191 fn test_dropdown_confirm_keeps_selection() {
3192 let config = test_config();
3193 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3194 state.show();
3195 state.toggle_focus();
3196
3197 state.dropdown_toggle();
3199
3200 state.dropdown_next();
3202 let after_change = state.current_item().and_then(|item| {
3203 if let SettingControl::Dropdown(ref d) = item.control {
3204 Some(d.selected)
3205 } else {
3206 None
3207 }
3208 });
3209
3210 state.dropdown_confirm();
3212 assert!(!state.is_dropdown_open());
3213
3214 let after_confirm = state.current_item().and_then(|item| {
3215 if let SettingControl::Dropdown(ref d) = item.control {
3216 Some(d.selected)
3217 } else {
3218 None
3219 }
3220 });
3221 assert_eq!(after_change, after_confirm);
3222 }
3223
3224 #[test]
3225 fn test_number_editing() {
3226 let config = test_config();
3227 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3228 state.show();
3229 state.toggle_focus();
3230
3231 state.select_next();
3233
3234 assert!(!state.is_number_editing());
3236
3237 state.start_number_editing();
3239 assert!(state.is_number_editing());
3240
3241 state.number_insert('8');
3243
3244 state.number_confirm();
3246 assert!(!state.is_number_editing());
3247 }
3248
3249 #[test]
3250 fn test_number_cancel_editing() {
3251 let config = test_config();
3252 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3253 state.show();
3254 state.toggle_focus();
3255
3256 state.select_next();
3258
3259 let initial_value = state.current_item().and_then(|item| {
3261 if let SettingControl::Number(ref n) = item.control {
3262 Some(n.value)
3263 } else {
3264 None
3265 }
3266 });
3267
3268 state.start_number_editing();
3270 state.number_backspace();
3271 state.number_insert('9');
3272 state.number_insert('9');
3273
3274 state.number_cancel();
3276 assert!(!state.is_number_editing());
3277
3278 let after_cancel = state.current_item().and_then(|item| {
3280 if let SettingControl::Number(ref n) = item.control {
3281 Some(n.value)
3282 } else {
3283 None
3284 }
3285 });
3286 assert_eq!(initial_value, after_cancel);
3287 }
3288
3289 #[test]
3290 fn test_number_backspace() {
3291 let config = test_config();
3292 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3293 state.show();
3294 state.toggle_focus();
3295 state.select_next();
3296
3297 state.start_number_editing();
3298 state.number_backspace();
3299
3300 let display_text = state.current_item().and_then(|item| {
3302 if let SettingControl::Number(ref n) = item.control {
3303 Some(n.display_text())
3304 } else {
3305 None
3306 }
3307 });
3308 assert_eq!(display_text, Some(String::new()));
3310
3311 state.number_cancel();
3312 }
3313
3314 #[test]
3315 fn test_layer_selection() {
3316 let config = test_config();
3317 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3318
3319 assert_eq!(state.target_layer, ConfigLayer::User);
3321 assert_eq!(state.target_layer_name(), "User");
3322
3323 state.cycle_target_layer();
3325 assert_eq!(state.target_layer, ConfigLayer::Project);
3326 assert_eq!(state.target_layer_name(), "Project");
3327
3328 state.cycle_target_layer();
3329 assert_eq!(state.target_layer, ConfigLayer::Session);
3330 assert_eq!(state.target_layer_name(), "Session");
3331
3332 state.cycle_target_layer();
3333 assert_eq!(state.target_layer, ConfigLayer::User);
3334
3335 state.set_target_layer(ConfigLayer::Project);
3337 assert_eq!(state.target_layer, ConfigLayer::Project);
3338
3339 state.set_target_layer(ConfigLayer::System);
3341 assert_eq!(state.target_layer, ConfigLayer::Project);
3342 }
3343
3344 #[test]
3345 fn test_layer_switch_clears_pending_changes() {
3346 let config = test_config();
3347 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3348
3349 state.set_pending_change("/theme", serde_json::Value::String("light".to_string()));
3351 assert!(state.has_changes());
3352
3353 state.cycle_target_layer();
3355 assert!(!state.has_changes());
3356 }
3357
3358 #[test]
3377 fn nested_array_save_records_full_entry_path() {
3378 use crate::view::settings::schema::SettingType;
3381
3382 let config = test_config();
3383 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3384
3385 let item_schema = SettingSchema {
3387 path: "/item".to_string(),
3388 name: "Server".to_string(),
3389 description: None,
3390 setting_type: SettingType::Object {
3391 properties: vec![SettingSchema {
3392 path: "/enabled".to_string(),
3393 name: "Enabled".to_string(),
3394 description: None,
3395 setting_type: SettingType::Boolean,
3396 default: Some(serde_json::json!(false)),
3397 read_only: false,
3398 section: None,
3399 order: None,
3400 nullable: false,
3401 enum_from: None,
3402 dual_list_sibling: None,
3403 dynamically_extendable_status_bar_elements: false,
3404 }],
3405 },
3406 default: None,
3407 read_only: false,
3408 section: None,
3409 order: None,
3410 nullable: false,
3411 enum_from: None,
3412 dual_list_sibling: None,
3413 dynamically_extendable_status_bar_elements: false,
3414 };
3415
3416 let value_schema = SettingSchema {
3421 path: String::new(),
3422 name: "value".to_string(),
3423 description: None,
3424 setting_type: SettingType::ObjectArray {
3425 item_schema: Box::new(item_schema.clone()),
3426 display_field: None,
3427 },
3428 default: None,
3429 read_only: false,
3430 section: None,
3431 order: None,
3432 nullable: false,
3433 enum_from: None,
3434 dual_list_sibling: None,
3435 dynamically_extendable_status_bar_elements: false,
3436 };
3437
3438 let parent = EntryDialogState::from_schema(
3442 "quicklsp".to_string(),
3443 &serde_json::json!([{ "enabled": true }]),
3444 &value_schema,
3445 "/universal_lsp",
3446 false, false,
3448 &HashMap::new(),
3449 );
3450
3451 assert!(
3453 parent.is_single_value,
3454 "array value_schema should trigger is_single_value path"
3455 );
3456 assert_eq!(parent.entry_path(), "/universal_lsp/quicklsp");
3457
3458 state.entry_dialog_stack.push(parent);
3459
3460 state.open_nested_entry_dialog();
3465
3466 assert_eq!(
3468 state.entry_dialog_stack.len(),
3469 2,
3470 "open_nested_entry_dialog should have pushed a nested dialog"
3471 );
3472
3473 let nested_map_path = state
3476 .entry_dialog_stack
3477 .last()
3478 .map(|d| d.map_path.clone())
3479 .unwrap();
3480 assert_eq!(
3481 nested_map_path, "/universal_lsp/quicklsp",
3482 "BUG: nested dialog's map_path dropped the 'quicklsp' key segment"
3483 );
3484
3485 state.save_entry_dialog();
3487
3488 assert_eq!(state.entry_dialog_stack.len(), 1);
3490
3491 assert!(
3494 !state.pending_changes.contains_key("/universal_lsp/"),
3495 "regression: pending change recorded under empty-key path /universal_lsp/. \
3496 All keys: {:?}",
3497 state.pending_changes.keys().collect::<Vec<_>>()
3498 );
3499 assert!(
3500 !state
3501 .pending_changes
3502 .keys()
3503 .any(|k| k.starts_with("/universal_lsp") && k.ends_with('/')),
3504 "no /universal_lsp/* path should end in a trailing slash; got {:?}",
3505 state.pending_changes.keys().collect::<Vec<_>>()
3506 );
3507 assert!(
3508 state
3509 .pending_changes
3510 .contains_key("/universal_lsp/quicklsp"),
3511 "expected pending change at /universal_lsp/quicklsp, got {:?}",
3512 state.pending_changes.keys().collect::<Vec<_>>()
3513 );
3514 }
3515
3516 #[test]
3517 fn test_refresh_dual_list_sibling_updates_excluded() {
3518 use crate::view::controls::DualListState;
3519
3520 let schema = include_str!("../../../plugins/config-schema.json");
3523 let config = test_config();
3524 let mut state = SettingsState::new(schema, &config).unwrap();
3525
3526 let editor_page_idx = state
3528 .pages
3529 .iter()
3530 .position(|p| p.path == "/editor")
3531 .expect("editor page");
3532 state.selected_category = editor_page_idx;
3533
3534 let (left_idx, right_idx) = {
3535 let page = &state.pages[editor_page_idx];
3536 let l = page
3537 .items
3538 .iter()
3539 .position(|i| i.path == "/editor/status_bar/left")
3540 .expect("left item");
3541 let r = page
3542 .items
3543 .iter()
3544 .position(|i| i.path == "/editor/status_bar/right")
3545 .expect("right item");
3546 (l, r)
3547 };
3548
3549 assert!(matches!(
3551 &state.pages[editor_page_idx].items[left_idx].control,
3552 SettingControl::DualList(_)
3553 ));
3554
3555 let default_right_items: Vec<String> =
3557 match &state.pages[editor_page_idx].items[right_idx].control {
3558 SettingControl::DualList(dl) => dl.included.clone(),
3559 _ => panic!("right should be DualList"),
3560 };
3561 let initial_left_excluded: Vec<String> =
3562 match &state.pages[editor_page_idx].items[left_idx].control {
3563 SettingControl::DualList(dl) => dl.excluded.clone(),
3564 _ => panic!("left should be DualList"),
3565 };
3566 assert_eq!(
3567 initial_left_excluded, default_right_items,
3568 "left.excluded should mirror right's included on initial build"
3569 );
3570
3571 let new_element = "{chord}".to_string();
3573 state.selected_item = left_idx;
3574 state
3575 .with_current_dual_list_mut(|dl: &mut DualListState| {
3576 if !dl.included.contains(&new_element) {
3577 dl.included.push(new_element.clone());
3578 }
3579 })
3580 .expect("current item is a DualList");
3581
3582 state.refresh_dual_list_sibling();
3584
3585 match &state.pages[editor_page_idx].items[right_idx].control {
3586 SettingControl::DualList(dl) => {
3587 assert!(
3588 dl.excluded.contains(&new_element),
3589 "right.excluded should be updated to reflect left's new inclusion"
3590 );
3591 }
3592 _ => panic!("right should be DualList"),
3593 }
3594 }
3595
3596 #[test]
3597 fn test_with_dual_list_mut_returns_none_for_non_dual_list() {
3598 let config = test_config();
3599 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3600
3601 let result = state.with_dual_list_mut(0, |_| ());
3603 assert!(result.is_none());
3604 }
3605}