irongit

perf: stop cloning entire json repeatedly on jv

huncholanehuncholaneauthored
parent bd69b57commit b8a8ebb7ee60371d9445035d9e9cc253923f5352Browse files

1 file changed, +113 -31

+113-31src/views/browser.rs
@@ -111,34 +111,46 @@ impl BrowserView {
111111 /// files: all parsed files. The browser treats them as the root level of the tree.
112112 /// jq operates on whichever file is currently entered (first path segment = file index).
113113 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-
122114 let key = files.len() as u64
123115 ^ files.iter().map(|(n, _)| n.len() as u64).sum::<u64>() << 8;
124116 if self.cache_key != key {
125117 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+ }
129124 }
130125 self.sync_jq_from_path();
131126 }
132127
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)
136133 } 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)
138143 };
139144
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);
142154
143155 if let Some(key) = self.restore_key.take() {
144156 if let Some(idx) = current_entries.iter().position(|e| e.label == key) {
@@ -150,20 +162,25 @@ impl BrowserView {
150162 self.selection = current_entries.len() - 1;
151163 }
152164
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+ };
156177
157178 // --- 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+ };
167184 if let Some(val) = jq_value {
168185 self.show_jq_bar(ui, val);
169186 } else {
@@ -196,7 +213,16 @@ impl BrowserView {
196213 if action == crate::widgets::MillerAction::Enter {
197214 if let Some(entry) = current_entries.get(self.selection) {
198215 if entry.is_container {
199- self.enter_selected(current, &current_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, &current_entries);
225+ }
200226 }
201227 }
202228 }
@@ -293,7 +319,15 @@ impl BrowserView {
293319 self.selection = idx;
294320 if let Some(entry) = current_entries.get(idx) {
295321 if entry.is_container {
296- self.enter_selected(current, &current_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, &current_entries);
330+ }
297331 }
298332 }
299333 }
@@ -940,6 +974,54 @@ impl BrowserView {
940974
941975 // --- Helper functions ---
942976
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+
9431025 fn resolve_path<'a>(
9441026 root: &'a serde_json::Value,
9451027 path: &[PathSegment],