Skip to main content

cgraph/app/
config.rs

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 &current {
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}