Skip to main content

lex_analysis/
references.rs

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