feat: session based focus list
3 files changed, +67 -17
+30-17src/app.rs
| @@ -259,6 +259,7 @@ impl JvApp { | ||
| 259 | 259 | }; |
| 260 | 260 | if let Some(loaded) = &app.current_session { |
| 261 | 261 | app.applied_threshold = loaded.session.jaccard_threshold; |
| 262 | + app.browser_view.load_focus_list(&loaded.session.focus_list); | |
| 262 | 263 | } |
| 263 | 264 | tracing::info!("UI loaded"); |
| 264 | 265 | app |
| @@ -351,6 +352,7 @@ impl JvApp { | ||
| 351 | 352 | if ui.selectable_label(name == ¤t_name, name).clicked() { |
| 352 | 353 | let session = self.session_manager.sessions[i].clone(); |
| 353 | 354 | self.session_manager.save_last_session_id(&session.id); |
| 355 | + self.browser_view.load_focus_list(&session.focus_list); | |
| 354 | 356 | self.current_session = Some(LoadedSession::new(session)); |
| 355 | 357 | self.selected_file_index = 0; |
| 356 | 358 | self.rebuild_schema(); |
| @@ -889,27 +891,38 @@ impl JvApp { | ||
| 889 | 891 | return; |
| 890 | 892 | } |
| 891 | 893 | |
| 892 | - let loaded = self.current_session.as_ref().unwrap(); | |
| 893 | - let file_idx = self.selected_file_index.min(loaded.parsed_files.len().saturating_sub(1)); | |
| 894 | - let file_count = loaded.parsed_files.len(); | |
| 895 | - | |
| 896 | - // Invalidate only when files are added/removed (not on selection change) | |
| 897 | - if file_count != self.last_file_count { | |
| 898 | - self.browser_view.invalidate(); | |
| 899 | - self.code_view.invalidate(); | |
| 900 | - self.schema_diagram_view.invalidate(); | |
| 901 | - self.last_file_count = file_count; | |
| 894 | + { | |
| 895 | + let loaded = self.current_session.as_ref().unwrap(); | |
| 896 | + let file_idx = self.selected_file_index.min(loaded.parsed_files.len().saturating_sub(1)); | |
| 897 | + let file_count = loaded.parsed_files.len(); | |
| 898 | + | |
| 899 | + if file_count != self.last_file_count { | |
| 900 | + self.browser_view.invalidate(); | |
| 901 | + self.code_view.invalidate(); | |
| 902 | + self.schema_diagram_view.invalidate(); | |
| 903 | + self.last_file_count = file_count; | |
| 904 | + } | |
| 905 | + self.last_file_index = file_idx; | |
| 906 | + | |
| 907 | + self.browser_view.show(ui, &loaded.parsed_files); | |
| 902 | 908 | } |
| 903 | - self.last_file_index = file_idx; | |
| 904 | 909 | |
| 905 | - self.browser_view.show(ui, &loaded.parsed_files); | |
| 910 | + // Save focus list if it changed | |
| 911 | + if self.browser_view.take_focus_dirty() { | |
| 912 | + if let Some(loaded) = &mut self.current_session { | |
| 913 | + loaded.session.focus_list = self.browser_view.save_focus_list(); | |
| 914 | + self.session_manager.update_session(&loaded.session); | |
| 915 | + } | |
| 916 | + } | |
| 906 | 917 | |
| 907 | 918 | // Sync sidebar selection from browser's current file |
| 908 | - if let Some(file_key) = self.browser_view.current_file_key(&loaded.parsed_files) { | |
| 909 | - if let Some(idx) = loaded.parsed_files.iter().position(|(n, _)| { | |
| 910 | - n.strip_suffix(".json").unwrap_or(n) == file_key | |
| 911 | - }) { | |
| 912 | - self.selected_file_index = idx; | |
| 919 | + if let Some(loaded) = &self.current_session { | |
| 920 | + if let Some(file_key) = self.browser_view.current_file_key(&loaded.parsed_files) { | |
| 921 | + if let Some(idx) = loaded.parsed_files.iter().position(|(n, _)| { | |
| 922 | + n.strip_suffix(".json").unwrap_or(n) == file_key | |
| 923 | + }) { | |
| 924 | + self.selected_file_index = idx; | |
| 925 | + } | |
| 913 | 926 | } |
| 914 | 927 | } |
| 915 | 928 | } |
+3-0src/session.rs
| @@ -27,6 +27,8 @@ pub struct Session { | ||
| 27 | 27 | pub enum_conversions: Vec<EnumConversion>, |
| 28 | 28 | #[serde(default)] |
| 29 | 29 | pub hidden_fields: Vec<String>, // "StructName.field_name" format |
| 30 | + #[serde(default)] | |
| 31 | + pub focus_list: Vec<Vec<String>>, // saved focus paths for jv browser | |
| 30 | 32 | } |
| 31 | 33 | |
| 32 | 34 | fn default_jaccard() -> f32 { |
| @@ -141,6 +143,7 @@ impl Session { | ||
| 141 | 143 | jaccard_threshold: default_jaccard(), |
| 142 | 144 | enum_conversions: Vec::new(), |
| 143 | 145 | hidden_fields: Vec::new(), |
| 146 | + focus_list: Vec::new(), | |
| 144 | 147 | } |
| 145 | 148 | } |
| 146 | 149 | } |
+34-0src/views/browser.rs
| @@ -54,6 +54,7 @@ pub struct BrowserView { | ||
| 54 | 54 | // Focus list: full paths to pinned entries (e.g. ["activity_Aug.responseData.status"]) |
| 55 | 55 | focused: Vec<Vec<PathSegment>>, |
| 56 | 56 | focus_mode: bool, |
| 57 | + focus_dirty: bool, | |
| 57 | 58 | } |
| 58 | 59 | |
| 59 | 60 | impl BrowserView { |
| @@ -73,6 +74,7 @@ impl BrowserView { | ||
| 73 | 74 | cache_key: 0, |
| 74 | 75 | focused: Vec::new(), |
| 75 | 76 | focus_mode: false, |
| 77 | + focus_dirty: false, | |
| 76 | 78 | } |
| 77 | 79 | } |
| 78 | 80 | |
| @@ -112,6 +114,37 @@ impl BrowserView { | ||
| 112 | 114 | self.sync_jq_from_path(); |
| 113 | 115 | } |
| 114 | 116 | |
| 117 | + /// Load focus list from session data. | |
| 118 | + pub fn load_focus_list(&mut self, paths: &[Vec<String>]) { | |
| 119 | + self.focused = paths.iter().map(|segs| { | |
| 120 | + segs.iter().map(|s| { | |
| 121 | + if let Some(stripped) = s.strip_prefix('[').and_then(|s| s.strip_suffix(']')) { | |
| 122 | + if let Ok(i) = stripped.parse::<usize>() { | |
| 123 | + return PathSegment::Index(i); | |
| 124 | + } | |
| 125 | + } | |
| 126 | + PathSegment::Key(s.clone()) | |
| 127 | + }).collect() | |
| 128 | + }).collect(); | |
| 129 | + } | |
| 130 | + | |
| 131 | + /// Save focus list as session-serializable data. | |
| 132 | + pub fn save_focus_list(&self) -> Vec<Vec<String>> { | |
| 133 | + self.focused.iter().map(|path| { | |
| 134 | + path.iter().map(|seg| match seg { | |
| 135 | + PathSegment::Key(k) => k.clone(), | |
| 136 | + PathSegment::Index(i) => format!("[{}]", i), | |
| 137 | + }).collect() | |
| 138 | + }).collect() | |
| 139 | + } | |
| 140 | + | |
| 141 | + /// Returns true if the focus list was modified since last check, and clears the flag. | |
| 142 | + pub fn take_focus_dirty(&mut self) -> bool { | |
| 143 | + let dirty = self.focus_dirty; | |
| 144 | + self.focus_dirty = false; | |
| 145 | + dirty | |
| 146 | + } | |
| 147 | + | |
| 115 | 148 | pub fn invalidate(&mut self) { |
| 116 | 149 | self.cache_key = 0; |
| 117 | 150 | self.path.clear(); |
| @@ -322,6 +355,7 @@ impl BrowserView { | ||
| 322 | 355 | } else { |
| 323 | 356 | self.focused.push(full_path); |
| 324 | 357 | } |
| 358 | + self.focus_dirty = true; | |
| 325 | 359 | } |
| 326 | 360 | } |
| 327 | 361 | // 'F' (shift-f) toggles focus mode |