1use crate::{
2 config::SymbolFilter,
3 fetch::CachePolicy,
4 state::{HierarchyDirection, LoadState},
5};
6
7use super::{App, HierarchyLoadRequest};
8
9impl App {
10 pub fn set_symbol_filter(&mut self, symbol_filter: SymbolFilter) {
11 self.symbol_filter = symbol_filter;
12 }
13
14 pub fn reload_symbol_filter(
15 &mut self,
16 symbol_filter: SymbolFilter,
17 hierarchy_available: bool,
18 ) -> Vec<HierarchyLoadRequest> {
19 self.symbol_filter = symbol_filter;
20 let mut targets = Vec::new();
21 for node_id in self.graph.known_graph().nodes {
22 let Some(node) = self.graph.node(node_id) else {
23 continue;
24 };
25 for direction in [HierarchyDirection::Incoming, HierarchyDirection::Outgoing] {
26 if matches!(
27 node.branch(direction).load_state,
28 LoadState::Loaded | LoadState::Loading
29 ) {
30 targets.push((node_id, node.identity(), direction));
31 }
32 }
33 }
34
35 if !hierarchy_available {
36 self.canvas_notice = Some(
37 "Project config reloaded; graph refresh requires an analysis provider".to_owned(),
38 );
39 return Vec::new();
40 }
41
42 let requests = targets
43 .into_iter()
44 .filter_map(|(node_id, identity, direction)| {
45 self.begin_hierarchy_load(node_id, identity, direction, CachePolicy::Refresh, false)
46 })
47 .collect::<Vec<_>>();
48 self.canvas_notice = Some(if requests.is_empty() {
49 "Project config reloaded; no loaded graph branches to refresh".to_owned()
50 } else {
51 format!(
52 "Project config reloaded; refreshing {} graph branches",
53 requests.len()
54 )
55 });
56 requests
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use clap::Parser;
63
64 use super::super::App;
65 use crate::{
66 cli::Cli,
67 config::SymbolFilter,
68 fetch::{FetchSource, HierarchyResponse},
69 state::{HierarchyDirection, HierarchyKind, NodeId, SourceLocation, SymbolIdentity},
70 };
71
72 #[test]
73 fn reloads_filters_across_loaded_branches_and_supersedes_old_requests() {
74 let mut app = App::from_cli(Cli::try_parse_from(["cgraph", "call", "root"]).unwrap());
75 for direction in [HierarchyDirection::Incoming, HierarchyDirection::Outgoing] {
76 let request = app.toggle_selected_branch(direction, true).unwrap();
77 assert!(app.finish_hierarchy(&request, Ok(response(&request, Vec::new()))));
78 }
79 let stale = app.refresh_selected_branches(true);
80
81 let current =
82 app.reload_symbol_filter(SymbolFilter::from_patterns(["noise"]).unwrap(), true);
83
84 assert_eq!(current.len(), 2);
85 assert!(current.iter().all(|request| {
86 stale
87 .iter()
88 .find(|stale| stale.query.direction == request.query.direction)
89 .is_some_and(|stale| stale.request_id != request.request_id)
90 }));
91 assert!(!app.finish_hierarchy(&stale[0], Ok(response(&stale[0], vec![identity("stale")]))));
92 for request in ¤t {
93 assert!(app.finish_hierarchy(
94 request,
95 Ok(response(request, vec![identity("noise"), identity("keep")]))
96 ));
97 }
98 let root = app.selected.unwrap();
99 assert_eq!(
100 branch_names(&app, root, HierarchyDirection::Incoming),
101 ["keep"]
102 );
103 assert_eq!(
104 branch_names(&app, root, HierarchyDirection::Outgoing),
105 ["keep"]
106 );
107 assert!(app.graph.is_anchor(root));
108 }
109
110 fn response(
111 request: &super::HierarchyLoadRequest,
112 children: Vec<SymbolIdentity>,
113 ) -> HierarchyResponse {
114 HierarchyResponse {
115 query: request.query.clone(),
116 children,
117 source: FetchSource::Lsp,
118 }
119 }
120
121 fn identity(symbol: &str) -> SymbolIdentity {
122 SymbolIdentity {
123 symbol: symbol.to_owned(),
124 kind: HierarchyKind::Call,
125 location: Some(SourceLocation {
126 uri: format!("file:///workspace/{symbol}.rs"),
127 line: Some(0),
128 character: Some(0),
129 }),
130 }
131 }
132
133 fn branch_names(app: &App, node_id: NodeId, direction: HierarchyDirection) -> Vec<&str> {
134 app.graph
135 .node(node_id)
136 .unwrap()
137 .branch(direction)
138 .neighbors
139 .iter()
140 .map(|neighbor| app.graph.node(*neighbor).unwrap().symbol.as_str())
141 .collect()
142 }
143}