Skip to content

Commit 6001fb7

Browse files
authored
Merge pull request #35 from dmackdev/refactor-path-ids
Refactor JSON path ID tracking to reduce allocations
2 parents 3512dd9 + df871bf commit 6001fb7

File tree

3 files changed

+93
-74
lines changed

3 files changed

+93
-74
lines changed

src/node.rs

+32-58
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::collections::{HashMap, HashSet};
1+
use std::collections::HashSet;
22

33
use egui::{
44
collapsing_header::{paint_default_icon, CollapsingState},
@@ -41,30 +41,32 @@ impl<'a, T: ToJsonTreeValue> JsonTreeNode<'a, T> {
4141
) -> JsonTreeResponse {
4242
let persistent_id = ui.id();
4343
let tree_id = self.id;
44-
let make_persistent_id = |path_segments: &Vec<JsonPointerSegment>| {
45-
persistent_id.with(tree_id.with(path_segments))
46-
};
44+
let make_persistent_id =
45+
|path_segments: &[JsonPointerSegment]| persistent_id.with(tree_id.with(path_segments));
4746

4847
let style = config.style.unwrap_or_default();
4948
let default_expand = config.default_expand.unwrap_or_default();
5049

51-
let mut path_id_map = HashMap::new();
50+
let mut reset_path_ids = HashSet::new();
5251

5352
let (default_expand, search_term) = match default_expand {
5453
DefaultExpand::All => (InnerExpand::All, None),
5554
DefaultExpand::None => (InnerExpand::None, None),
5655
DefaultExpand::ToLevel(l) => (InnerExpand::ToLevel(l), None),
5756
DefaultExpand::SearchResults(search_str) => {
58-
// If searching, the entire path_id_map must be populated.
59-
populate_path_id_map(self.value, &mut path_id_map, &make_persistent_id);
6057
let search_term = SearchTerm::parse(search_str);
61-
let paths = search_term
58+
let search_match_path_ids = search_term
6259
.as_ref()
6360
.map(|search_term| {
64-
search_term.find_matching_paths_in(self.value, style.abbreviate_root)
61+
search_term.find_matching_paths_in(
62+
self.value,
63+
style.abbreviate_root,
64+
&make_persistent_id,
65+
&mut reset_path_ids,
66+
)
6567
})
6668
.unwrap_or_default();
67-
(InnerExpand::Paths(paths), search_term)
69+
(InnerExpand::Paths(search_match_path_ids), search_term)
6870
}
6971
};
7072

@@ -85,25 +87,25 @@ impl<'a, T: ToJsonTreeValue> JsonTreeNode<'a, T> {
8587
self.show_impl(
8688
ui,
8789
&mut vec![],
88-
&mut path_id_map,
90+
&mut reset_path_ids,
8991
&make_persistent_id,
9092
&node_config,
9193
&mut renderer,
9294
);
9395
});
9496

9597
JsonTreeResponse {
96-
collapsing_state_ids: path_id_map.into_values().collect(),
98+
collapsing_state_ids: reset_path_ids,
9799
}
98100
}
99101

