perf: stop cloning entire json repeatedly on jv
1 file changed, +113 -31
+113-31src/views/browser.rs
| @@ -111,34 +111,46 @@ impl BrowserView { | ||
| 111 | 111 | /// files: all parsed files. The browser treats them as the root level of the tree. |
| 112 | 112 | /// jq operates on whichever file is currently entered (first path segment = file index). |
| 113 | 113 | pub fn show(&mut self, ui: &mut Ui, files: &[(String, serde_json::Value)]) { |
| 114 | - // Build a virtual root: an Object keyed by filename | |
| 115 | - let virtual_root = serde_json::Value::Object( | |
| 116 | - files.iter().map(|(name, val)| { | |
| 117 | - let display = name.strip_suffix(".json").unwrap_or(name).to_string(); | |
| 118 | - (display, val.clone()) | |
| 119 | - }).collect() | |
| 120 | - ); | |
| 121 | - | |
| 122 | 114 | let key = files.len() as u64 |
| 123 | 115 | ^ files.iter().map(|(n, _)| n.len() as u64).sum::<u64>() << 8; |
| 124 | 116 | if self.cache_key != key { |
| 125 | 117 | self.cache_key = key; |
| 126 | - if resolve_path(&virtual_root, &self.path).is_none() { | |
| 127 | - self.path.clear(); | |
| 128 | - self.selection = 0; | |
| 118 | + // Validate path still resolves | |
| 119 | + if !self.path.is_empty() { | |
| 120 | + if resolve_from_files(files, &self.path).is_none() { | |
| 121 | + self.path.clear(); | |
| 122 | + self.selection = 0; | |
| 123 | + } | |
| 129 | 124 | } |
| 130 | 125 | self.sync_jq_from_path(); |
| 131 | 126 | } |
| 132 | 127 | |
| 133 | - let current = resolve_path(&virtual_root, &self.path).unwrap_or(&virtual_root); | |
| 134 | - let parent = if self.path.is_empty() { | |
| 135 | - None | |
| 128 | + // Build entries for current + parent level (no cloning) | |
| 129 | + let (current_entries, parent_entries) = if self.path.is_empty() { | |
| 130 | + // Root: file list | |
| 131 | + let entries = build_file_entries(files); | |
| 132 | + (entries, None) | |
| 136 | 133 | } else { |
| 137 | - resolve_path(&virtual_root, &self.path[..self.path.len() - 1]) | |
| 134 | + let current = resolve_from_files(files, &self.path); | |
| 135 | + let parent = if self.path.len() == 1 { | |
| 136 | + // Parent is the file list | |
| 137 | + Some(build_file_entries(files)) | |
| 138 | + } else { | |
| 139 | + resolve_from_files(files, &self.path[..self.path.len() - 1]) | |
| 140 | + .map(build_entries) | |
| 141 | + }; | |
| 142 | + (current.map(build_entries).unwrap_or_default(), parent) | |
| 138 | 143 | }; |
| 139 | 144 | |
| 140 | - let current_entries = build_entries(current); | |
| 141 | - let parent_entries = parent.map(|p| build_entries(p)); | |
| 145 | + // Resolve current JSON value for child lookups | |
| 146 | + let current_value = if self.path.is_empty() { | |
| 147 | + None | |
| 148 | + } else { | |
| 149 | + resolve_from_files(files, &self.path) | |
| 150 | + }; | |
| 151 | + // Fallback for APIs that need a &Value (enter_selected, child_value, render) | |
| 152 | + let empty_obj = serde_json::Value::Object(Default::default()); | |
| 153 | + let current = current_value.unwrap_or(&empty_obj); | |
| 142 | 154 | |
| 143 | 155 | if let Some(key) = self.restore_key.take() { |
| 144 | 156 | if let Some(idx) = current_entries.iter().position(|e| e.label == key) { |
| @@ -150,20 +162,25 @@ impl BrowserView { | ||
| 150 | 162 | self.selection = current_entries.len() - 1; |
| 151 | 163 | } |
| 152 | 164 | |
| 153 | - let selected_child = current_entries | |
| 154 | - .get(self.selection) | |
| 155 | - .and_then(|e| child_value(current, self.selection, &e.label)); | |
| 165 | + let selected_child = if let Some(cv) = current_value { | |
| 166 | + // Inside a file — look up child by key/index | |
| 167 | + current_entries.get(self.selection) | |
| 168 | + .and_then(|e| child_value(cv, self.selection, &e.label)) | |
| 169 | + } else { | |
| 170 | + // Root file list — selected entry is a file, look it up directly | |
| 171 | + current_entries.get(self.selection) | |
| 172 | + .and_then(|e| files.iter().find(|(n, _)| { | |
| 173 | + n.strip_suffix(".json").unwrap_or(n) == e.label | |
| 174 | + })) | |
| 175 | + .map(|(_, v)| v) | |
| 176 | + }; | |
| 156 | 177 | |
| 157 | 178 | // --- jq bar (operates on the selected file, not the virtual root) --- |
| 158 | - // The first path segment selects the file; jq runs on that file's value | |
| 159 | - let jq_value = self.path.first().and_then(|seg| { | |
| 160 | - match seg { | |
| 161 | - PathSegment::Key(k) => files.iter().find(|(n, _)| { | |
| 162 | - n.strip_suffix(".json").unwrap_or(n) == k | |
| 163 | - }).map(|(_, v)| v), | |
| 164 | - _ => None, | |
| 165 | - } | |
| 166 | - }); | |
| 179 | + let jq_value = if !self.path.is_empty() { | |
| 180 | + resolve_from_files(files, &self.path[..1]) | |
| 181 | + } else { | |
| 182 | + None | |
| 183 | + }; | |
| 167 | 184 | if let Some(val) = jq_value { |
| 168 | 185 | self.show_jq_bar(ui, val); |
| 169 | 186 | } else { |
| @@ -196,7 +213,16 @@ impl BrowserView { | ||
| 196 | 213 | if action == crate::widgets::MillerAction::Enter { |
| 197 | 214 | if let Some(entry) = current_entries.get(self.selection) { |
| 198 | 215 | if entry.is_container { |
| 199 | - self.enter_selected(current, ¤t_entries); | |
| 216 | + if self.path.is_empty() { | |
| 217 | + // Root: enter a file by name | |
| 218 | + self.path.push(PathSegment::Key(entry.label.clone())); | |
| 219 | + self.selection = 0; | |
| 220 | + self.scroll_to_selection = true; | |
| 221 | + self.jq_synced = true; | |
| 222 | + self.sync_jq_from_path(); | |
| 223 | + } else { | |
| 224 | + self.enter_selected(current, ¤t_entries); | |
| 225 | + } | |
| 200 | 226 | } |
| 201 | 227 | } |
| 202 | 228 | } |
| @@ -293,7 +319,15 @@ impl BrowserView { | ||
| 293 | 319 | self.selection = idx; |
| 294 | 320 | if let Some(entry) = current_entries.get(idx) { |
| 295 | 321 | if entry.is_container { |
| 296 | - self.enter_selected(current, ¤t_entries); | |
| 322 | + if self.path.is_empty() { | |
| 323 | + self.path.push(PathSegment::Key(entry.label.clone())); | |
| 324 | + self.selection = 0; | |
| 325 | + self.scroll_to_selection = true; | |
| 326 | + self.jq_synced = true; | |
| 327 | + self.sync_jq_from_path(); | |
| 328 | + } else { | |
| 329 | + self.enter_selected(current, ¤t_entries); | |
| 330 | + } | |
| 297 | 331 | } |
| 298 | 332 | } |
| 299 | 333 | } |
| @@ -940,6 +974,54 @@ impl BrowserView { | ||
| 940 | 974 | |
| 941 | 975 | // --- Helper functions --- |
| 942 | 976 | |
| 977 | +/// Resolve a path starting from the file list. path[0] is a file display name, | |
| 978 | +/// remaining segments navigate into that file's JSON value. No cloning. | |
| 979 | +fn resolve_from_files<'a>( | |
| 980 | + files: &'a [(String, serde_json::Value)], | |
| 981 | + path: &[PathSegment], | |
| 982 | +) -> Option<&'a serde_json::Value> { | |
| 983 | + if path.is_empty() { | |
| 984 | + return None; | |
| 985 | + } | |
| 986 | + // First segment is the file name | |
| 987 | + let file_key = match &path[0] { | |
| 988 | + PathSegment::Key(k) => k, | |
| 989 | + _ => return None, | |
| 990 | + }; | |
| 991 | + let (_, root) = files.iter().find(|(n, _)| { | |
| 992 | + n.strip_suffix(".json").unwrap_or(n) == file_key | |
| 993 | + })?; | |
| 994 | + // Navigate remaining segments into the file's JSON | |
| 995 | + let mut current = root; | |
| 996 | + for seg in &path[1..] { | |
| 997 | + match seg { | |
| 998 | + PathSegment::Key(k) => { | |
| 999 | + current = current.as_object()?.get(k)?; | |
| 1000 | + } | |
| 1001 | + PathSegment::Index(i) => { | |
| 1002 | + current = current.as_array()?.get(*i)?; | |
| 1003 | + } | |
| 1004 | + } | |
| 1005 | + } | |
| 1006 | + Some(current) | |
| 1007 | +} | |
| 1008 | + | |
| 1009 | +/// Build entries for the root file list (no JSON cloning). | |
| 1010 | +fn build_file_entries(files: &[(String, serde_json::Value)]) -> Vec<Entry> { | |
| 1011 | + files.iter().map(|(name, val)| { | |
| 1012 | + let display = name.strip_suffix(".json").unwrap_or(name); | |
| 1013 | + let (icon, color) = type_icon_color(val); | |
| 1014 | + Entry { | |
| 1015 | + label: display.to_string(), | |
| 1016 | + type_icon: icon, | |
| 1017 | + type_label: type_label(val), | |
| 1018 | + preview: value_preview(val), | |
| 1019 | + color, | |
| 1020 | + is_container: true, | |
| 1021 | + } | |
| 1022 | + }).collect() | |
| 1023 | +} | |
| 1024 | + | |
| 943 | 1025 | fn resolve_path<'a>( |
| 944 | 1026 | root: &'a serde_json::Value, |
| 945 | 1027 | path: &[PathSegment], |