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        (ReferenceType::FootnoteLabeled { label }, ReferenceTarget::AnnotationLabel(expected)) => {
135            label.eq_ignore_ascii_case(expected)
136        }
137        (ReferenceType::FootnoteNumber { number }, ReferenceTarget::AnnotationLabel(expected)) => {
138            expected == &number.to_string()
139        }
140        (ReferenceType::Citation(data), ReferenceTarget::CitationKey(key)) => data
141            .keys
142            .iter()
143            .any(|candidate| candidate.eq_ignore_ascii_case(key)),
144        (ReferenceType::Citation(data), ReferenceTarget::AnnotationLabel(label)) => data
145            .keys
146            .iter()
147            .any(|candidate| candidate.eq_ignore_ascii_case(label)),
148        (ReferenceType::General { target: value }, ReferenceTarget::DefinitionSubject(subject)) => {
149            normalize(value) == normalize(subject)
150        }
151        (
152            ReferenceType::ToCome {
153                identifier: Some(value),
154            },
155            ReferenceTarget::DefinitionSubject(subject),
156        ) => normalize(value) == normalize(subject),
157        (ReferenceType::Session { target }, ReferenceTarget::Session(identifier)) => {
158            target.eq_ignore_ascii_case(identifier)
159        }
160        _ => false,
161    }
162}
163
164fn normalize(text: &str) -> String {
165    text.trim().to_ascii_lowercase()
166}
167
168fn dedup_ranges(ranges: &mut Vec<Range>) {
169    ranges.sort_by_key(|range| (range.span.start, range.span.end));
170    ranges.dedup_by(|a, b| a.span == b.span && a.start == b.start && a.end == b.end);
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use lex_core::lex::parsing;
177
178    fn fixture() -> (Document, String) {
179        let source = r#":: note ::
180    Something.
181
182Cache:
183    Definition body.
184
1851. Intro
186
187    First reference [Cache].
188    Second reference [Cache] and footnote [^note].
189"#;
190        let document = parsing::parse_document(source).expect("fixture parses");
191        (document, source.to_string())
192    }
193
194    fn position_of(source: &str, needle: &str) -> Position {
195        let offset = source
196            .find(needle)
197            .unwrap_or_else(|| panic!("needle not found: {needle}"));
198        let mut line = 0;
199        let mut col = 0;
200        for ch in source[..offset].chars() {
201            if ch == '\n' {
202                line += 1;
203                col = 0;
204            } else {
205                col += ch.len_utf8();
206            }
207        }
208        Position::new(line, col)
209    }
210
211    #[test]
212    fn finds_references_from_usage() {
213        let (document, source) = fixture();
214        let position = position_of(&source, "Cache]");
215        let ranges = find_references(&document, position, false);
216        assert_eq!(ranges.len(), 2);
217    }
218
219    #[test]
220    fn finds_references_from_definition() {
221        let (document, source) = fixture();
222        let position = position_of(&source, "Cache:");
223        let ranges = find_references(&document, position, false);
224        assert_eq!(ranges.len(), 2);
225    }
226
227    #[test]
228    fn includes_declaration_when_requested() {
229        let (document, source) = fixture();
230        let position = position_of(&source, "Cache:");
231        let ranges = find_references(&document, position, true);
232        assert!(ranges.len() >= 3);
233        let definition_header = document
234            .root
235            .children
236            .iter()
237            .find_map(|item| match item {
238                lex_core::lex::ast::ContentItem::Definition(def) => def
239                    .header_location()
240                    .cloned()
241                    .or_else(|| Some(def.range().clone())),
242                _ => None,
243            })
244            .expect("definition header available");
245        assert!(ranges.contains(&definition_header));
246    }
247
248    #[test]
249    fn finds_annotation_references() {
250        let (document, source) = fixture();
251        let position = position_of(&source, "^note]");
252        let ranges = find_references(&document, position, false);
253        assert_eq!(ranges.len(), 1);
254        assert!(ranges[0].contains(position));
255    }
256}