Skip to main content

lex_analysis/
references.rs

1use crate::inline::extract_references;
2use crate::reference_targets::{
3    targets_from_annotation, targets_from_definition, targets_from_reference_type,
4    targets_from_session, ReferenceTarget,
5};
6use crate::utils::{
7    find_annotation_at_position, find_definition_at_position, find_definitions_by_subject,
8    find_session_at_position, find_sessions_by_identifier, for_each_text_content,
9    reference_at_position,
10};
11use lex_core::lex::ast::traits::AstNode;
12use lex_core::lex::ast::{Document, Position, Range};
13
14pub fn find_references(
15    document: &Document,
16    position: Position,
17    include_declaration: bool,
18) -> Vec<Range> {
19    let targets = determine_targets(document, position);
20    if targets.is_empty() {
21        return Vec::new();
22    }
23
24    let mut ranges = Vec::new();
25    if include_declaration {
26        ranges.extend(declaration_ranges(document, &targets));
27    }
28    ranges.extend(reference_occurrences(document, &targets));
29    dedup_ranges(&mut ranges);
30    ranges
31}
32
33fn determine_targets(document: &Document, position: Position) -> Vec<ReferenceTarget> {
34    if let Some(reference) = reference_at_position(document, position) {
35        let targets = targets_from_reference_type(&reference.reference_type);
36        if !targets.is_empty() {
37            return targets;
38        }
39    }
40
41    if let Some(annotation) = find_annotation_at_position(document, position) {
42        let targets = targets_from_annotation(annotation);
43        if !targets.is_empty() {
44            return targets;
45        }
46    }
47
48    if let Some(definition) = find_definition_at_position(document, position) {
49        let targets = targets_from_definition(definition);
50        if !targets.is_empty() {
51            return targets;
52        }
53    }
54
55    if let Some(session) = find_session_at_position(document, position) {
56        let targets = targets_from_session(session);
57        if !targets.is_empty() {
58            return targets;
59        }
60    }
61
62    Vec::new()
63}
64
65fn declaration_ranges(document: &Document, targets: &[ReferenceTarget]) -> Vec<Range> {
66    let mut ranges = Vec::new();
67    for target in targets {
68        match target {
69            ReferenceTarget::AnnotationLabel(label) => {
70                for annotation in document.find_annotations_by_label(label) {
71                    ranges.push(annotation.header_location().clone());
72                }
73            }
74            ReferenceTarget::CitationKey(key) => {
75                let annotations = document.find_annotations_by_label(key);
76                if annotations.is_empty() {
77                    ranges.extend(definition_ranges(document, key));
78                } else {
79                    for annotation in annotations {
80                        ranges.push(annotation.header_location().clone());
81                    }
82                }
83            }
84            ReferenceTarget::DefinitionSubject(subject) => {
85                ranges.extend(definition_ranges(document, subject));
86            }
87            ReferenceTarget::Session(identifier) => {
88                for session in find_sessions_by_identifier(document, identifier) {
89                    if let Some(header) = session.header_location() {
90                        ranges.push(header.clone());
91                    } else {
92                        ranges.push(session.range().clone());
93                    }
94                }
95            }
96        }
97    }
98    ranges
99}
100
101fn definition_ranges(document: &Document, subject: &str) -> Vec<Range> {
102    find_definitions_by_subject(document, subject)
103        .into_iter()
104        .map(|definition| {
105            definition
106                .header_location()
107                .cloned()
108                .unwrap_or_else(|| definition.range().clone())
109        })
110        .collect()
111}
112
113pub fn reference_occurrences(document: &Document, targets: &[ReferenceTarget]) -> Vec<Range> {
114    let mut matches = Vec::new();
115    for_each_text_content(document, &mut |text| {
116        for reference in extract_references(text) {
117            if targets
118                .iter()
119                .any(|target| reference_matches(&reference.reference_type, target))
120            {
121                matches.push(reference.range);
122            }
123        }
124    });
125    matches
126}
127
128fn reference_matches(
129    reference: &lex_core::lex::inlines::ReferenceType,
130    target: &ReferenceTarget,
131) -> bool {
132    use lex_core::lex::inlines::ReferenceType;
133    match (reference, target) {
134        (
135            ReferenceType::AnnotationReference { label },
136            ReferenceTarget::AnnotationLabel(expected),
137        ) => label.eq_ignore_ascii_case(expected),
138        (ReferenceType::FootnoteNumber { number }, ReferenceTarget::AnnotationLabel(expected)) => {
139            expected == &number.to_string()
140        }
141        (ReferenceType::Citation(data), ReferenceTarget::CitationKey(key)) => data
142            .keys
143            .iter()
144            .any(|candidate| candidate.eq_ignore_ascii_case(key)),
145        (ReferenceType::Citation(data), ReferenceTarget::AnnotationLabel(label)) => data
146            .keys
147            .iter()
148            .any(|candidate| candidate.eq_ignore_ascii_case(label)),
149        (ReferenceType::General { target: value }, ReferenceTarget::DefinitionSubject(subject)) => {
150            normalize(value) == normalize(subject)
151        }
152        (
153            ReferenceType::ToCome {
154                identifier: Some(value),
155            },
156            ReferenceTarget::DefinitionSubject(subject),
157        ) => normalize(value) == normalize(subject),
158        (ReferenceType::Session { target }, ReferenceTarget::Session(identifier)) => {
159            target.eq_ignore_ascii_case(identifier)
160        }
161        _ => false,
162    }
163}
164
165fn normalize(text: &str) -> String {
166    text.trim().to_ascii_lowercase()
167}
168
169fn dedup_ranges(ranges: &mut Vec<Range>) {
170    ranges.sort_by_key(|range| (range.span.start, range.span.end));
171    ranges.dedup_by(|a, b| a.span == b.span && a.start == b.start && a.end == b.end);
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use lex_core::lex::parsing;
178
179    fn fixture() -> (Document, String) {
180        let source = r#":: note ::
181    Something.
182
183Cache:
184    Definition body.
185
1861. Intro
187
188    First reference [Cache].
189    Second reference [Cache] and annotation [::note].
190"#;
191        let document = parsing::parse_document(source).expect("fixture parses");
192        (document, source.to_string())
193    }
194
195    fn position_of(source: &str, needle: &str) -> Position {
196        let offset = source
197            .find(needle)
198            .unwrap_or_else(|| panic!("needle not found: {needle}"));
199        let mut line = 0;
200        let mut col = 0;
201        for ch in source[..offset].chars() {
202            if ch == '\n' {
203                line += 1;
204                col = 0;
205            } else {
206                col += ch.len_utf8();
207            }
208        }
209        Position::new(line, col)
210    }
211
212    #[test]
213    fn finds_references_from_usage() {
214        let (document, source) = fixture();
215        let position = position_of(&source, "Cache]");
216        let ranges = find_references(&document, position, false);
217        assert_eq!(ranges.len(), 2);
218    }
219
220    #[test]
221    fn finds_references_from_definition() {
222        let (document, source) = fixture();
223        let position = position_of(&source, "Cache:");
224        let ranges = find_references(&document, position, false);
225        assert_eq!(ranges.len(), 2);
226    }
227
228    #[test]
229    fn includes_declaration_when_requested() {
230        let (document, source) = fixture();
231        let position = position_of(&source, "Cache:");
232        let ranges = find_references(&document, position, true);
233        assert!(ranges.len() >= 3);
234        let definition_header = document
235            .root
236            .children
237            .iter()
238            .find_map(|item| match item {
239                lex_core::lex::ast::ContentItem::Definition(def) => def
240                    .header_location()
241                    .cloned()
242                    .or_else(|| Some(def.range().clone())),
243                _ => None,
244            })
245            .expect("definition header available");
246        assert!(ranges.contains(&definition_header));
247    }
248
249    #[test]
250    fn finds_annotation_references() {
251        let (document, source) = fixture();
252        let position = position_of(&source, "::note]");
253        let ranges = find_references(&document, position, false);
254        assert_eq!(ranges.len(), 1);
255        assert!(ranges[0].contains(position));
256    }
257}