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 target_resolves(document: &Document, target: &ReferenceTarget) -> bool {
122 match target {
128 ReferenceTarget::AnnotationLabel(label) => annotation_label_exists(document, label.trim()),
129 ReferenceTarget::CitationKey(key) => {
130 let trimmed = key.trim();
131 annotation_label_exists(document, trimmed)
132 || !find_definitions_by_subject(document, trimmed).is_empty()
133 }
134 ReferenceTarget::DefinitionSubject(subject) => {
135 !find_definitions_by_subject(document, subject.trim()).is_empty()
136 }
137 ReferenceTarget::Session(identifier) => {
138 !find_sessions_by_identifier(document, identifier.trim()).is_empty()
139 }
140 }
141}
142
143fn annotation_label_exists(document: &Document, label: &str) -> bool {
149 let needle = label.trim();
150 document
151 .annotations()
152 .iter()
153 .chain(document.root.iter_annotations_recursive())
154 .any(|ann| ann.data.label.value.trim().eq_ignore_ascii_case(needle))
155}
156
157pub fn reference_occurrences(document: &Document, targets: &[ReferenceTarget]) -> Vec<Range> {
158 let mut matches = Vec::new();
159 for_each_text_content(document, &mut |text| {
160 for reference in extract_references(text) {
161 if targets
162 .iter()
163 .any(|target| reference_matches(&reference.reference_type, target))
164 {
165 matches.push(reference.range);
166 }
167 }
168 });
169 matches
170}
171
172fn reference_matches(
173 reference: &lex_core::lex::inlines::ReferenceType,
174 target: &ReferenceTarget,
175) -> bool {
176 use lex_core::lex::inlines::ReferenceType;
177 match (reference, target) {
178 (
179 ReferenceType::AnnotationReference { label },
180 ReferenceTarget::AnnotationLabel(expected),
181 ) => label.eq_ignore_ascii_case(expected),
182 (ReferenceType::FootnoteNumber { number }, ReferenceTarget::AnnotationLabel(expected)) => {
183 expected == &number.to_string()
184 }
185 (ReferenceType::Citation(data), ReferenceTarget::CitationKey(key)) => data
186 .keys
187 .iter()
188 .any(|candidate| candidate.eq_ignore_ascii_case(key)),
189 (ReferenceType::Citation(data), ReferenceTarget::AnnotationLabel(label)) => data
190 .keys
191 .iter()
192 .any(|candidate| candidate.eq_ignore_ascii_case(label)),
193 (ReferenceType::General { target: value }, ReferenceTarget::DefinitionSubject(subject)) => {
194 normalize(value) == normalize(subject)
195 }
196 (
197 ReferenceType::ToCome {
198 identifier: Some(value),
199 },
200 ReferenceTarget::DefinitionSubject(subject),
201 ) => normalize(value) == normalize(subject),
202 (ReferenceType::Session { target }, ReferenceTarget::Session(identifier)) => {
203 target.eq_ignore_ascii_case(identifier)
204 }
205 _ => false,
206 }
207}
208
209fn normalize(text: &str) -> String {
210 text.trim().to_ascii_lowercase()
211}
212
213fn dedup_ranges(ranges: &mut Vec<Range>) {
214 ranges.sort_by_key(|range| (range.span.start, range.span.end));
215 ranges.dedup_by(|a, b| a.span == b.span && a.start == b.start && a.end == b.end);
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221 use lex_core::lex::parsing;
222
223 fn fixture() -> (Document, String) {
224 let source = r#":: test.note ::
225 Something.
226
227Cache:
228 Definition body.
229
2301. Intro
231
232 First reference [Cache].
233 Second reference [Cache] and annotation [::note].
234"#;
235 let document = parsing::parse_document(source).expect("fixture parses");
236 (document, source.to_string())
237 }
238
239 fn position_of(source: &str, needle: &str) -> Position {
240 let offset = source
241 .find(needle)
242 .unwrap_or_else(|| panic!("needle not found: {needle}"));
243 let mut line = 0;
244 let mut col = 0;
245 for ch in source[..offset].chars() {
246 if ch == '\n' {
247 line += 1;
248 col = 0;
249 } else {
250 col += ch.len_utf8();
251 }
252 }
253 Position::new(line, col)
254 }
255
256 #[test]
257 fn finds_references_from_usage() {
258 let (document, source) = fixture();
259 let position = position_of(&source, "Cache]");
260 let ranges = find_references(&document, position, false);
261 assert_eq!(ranges.len(), 2);
262 }
263
264 #[test]
265 fn finds_references_from_definition() {
266 let (document, source) = fixture();
267 let position = position_of(&source, "Cache:");
268 let ranges = find_references(&document, position, false);
269 assert_eq!(ranges.len(), 2);
270 }
271
272 #[test]
273 fn includes_declaration_when_requested() {
274 let (document, source) = fixture();
275 let position = position_of(&source, "Cache:");
276 let ranges = find_references(&document, position, true);
277 assert!(ranges.len() >= 3);
278 let definition_header = document
279 .root
280 .children
281 .iter()
282 .find_map(|item| match item {
283 lex_core::lex::ast::ContentItem::Definition(def) => def
284 .header_location()
285 .cloned()
286 .or_else(|| Some(def.range().clone())),
287 _ => None,
288 })
289 .expect("definition header available");
290 assert!(ranges.contains(&definition_header));
291 }
292
293 #[test]
294 fn finds_annotation_references() {
295 let (document, source) = fixture();
296 let position = position_of(&source, "::note]");
297 let ranges = find_references(&document, position, false);
298 assert_eq!(ranges.len(), 1);
299 assert!(ranges[0].contains(position));
300 }
301}