Skip to main content

logicaffeine_lsp/
call_hierarchy.rs

1//! Call hierarchy over the `SymbolIndex`'s call sites: who calls this
2//! function, and what does it call. `## Main` callers have no hierarchy item
3//! (Main is the program, not a function) — their calls simply don't appear
4//! as incoming edges.
5
6use tower_lsp::lsp_types::{
7    CallHierarchyIncomingCall, CallHierarchyItem, CallHierarchyOutgoingCall, Position, Range,
8    SymbolKind, Url,
9};
10
11use crate::document::DocumentState;
12use crate::index::DefinitionKind;
13
14pub fn prepare(
15    doc: &DocumentState,
16    position: Position,
17    uri: &Url,
18) -> Option<Vec<CallHierarchyItem>> {
19    let offset = doc.line_index.offset(position);
20    let def_idx = doc
21        .symbol_index
22        .references
23        .iter()
24        .find(|r| r.span.start <= offset && offset < r.span.end)
25        .and_then(|r| r.definition_idx)
26        .or_else(|| {
27            doc.symbol_index
28                .definitions
29                .iter()
30                .position(|d| d.span.start <= offset && offset < d.span.end)
31        })?;
32    if doc.symbol_index.definitions[def_idx].kind != DefinitionKind::Function {
33        return None;
34    }
35    Some(vec![item_for(doc, def_idx, uri)])
36}
37
38pub fn incoming_calls(
39    doc: &DocumentState,
40    item: &CallHierarchyItem,
41    uri: &Url,
42) -> Vec<CallHierarchyIncomingCall> {
43    let Some(callee) = function_index_by_name(doc, &item.name) else {
44        return Vec::new();
45    };
46
47    // Group call sites by calling function, preserving first-seen order.
48    let mut callers: Vec<(usize, Vec<Range>)> = Vec::new();
49    for site in &doc.symbol_index.call_sites {
50        if site.callee != callee {
51            continue;
52        }
53        let Some(caller) = site.caller else { continue };
54        let range = to_range(doc, site.span);
55        match callers.iter_mut().find(|(ix, _)| *ix == caller) {
56            Some((_, ranges)) => ranges.push(range),
57            None => callers.push((caller, vec![range])),
58        }
59    }
60
61    callers
62        .into_iter()
63        .map(|(caller, from_ranges)| CallHierarchyIncomingCall {
64            from: item_for(doc, caller, uri),
65            from_ranges,
66        })
67        .collect()
68}
69
70pub fn outgoing_calls(
71    doc: &DocumentState,
72    item: &CallHierarchyItem,
73    uri: &Url,
74) -> Vec<CallHierarchyOutgoingCall> {
75    let Some(caller) = function_index_by_name(doc, &item.name) else {
76        return Vec::new();
77    };
78
79    let mut callees: Vec<(usize, Vec<Range>)> = Vec::new();
80    for site in &doc.symbol_index.call_sites {
81        if site.caller != Some(caller) {
82            continue;
83        }
84        let range = to_range(doc, site.span);
85        match callees.iter_mut().find(|(ix, _)| *ix == site.callee) {
86            Some((_, ranges)) => ranges.push(range),
87            None => callees.push((site.callee, vec![range])),
88        }
89    }
90
91    callees
92        .into_iter()
93        .map(|(callee, from_ranges)| CallHierarchyOutgoingCall {
94            to: item_for(doc, callee, uri),
95            from_ranges,
96        })
97        .collect()
98}
99
100fn function_index_by_name(doc: &DocumentState, name: &str) -> Option<usize> {
101    doc.symbol_index.name_to_defs.get(name)?.iter().copied().find(|&ix| {
102        doc.symbol_index.definitions[ix].kind == DefinitionKind::Function
103    })
104}
105
106fn item_for(doc: &DocumentState, def_idx: usize, uri: &Url) -> CallHierarchyItem {
107    let def = &doc.symbol_index.definitions[def_idx];
108    let range = to_range(doc, def.span);
109    CallHierarchyItem {
110        name: def.name.clone(),
111        kind: SymbolKind::FUNCTION,
112        tags: None,
113        detail: def.detail.clone(),
114        uri: uri.clone(),
115        range,
116        selection_range: range,
117        data: None,
118    }
119}
120
121fn to_range(doc: &DocumentState, span: logicaffeine_language::token::Span) -> Range {
122    Range {
123        start: doc.line_index.position(span.start),
124        end: doc.line_index.position(span.end),
125    }
126}