lex_analysis/
utils.rs

1use crate::inline::{extract_inline_spans, InlineSpan, InlineSpanKind};
2use lex_core::lex::ast::traits::AstNode;
3use lex_core::lex::ast::{
4    Annotation, ContentItem, Definition, Document, Position, Session, TextContent,
5};
6
7/// Visits every text content node in the document, invoking the callback for each.
8///
9/// Traverses the full document tree including session titles, paragraph lines,
10/// list item text, definition subjects, and annotation bodies. Useful for
11/// extracting inline references or performing text-level analysis.
12pub fn for_each_text_content<F>(document: &Document, f: &mut F)
13where
14    F: FnMut(&TextContent),
15{
16    for annotation in document.annotations() {
17        visit_annotation_text(annotation, f);
18    }
19    visit_session_text(&document.root, true, f);
20}
21
22/// Visits every annotation in the document, invoking the callback for each.
23///
24/// Traverses the full document tree to find annotations at all levels:
25/// document-level, session-level, and nested within content items like
26/// paragraphs, lists, definitions, and verbatim blocks. Annotations are
27/// visited in document order (top to bottom).
28///
29/// Use this for annotation-related features like navigation, resolution
30/// toggling, or collecting annotation labels for completion.
31pub fn for_each_annotation<F>(document: &Document, f: &mut F)
32where
33    F: FnMut(&Annotation),
34{
35    for annotation in document.annotations() {
36        visit_annotation_recursive(annotation, f);
37    }
38    visit_session_annotations(&document.root, f);
39}
40
41/// Collects all annotations in the document into a vector.
42///
43/// Returns annotations in document order (top to bottom), including those
44/// at document-level, session-level, and nested within content items.
45/// This is a convenience wrapper around [`for_each_annotation`] for cases
46/// where you need a collected result rather than a streaming callback.
47pub fn collect_all_annotations(document: &Document) -> Vec<&Annotation> {
48    let mut annotations = Vec::new();
49    for annotation in document.annotations() {
50        collect_annotation_recursive(annotation, &mut annotations);
51    }
52    collect_annotations_into(&document.root, &mut annotations);
53    annotations
54}
55
56fn collect_annotations_into<'a>(session: &'a Session, out: &mut Vec<&'a Annotation>) {
57    for annotation in session.annotations() {
58        collect_annotation_recursive(annotation, out);
59    }
60    for child in session.children.iter() {
61        collect_content_annotations(child, out);
62    }
63}
64
65fn collect_annotation_recursive<'a>(annotation: &'a Annotation, out: &mut Vec<&'a Annotation>) {
66    out.push(annotation);
67    for child in annotation.children.iter() {
68        collect_content_annotations(child, out);
69    }
70}
71
72fn collect_content_annotations<'a>(item: &'a ContentItem, out: &mut Vec<&'a Annotation>) {
73    match item {
74        ContentItem::Annotation(annotation) => {
75            collect_annotation_recursive(annotation, out);
76        }
77        ContentItem::Paragraph(paragraph) => {
78            for annotation in paragraph.annotations() {
79                collect_annotation_recursive(annotation, out);
80            }
81            for line in &paragraph.lines {
82                collect_content_annotations(line, out);
83            }
84        }
85        ContentItem::List(list) => {
86            for annotation in list.annotations() {
87                collect_annotation_recursive(annotation, out);
88            }
89            for entry in &list.items {
90                collect_content_annotations(entry, out);
91            }
92        }
93        ContentItem::ListItem(list_item) => {
94            for annotation in list_item.annotations() {
95                collect_annotation_recursive(annotation, out);
96            }
97            for child in list_item.children.iter() {
98                collect_content_annotations(child, out);
99            }
100        }
101        ContentItem::Definition(definition) => {
102            for annotation in definition.annotations() {
103                collect_annotation_recursive(annotation, out);
104            }
105            for child in definition.children.iter() {
106                collect_content_annotations(child, out);
107            }
108        }
109        ContentItem::Session(session) => collect_annotations_into(session, out),
110        ContentItem::VerbatimBlock(verbatim) => {
111            for annotation in verbatim.annotations() {
112                collect_annotation_recursive(annotation, out);
113            }
114        }
115        ContentItem::TextLine(_)
116        | ContentItem::VerbatimLine(_)
117        | ContentItem::BlankLineGroup(_) => {}
118    }
119}
120
121fn visit_annotation_recursive<F>(annotation: &Annotation, f: &mut F)
122where
123    F: FnMut(&Annotation),
124{
125    f(annotation);
126    for child in annotation.children.iter() {
127        visit_content_annotations(child, f);
128    }
129}
130
131fn visit_session_annotations<F>(session: &Session, f: &mut F)
132where
133    F: FnMut(&Annotation),
134{
135    for annotation in session.annotations() {
136        visit_annotation_recursive(annotation, f);
137    }
138    for child in session.children.iter() {
139        visit_content_annotations(child, f);
140    }
141}
142
143fn visit_content_annotations<F>(item: &ContentItem, f: &mut F)
144where
145    F: FnMut(&Annotation),
146{
147    match item {
148        ContentItem::Annotation(annotation) => {
149            visit_annotation_recursive(annotation, f);
150        }
151        ContentItem::Paragraph(paragraph) => {
152            for annotation in paragraph.annotations() {
153                visit_annotation_recursive(annotation, f);
154            }
155            for line in &paragraph.lines {
156                visit_content_annotations(line, f);
157            }
158        }
159        ContentItem::List(list) => {
160            for annotation in list.annotations() {
161                visit_annotation_recursive(annotation, f);
162            }
163            for entry in &list.items {
164                visit_content_annotations(entry, f);
165            }
166        }
167        ContentItem::ListItem(list_item) => {
168            for annotation in list_item.annotations() {
169                visit_annotation_recursive(annotation, f);
170            }
171            for child in list_item.children.iter() {
172                visit_content_annotations(child, f);
173            }
174        }
175        ContentItem::Definition(definition) => {
176            for annotation in definition.annotations() {
177                visit_annotation_recursive(annotation, f);
178            }
179            for child in definition.children.iter() {
180                visit_content_annotations(child, f);
181            }
182        }
183        ContentItem::Session(session) => visit_session_annotations(session, f),
184        ContentItem::VerbatimBlock(verbatim) => {
185            for annotation in verbatim.annotations() {
186                visit_annotation_recursive(annotation, f);
187            }
188        }
189        ContentItem::TextLine(_)
190        | ContentItem::VerbatimLine(_)
191        | ContentItem::BlankLineGroup(_) => {}
192    }
193}
194
195pub fn find_definition_by_subject<'a>(
196    document: &'a Document,
197    target: &str,
198) -> Option<&'a Definition> {
199    find_definitions_by_subject(document, target)
200        .into_iter()
201        .next()
202}
203
204pub fn find_definitions_by_subject<'a>(
205    document: &'a Document,
206    target: &str,
207) -> Vec<&'a Definition> {
208    let normalized = normalize_key(target);
209    if normalized.is_empty() {
210        return Vec::new();
211    }
212    let mut matches = Vec::new();
213    for annotation in document.annotations() {
214        collect_definitions(annotation.children.iter(), &normalized, &mut matches);
215    }
216    collect_definitions(document.root.children.iter(), &normalized, &mut matches);
217    matches
218}
219
220pub fn find_definition_at_position(document: &Document, position: Position) -> Option<&Definition> {
221    for annotation in document.annotations() {
222        if let Some(definition) = find_definition_in_items(annotation.children.iter(), position) {
223            return Some(definition);
224        }
225    }
226    find_definition_in_items(document.root.children.iter(), position)
227}
228
229pub fn find_annotation_at_position(document: &Document, position: Position) -> Option<&Annotation> {
230    for annotation in document.annotations() {
231        if annotation.header_location().contains(position) {
232            return Some(annotation);
233        }
234        if let Some(found) = find_annotation_in_items(annotation.children.iter(), position) {
235            return Some(found);
236        }
237    }
238    find_annotation_in_session(&document.root, position, true)
239}
240
241pub fn find_session_at_position(document: &Document, position: Position) -> Option<&Session> {
242    find_session_in_branch(&document.root, position, true)
243}
244
245pub fn find_sessions_by_identifier<'a>(
246    document: &'a Document,
247    identifier: &str,
248) -> Vec<&'a Session> {
249    let normalized = normalize_key(identifier);
250    if normalized.is_empty() {
251        return Vec::new();
252    }
253    let mut matches = Vec::new();
254    collect_sessions_by_identifier(&document.root, &normalized, &mut matches, true);
255    matches
256}
257
258pub fn session_identifier(session: &Session) -> Option<String> {
259    extract_session_identifier(session.title.as_string())
260}
261
262pub fn reference_span_at_position(document: &Document, position: Position) -> Option<InlineSpan> {
263    let mut result = None;
264    for_each_text_content(document, &mut |text| {
265        if result.is_some() {
266            return;
267        }
268        for span in extract_inline_spans(text) {
269            if matches!(span.kind, InlineSpanKind::Reference(_)) && span.range.contains(position) {
270                result = Some(span);
271                break;
272            }
273        }
274    });
275    result
276}
277
278fn visit_session_text<F>(session: &Session, is_root: bool, f: &mut F)
279where
280    F: FnMut(&TextContent),
281{
282    if !is_root {
283        f(&session.title);
284    }
285    for annotation in session.annotations() {
286        visit_annotation_text(annotation, f);
287    }
288    for child in session.children.iter() {
289        visit_content_text(child, f);
290    }
291}
292
293fn visit_annotation_text<F>(annotation: &Annotation, f: &mut F)
294where
295    F: FnMut(&TextContent),
296{
297    for child in annotation.children.iter() {
298        visit_content_text(child, f);
299    }
300}
301
302fn visit_content_text<F>(item: &ContentItem, f: &mut F)
303where
304    F: FnMut(&TextContent),
305{
306    match item {
307        ContentItem::Paragraph(paragraph) => {
308            for line in &paragraph.lines {
309                if let ContentItem::TextLine(text_line) = line {
310                    f(&text_line.content);
311                }
312            }
313            for annotation in paragraph.annotations() {
314                visit_annotation_text(annotation, f);
315            }
316        }
317        ContentItem::Session(session) => visit_session_text(session, false, f),
318        ContentItem::List(list) => {
319            for annotation in list.annotations() {
320                visit_annotation_text(annotation, f);
321            }
322            for entry in &list.items {
323                if let ContentItem::ListItem(list_item) = entry {
324                    for text in &list_item.text {
325                        f(text);
326                    }
327                    for annotation in list_item.annotations() {
328                        visit_annotation_text(annotation, f);
329                    }
330                    for child in list_item.children.iter() {
331                        visit_content_text(child, f);
332                    }
333                }
334            }
335        }
336        ContentItem::ListItem(list_item) => {
337            for text in &list_item.text {
338                f(text);
339            }
340            for annotation in list_item.annotations() {
341                visit_annotation_text(annotation, f);
342            }
343            for child in list_item.children.iter() {
344                visit_content_text(child, f);
345            }
346        }
347        ContentItem::Definition(definition) => {
348            f(&definition.subject);
349            for annotation in definition.annotations() {
350                visit_annotation_text(annotation, f);
351            }
352            for child in definition.children.iter() {
353                visit_content_text(child, f);
354            }
355        }
356        ContentItem::Annotation(annotation) => visit_annotation_text(annotation, f),
357        _ => {}
358    }
359}
360
361fn collect_definitions<'a>(
362    items: impl Iterator<Item = &'a ContentItem>,
363    target: &str,
364    matches: &mut Vec<&'a Definition>,
365) {
366    for item in items {
367        collect_definitions_in_content(item, target, matches);
368    }
369}
370
371fn collect_definitions_in_content<'a>(
372    item: &'a ContentItem,
373    target: &str,
374    matches: &mut Vec<&'a Definition>,
375) {
376    match item {
377        ContentItem::Definition(definition) => {
378            if subject_matches(definition, target) {
379                matches.push(definition);
380            }
381            collect_definitions(definition.children.iter(), target, matches);
382        }
383        ContentItem::Session(session) => {
384            collect_definitions(session.children.iter(), target, matches);
385        }
386        ContentItem::List(list) => {
387            for entry in &list.items {
388                if let ContentItem::ListItem(list_item) = entry {
389                    collect_definitions(list_item.children.iter(), target, matches);
390                }
391            }
392        }
393        ContentItem::ListItem(list_item) => {
394            collect_definitions(list_item.children.iter(), target, matches);
395        }
396        ContentItem::Annotation(annotation) => {
397            collect_definitions(annotation.children.iter(), target, matches);
398        }
399        ContentItem::Paragraph(paragraph) => {
400            for annotation in paragraph.annotations() {
401                collect_definitions(annotation.children.iter(), target, matches);
402            }
403        }
404        _ => {}
405    }
406}
407
408fn find_definition_in_items<'a>(
409    items: impl Iterator<Item = &'a ContentItem>,
410    position: Position,
411) -> Option<&'a Definition> {
412    for item in items {
413        if let Some(definition) = find_definition_in_content(item, position) {
414            return Some(definition);
415        }
416    }
417    None
418}
419
420fn find_definition_in_content(item: &ContentItem, position: Position) -> Option<&Definition> {
421    match item {
422        ContentItem::Definition(definition) => {
423            if definition
424                .header_location()
425                .map(|range| range.contains(position))
426                .unwrap_or_else(|| definition.range().contains(position))
427            {
428                return Some(definition);
429            }
430            find_definition_in_items(definition.children.iter(), position)
431        }
432        ContentItem::Session(session) => {
433            find_definition_in_items(session.children.iter(), position)
434        }
435        ContentItem::List(list) => list.items.iter().find_map(|entry| match entry {
436            ContentItem::ListItem(list_item) => {
437                find_definition_in_items(list_item.children.iter(), position)
438            }
439            _ => None,
440        }),
441        ContentItem::ListItem(list_item) => {
442            find_definition_in_items(list_item.children.iter(), position)
443        }
444        ContentItem::Annotation(annotation) => {
445            find_definition_in_items(annotation.children.iter(), position)
446        }
447        ContentItem::Paragraph(paragraph) => paragraph
448            .annotations()
449            .iter()
450            .find_map(|annotation| find_definition_in_items(annotation.children.iter(), position)),
451        _ => None,
452    }
453}
454
455fn find_annotation_in_session(
456    session: &Session,
457    position: Position,
458    is_root: bool,
459) -> Option<&Annotation> {
460    if !is_root {
461        if let Some(annotation) = session
462            .annotations()
463            .iter()
464            .find(|ann| ann.header_location().contains(position))
465        {
466            return Some(annotation);
467        }
468    }
469    for child in session.children.iter() {
470        if let Some(annotation) = find_annotation_in_content(child, position) {
471            return Some(annotation);
472        }
473    }
474    None
475}
476
477fn find_annotation_in_content(item: &ContentItem, position: Position) -> Option<&Annotation> {
478    match item {
479        ContentItem::Paragraph(paragraph) => paragraph
480            .annotations()
481            .iter()
482            .find(|ann| ann.header_location().contains(position))
483            .or_else(|| find_annotation_in_items(paragraph.lines.iter(), position)),
484        ContentItem::Session(session) => find_annotation_in_session(session, position, false),
485        ContentItem::List(list) => {
486            if let Some(annotation) = list
487                .annotations()
488                .iter()
489                .find(|ann| ann.header_location().contains(position))
490            {
491                return Some(annotation);
492            }
493            for entry in &list.items {
494                if let ContentItem::ListItem(list_item) = entry {
495                    if let Some(annotation) = list_item
496                        .annotations()
497                        .iter()
498                        .find(|ann| ann.header_location().contains(position))
499                    {
500                        return Some(annotation);
501                    }
502                    if let Some(found) =
503                        find_annotation_in_items(list_item.children.iter(), position)
504                    {
505                        return Some(found);
506                    }
507                }
508            }
509            None
510        }
511        ContentItem::ListItem(list_item) => list_item
512            .annotations()
513            .iter()
514            .find(|ann| ann.header_location().contains(position))
515            .or_else(|| find_annotation_in_items(list_item.children.iter(), position)),
516        ContentItem::Definition(definition) => definition
517            .annotations()
518            .iter()
519            .find(|ann| ann.header_location().contains(position))
520            .or_else(|| find_annotation_in_items(definition.children.iter(), position)),
521        ContentItem::Annotation(annotation) => {
522            if annotation.header_location().contains(position) {
523                return Some(annotation);
524            }
525            find_annotation_in_items(annotation.children.iter(), position)
526        }
527        ContentItem::VerbatimBlock(verbatim) => verbatim
528            .annotations()
529            .iter()
530            .find(|ann| ann.header_location().contains(position))
531            .or_else(|| find_annotation_in_items(verbatim.children.iter(), position)),
532        ContentItem::TextLine(_) => None,
533        _ => None,
534    }
535}
536
537fn find_annotation_in_items<'a>(
538    items: impl Iterator<Item = &'a ContentItem>,
539    position: Position,
540) -> Option<&'a Annotation> {
541    for item in items {
542        if let Some(annotation) = find_annotation_in_content(item, position) {
543            return Some(annotation);
544        }
545    }
546    None
547}
548
549fn find_session_in_branch(
550    session: &Session,
551    position: Position,
552    is_root: bool,
553) -> Option<&Session> {
554    if !is_root {
555        if let Some(header) = session.header_location() {
556            if header.contains(position) {
557                return Some(session);
558            }
559        }
560    }
561    for child in session.children.iter() {
562        if let ContentItem::Session(child_session) = child {
563            if let Some(found) = find_session_in_branch(child_session, position, false) {
564                return Some(found);
565            }
566        }
567    }
568    None
569}
570
571fn collect_sessions_by_identifier<'a>(
572    session: &'a Session,
573    target: &str,
574    matches: &mut Vec<&'a Session>,
575    is_root: bool,
576) {
577    if !is_root {
578        let title = session.title.as_string();
579        let normalized_title = title.trim().to_ascii_lowercase();
580        let title_matches =
581            normalized_title.starts_with(target) && has_session_boundary(title, target.len());
582        let identifier_matches = session_identifier(session)
583            .as_deref()
584            .map(|id| id.to_ascii_lowercase() == target)
585            .unwrap_or(false);
586        if title_matches || identifier_matches {
587            matches.push(session);
588        }
589    }
590    for child in session.children.iter() {
591        if let ContentItem::Session(child_session) = child {
592            collect_sessions_by_identifier(child_session, target, matches, false);
593        }
594    }
595}
596
597fn has_session_boundary(title: &str, len: usize) -> bool {
598    let trimmed = title.trim();
599    if trimmed.len() <= len {
600        return trimmed.len() == len;
601    }
602    matches!(
603        trimmed.chars().nth(len),
604        Some(ch) if matches!(ch, ' ' | '\t' | ':' | '.')
605    )
606}
607
608fn subject_matches(definition: &Definition, target: &str) -> bool {
609    normalize_key(definition.subject.as_string()).eq(target)
610}
611
612fn normalize_key(input: &str) -> String {
613    input.trim().to_ascii_lowercase()
614}
615
616fn extract_session_identifier(title: &str) -> Option<String> {
617    let trimmed = title.trim();
618    if trimmed.is_empty() {
619        return None;
620    }
621    let mut identifier = String::new();
622    for ch in trimmed.chars() {
623        if ch.is_ascii_digit() || ch == '.' {
624            identifier.push(ch);
625        } else {
626            break;
627        }
628    }
629    if identifier.ends_with('.') {
630        identifier.pop();
631    }
632    if identifier.is_empty() {
633        None
634    } else {
635        Some(identifier)
636    }
637}