100102
fn show_impl<'b>(
101103
self,
102104
ui: &mut Ui,
103105
path_segments: &'b mut Vec<JsonPointerSegment<'a>>,
104-
path_id_map: &'b mut PathIdMap<'a>,
105-
make_persistent_id: &'b dyn Fn(&Vec<JsonPointerSegment>) -> Id,
106-
config: &'b JsonTreeNodeConfig<'a>,
106+
reset_path_ids: &'b mut HashSet<Id>,
107+
make_persistent_id: &'b dyn Fn(&[JsonPointerSegment]) -> Id,
108+
config: &'b JsonTreeNodeConfig,
107109
renderer: &'b mut JsonTreeRenderer<'a, T>,
108110
) {
109111
match self.value.to_json_tree_value() {
@@ -155,7 +157,7 @@ impl<'a, T: ToJsonTreeValue> JsonTreeNode<'a, T> {
155157
show_expandable(
156158
ui,
157159
path_segments,
158-
path_id_map,
160+
reset_path_ids,
159161
expandable,
160162
&make_persistent_id,
161163
config,
@@ -169,10 +171,10 @@ impl<'a, T: ToJsonTreeValue> JsonTreeNode<'a, T> {
169171
fn show_expandable<'a, 'b, T: ToJsonTreeValue>(
170172
ui: &mut Ui,
171173
path_segments: &'b mut Vec<JsonPointerSegment<'a>>,
172-
path_id_map: &'b mut PathIdMap<'a>,
174+
reset_path_ids: &'b mut HashSet<Id>,
173175
expandable: Expandable<'a, T>,
174-
make_persistent_id: &'b dyn Fn(&Vec<JsonPointerSegment>) -> Id,
175-
config: &'b JsonTreeNodeConfig<'a>,
176+
make_persistent_id: &'b dyn Fn(&[JsonPointerSegment]) -> Id,
177+
config: &'b JsonTreeNodeConfig,
176178
renderer: &'b mut JsonTreeRenderer<'a, T>,
177179
) {
178180
let JsonTreeNodeConfig {
@@ -186,18 +188,17 @@ fn show_expandable<'a, 'b, T: ToJsonTreeValue>(
186188
ExpandableType::Object => &OBJECT_DELIMITERS,
187189
};
188190

191+
let path_id = make_persistent_id(path_segments);
192+
reset_path_ids.insert(path_id);
193+
189194
let default_open = match &default_expand {
190195
InnerExpand::All => true,
191196
InnerExpand::None => false,
192197
InnerExpand::ToLevel(num_levels_open) => (path_segments.len() as u8) <= *num_levels_open,
193-
InnerExpand::Paths(paths) => paths.contains(path_segments),
198+
InnerExpand::Paths(search_match_path_ids) => search_match_path_ids.contains(&path_id),
194199
};
195200

196-
let id_source = *path_id_map
197-
.entry(path_segments.to_vec())
198-
.or_insert_with(|| make_persistent_id(path_segments));
199-
200-
let mut state = CollapsingState::load_with_default_open(ui.ctx(), id_source, default_open);
201+
let mut state = CollapsingState::load_with_default_open(ui.ctx(), path_id, default_open);
201202
let is_expanded = state.is_open();
202203

203204
let header_res = ui.horizontal_wrapped(|ui| {
@@ -402,7 +403,7 @@ fn show_expandable<'a, 'b, T: ToJsonTreeValue>(
402403
nested_tree.show_impl(
403404
ui,
404405
path_segments,
405-
path_id_map,
406+
reset_path_ids,
406407
make_persistent_id,
407408
config,
408409
renderer,
@@ -420,7 +421,7 @@ fn show_expandable<'a, 'b, T: ToJsonTreeValue>(
420421
ui.spacing_mut().indent /= 2.0;
421422
}
422423

423-
ui.indent(id_source, add_nested_tree);
424+
ui.indent(path_id, add_nested_tree);
424425
});
425426
}
426427

@@ -447,18 +448,18 @@ fn show_expandable<'a, 'b, T: ToJsonTreeValue>(
447448
}
448449
}
449450

450-
struct JsonTreeNodeConfig<'a> {
451-
default_expand: InnerExpand<'a>,
451+
struct JsonTreeNodeConfig {
452+
default_expand: InnerExpand,
452453
style: JsonTreeStyle,
453454
search_term: Option<SearchTerm>,
454455
}
455456

456457
#[derive(Debug, Clone)]
457-
enum InnerExpand<'a> {
458+
enum InnerExpand {
458459
All,
459460
None,
460461
ToLevel(u8),
461-
Paths(HashSet<Vec<JsonPointerSegment<'a>>>),
462+
Paths(HashSet<Id>),
462463
}
463464

464465
struct Expandable<'a, T: ToJsonTreeValue> {
@@ -468,30 +469,3 @@ struct Expandable<'a, T: ToJsonTreeValue> {
468469
expandable_type: ExpandableType,
469470
parent: Option<JsonPointerSegment<'a>>,
470471
}
471-
472-
type PathIdMap<'a> = HashMap<Vec<JsonPointerSegment<'a>>, Id>;
473-
474-
fn populate_path_id_map<'a, 'b, T: ToJsonTreeValue>(
475-
value: &'a T,
476-
path_id_map: &'b mut PathIdMap<'a>,
477-
make_persistent_id: &'b dyn Fn(&Vec<JsonPointerSegment<'a>>) -> Id,
478-
) {
479-
populate_path_id_map_impl(value, &mut vec![], path_id_map, make_persistent_id);
480-
}
481-
482-
fn populate_path_id_map_impl<'a, 'b, T: ToJsonTreeValue>(
483-
value: &'a T,
484-
path_segments: &'b mut Vec<JsonPointerSegment<'a>>,
485-
path_id_map: &'b mut PathIdMap<'a>,
486-
make_persistent_id: &'b dyn Fn(&Vec<JsonPointerSegment<'a>>) -> Id,
487-
) {
488-
if let JsonTreeValue::Expandable(entries, _) = value.to_json_tree_value() {
489-
for (property, val) in entries {
490-
let id = make_persistent_id(path_segments);
491-
path_id_map.insert(path_segments.clone(), id);
492-
path_segments.push(property);
493-
populate_path_id_map_impl(val, path_segments, path_id_map, make_persistent_id);
494-
path_segments.pop();
495-
}
496-
}
497-
}

src/search.rs

+41-16
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use std::collections::HashSet;
22

3+
use egui::Id;
4+
35
use crate::{
46
pointer::JsonPointerSegment,
57
value::{ExpandableType, JsonTreeValue, ToJsonTreeValue},
@@ -29,21 +31,30 @@ impl SearchTerm {
2931
self.0.len()
3032
}
3133

32-
pub(crate) fn find_matching_paths_in<'a, T: ToJsonTreeValue>(
34+
pub(crate) fn find_matching_paths_in<T: ToJsonTreeValue>(
3335
&self,
34-
value: &'a T,
36+
value: &T,
3537
abbreviate_root: bool,
36-
) -> HashSet<Vec<JsonPointerSegment<'a>>> {
37-
let mut matching_paths = HashSet::new();
38+
make_persistent_id: &dyn Fn(&[JsonPointerSegment]) -> Id,
39+
reset_path_ids: &mut HashSet<Id>,
40+
) -> HashSet<Id> {
41+
let mut search_match_path_ids = HashSet::new();
3842

39-
search_impl(value, self, &mut vec![], &mut matching_paths);
43+
search_impl(
44+
value,
45+
self,
46+
&mut vec![],
47+
&mut search_match_path_ids,
48+
make_persistent_id,
49+
reset_path_ids,
50+
);
4051

41-
if !abbreviate_root && matching_paths.len() == 1 {
52+
if !abbreviate_root && search_match_path_ids.len() == 1 {
4253
// The only match was a top level key or value - no need to expand anything.
43-
matching_paths.clear();
54+
search_match_path_ids.clear();
4455
}
4556

46-
matching_paths
57+
search_match_path_ids
4758
}
4859

4960
fn matches<V: ToString + ?Sized>(&self, other: &V) -> bool {
@@ -55,35 +66,49 @@ fn search_impl<'a, T: ToJsonTreeValue>(
5566
value: &'a T,
5667
search_term: &SearchTerm,
5768
path_segments: &mut Vec<JsonPointerSegment<'a>>,
58-
matching_paths: &mut HashSet<Vec<JsonPointerSegment<'a>>>,
69+
search_match_path_ids: &mut HashSet<Id>,
70+
make_persistent_id: &dyn Fn(&[JsonPointerSegment]) -> Id,
71+
reset_path_ids: &mut HashSet<Id>,
5972
) {
6073
match value.to_json_tree_value() {
6174
JsonTreeValue::Base(_, display_value, _) => {
6275
if search_term.matches(display_value) {
63-
update_matches(path_segments, matching_paths);
76+
update_matches(path_segments, search_match_path_ids, make_persistent_id);
6477
}
6578
}
6679
JsonTreeValue::Expandable(entries, expandable_type) => {
6780
for (property, val) in entries.iter() {
6881
path_segments.push(*property);
6982

83+
if val.is_expandable() {
84+
reset_path_ids.insert(make_persistent_id(path_segments));
85+
}
86+
7087
// Ignore matches for indices in an array.
7188
if expandable_type == ExpandableType::Object && search_term.matches(property) {
72-
update_matches(path_segments, matching_paths);
89+
update_matches(path_segments, search_match_path_ids, make_persistent_id);
7390
}
7491

75-
search_impl(*val, search_term, path_segments, matching_paths);
92+
search_impl(
93+
*val,
94+
search_term,
95+
path_segments,
96+
search_match_path_ids,
97+
make_persistent_id,
98+
reset_path_ids,
99+
);
76100
path_segments.pop();
77101
}
78102
}
79103
};
80104
}
81105

82-
fn update_matches<'a>(
83-
path_segments: &[JsonPointerSegment<'a>],
84-
matching_paths: &mut HashSet<Vec<JsonPointerSegment<'a>>>,
106+
fn update_matches(
107+
path_segments: &[JsonPointerSegment],
108+
search_match_path_ids: &mut HashSet<Id>,
109+
make_persistent_id: &dyn Fn(&[JsonPointerSegment]) -> Id,
85110
) {
86111
for i in 0..path_segments.len() {
87-
matching_paths.insert(path_segments[0..i].to_vec());
112+
search_match_path_ids.insert(make_persistent_id(&path_segments[0..i]));
88113
}
89114
}

src/tree.rs

+20
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,23 @@ impl<'a, T: ToJsonTreeValue> JsonTree<'a, T> {
9898
JsonTreeNode::new(self.id, self.value).show_with_config(ui, self.config)
9999
}
100100
}
101+
102+
#[cfg(test)]
103+
mod test {
104+
use crate::DefaultExpand;
105+
106+
use super::JsonTree;
107+
108+
#[test]
109+
fn test_search_populates_all_collapsing_state_ids_in_response() {
110+
let value = serde_json::json!({"foo": [1, 2, [3]], "bar": { "qux" : false, "thud": { "a/b": [4, 5, { "m~n": "Greetings!" }]}, "grep": 21}, "baz": null});
111+
112+
egui::__run_test_ui(|ui| {
113+
let response = JsonTree::new("id", &value)
114+
.default_expand(DefaultExpand::SearchResults("g"))
115+
.show(ui);
116+
117+
assert_eq!(response.collapsing_state_ids.len(), 7);
118+
});
119+
}
120+
}

0 commit comments

Comments
 (0)