lex_analysis/
references.rs1use 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}