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