Skip to main content

merman_editor_core/
context.rs

1use crate::snapshot::{DocumentSnapshot, FenceSnapshot};
2use crate::types::{Position, Range};
3use merman_analysis::{
4    FenceCursorCompletionKind, FenceExpectedSyntaxKind, FenceTextIndexSource,
5    shape_object_value_prefix,
6};
7use merman_core::preprocess::split_frontmatter_block;
8
9#[derive(Debug)]
10pub struct CompletionContext<'a> {
11    snapshot: &'a DocumentSnapshot,
12    fence: &'a FenceSnapshot,
13    prefix: String,
14    prefix_start_offset: usize,
15    cursor_offset: usize,
16    source: FenceTextIndexSource,
17    source_start: bool,
18    directive_prefix: Option<&'static str>,
19    comment_or_directive_line: bool,
20    expected_syntax: Option<FenceExpectedSyntaxKind>,
21    expected_syntax_span: Option<(usize, usize)>,
22    completion_kinds: Vec<FenceCursorCompletionKind>,
23}
24
25impl<'a> CompletionContext<'a> {
26    pub fn from_snapshot(snapshot: &'a DocumentSnapshot, position: Position) -> Option<Self> {
27        let fence = snapshot.fence_at_position(position)?;
28        let cursor_offset = snapshot.byte_offset_for_position(position)?;
29        if cursor_offset < fence.body_start
30            || cursor_offset > fence.body_end
31            || (cursor_offset == fence.body_end && fence.end > fence.body_end)
32        {
33            return None;
34        }
35        let relative_cursor = cursor_offset
36            .saturating_sub(fence.body_start)
37            .min(fence.text.len());
38        let cursor_context = fence
39            .text_index
40            .cursor_context(&fence.text, relative_cursor);
41        let prefix_start_offset = fence.body_start + cursor_context.prefix_start();
42        let cursor_offset = fence.body_start + cursor_context.cursor();
43
44        Some(Self {
45            snapshot,
46            fence,
47            prefix: cursor_context.prefix().to_string(),
48            prefix_start_offset,
49            cursor_offset,
50            source: cursor_context.source(),
51            source_start: cursor_context.is_source_start(),
52            directive_prefix: cursor_context.directive_prefix(),
53            comment_or_directive_line: cursor_context.is_comment_or_directive_line(),
54            expected_syntax: cursor_context.expected_syntax(),
55            expected_syntax_span: cursor_context
56                .expected_syntax_span()
57                .map(|span| (fence.body_start + span.start, fence.body_start + span.end)),
58            completion_kinds: cursor_context.completion_kinds().to_vec(),
59        })
60    }
61
62    pub fn prefix(&self) -> &str {
63        &self.prefix
64    }
65
66    pub fn fence(&self) -> &FenceSnapshot {
67        self.fence
68    }
69
70    pub fn document_uri(&self) -> &str {
71        self.snapshot.uri.as_str()
72    }
73
74    pub fn has_parser_backed_facts(&self) -> bool {
75        self.source.is_parser_backed()
76    }
77
78    pub fn fact_source(&self) -> FenceTextIndexSource {
79        self.source
80    }
81
82    pub fn is_source_start(&self) -> bool {
83        self.source_start
84    }
85
86    pub fn prefix_range(&self) -> Option<Range> {
87        self.range_for_offsets(self.prefix_start_offset, self.cursor_offset)
88    }
89
90    pub fn direction_value_range(&self) -> Option<Range> {
91        if matches!(
92            self.expected_syntax,
93            Some(FenceExpectedSyntaxKind::Direction)
94        ) && let Some((start, end)) = self.expected_syntax_span
95        {
96            return self.range_for_offsets(start, end);
97        }
98
99        None
100    }
101
102    pub fn is_block_diagram(&self) -> bool {
103        self.fence.diagram_type.as_deref() == Some("block")
104    }
105
106    pub fn operator_range(&self) -> Option<Range> {
107        let suffix_start = operator_suffix_start(&self.prefix)?;
108        self.range_for_offsets(self.prefix_start_offset + suffix_start, self.cursor_offset)
109    }
110
111    pub fn shape_value_range(&self) -> Option<Range> {
112        self.shape_value_edit_parts().map(|(range, _, _)| range)
113    }
114
115    pub fn shape_value_edit(&self, value: &str) -> Option<CompletionTextEditParts> {
116        let (range, has_separator_space, append_closing_brace) = self.shape_value_edit_parts()?;
117        let replacement = if append_closing_brace {
118            if has_separator_space {
119                format!("{value} }}")
120            } else {
121                format!(" {value} }}")
122            }
123        } else if has_separator_space {
124            value.to_string()
125        } else {
126            format!(" {value}")
127        };
128
129        Some(CompletionTextEditParts { range, replacement })
130    }
131
132    pub fn shape_trigger_range(&self) -> Option<Range> {
133        if matches!(
134            self.expected_syntax,
135            Some(FenceExpectedSyntaxKind::ShapeTrigger)
136        ) && let Some((start, end)) = self.expected_syntax_span
137        {
138            return self.range_for_offsets(start, end);
139        }
140
141        let prefix = self.prefix.trim_end();
142        let trigger_len = if prefix.ends_with("((")
143            || prefix.ends_with("{{")
144            || prefix.ends_with("[/")
145            || prefix.ends_with("[\\")
146        {
147            2
148        } else if prefix.ends_with('[') || prefix.ends_with('>') {
149            1
150        } else {
151            return None;
152        };
153
154        self.range_for_offsets(
155            self.prefix_start_offset + prefix.len().saturating_sub(trigger_len),
156            self.cursor_offset,
157        )
158    }
159
160    pub fn offer_diagram_headers(&self) -> bool {
161        self.offers(FenceCursorCompletionKind::DiagramHeader)
162    }
163
164    pub fn offer_operator_items(&self) -> bool {
165        self.offers(FenceCursorCompletionKind::Operator)
166            && !self.degraded_flowchart_payload_context()
167    }
168
169    pub fn offer_directive_items(&self) -> bool {
170        if self.expected_syntax.is_some() {
171            return false;
172        }
173
174        self.offers(FenceCursorCompletionKind::Directive)
175    }
176
177    pub fn offer_direction_items(&self) -> bool {
178        if let Some(expected) = self.expected_syntax {
179            return matches!(expected, FenceExpectedSyntaxKind::Direction);
180        }
181
182        self.offers(FenceCursorCompletionKind::Direction)
183    }
184
185    pub fn offer_shape_items(&self) -> bool {
186        if let Some(expected) = self.expected_syntax {
187            return matches!(
188                expected,
189                FenceExpectedSyntaxKind::Shape | FenceExpectedSyntaxKind::ShapeTrigger
190            );
191        }
192
193        if self.degraded_flowchart_payload_context()
194            && !flowchart_top_level_shape_completion_context(&self.prefix)
195        {
196            return false;
197        }
198
199        self.offers(FenceCursorCompletionKind::Shape)
200    }
201
202    pub fn offer_node_items(&self) -> bool {
203        if let Some(expected) = self.expected_syntax {
204            return matches!(
205                expected,
206                FenceExpectedSyntaxKind::NodeIdentifier | FenceExpectedSyntaxKind::IdList
207            );
208        }
209
210        if self.degraded_flowchart_target_range().is_some() {
211            return true;
212        }
213
214        if self.has_parser_backed_facts() && self.offer_directive_target_node_items() {
215            return true;
216        }
217
218        false
219    }
220
221    pub fn offer_template_items(&self) -> bool {
222        if !self.source_start || self.directive_prefix.is_some() {
223            return false;
224        }
225        let prefix = self.prefix.trim_end();
226        !prefix.is_empty()
227            && !prefix.chars().any(char::is_whitespace)
228            && TEMPLATE_PREFIXES
229                .iter()
230                .any(|template_prefix| template_prefix.starts_with(prefix))
231    }
232
233    pub fn offer_frontmatter_items(&self) -> bool {
234        let relative_cursor = self
235            .cursor_offset
236            .saturating_sub(self.fence.body_start)
237            .min(self.fence.text.len());
238        is_frontmatter_authoring_position(
239            &self.fence.text,
240            relative_cursor,
241            &self.prefix,
242            self.source_start,
243        )
244    }
245
246    pub fn offer_class_name_items(&self) -> bool {
247        if self.payload_completion_context() {
248            return false;
249        }
250        if !self.has_parser_backed_facts() {
251            return false;
252        }
253        directive_slot_for_prefix(&self.prefix, self.directive_prefix)
254            == DirectiveCompletionSlot::ClassName
255    }
256
257    pub fn offer_style_snippet_items(&self) -> bool {
258        if self.payload_completion_context() {
259            return false;
260        }
261        if !self.has_parser_backed_facts() {
262            return false;
263        }
264        directive_slot_for_prefix(&self.prefix, self.directive_prefix)
265            == DirectiveCompletionSlot::Style
266    }
267
268    pub fn offer_interaction_snippet_items(&self) -> bool {
269        if self.payload_completion_context() {
270            return false;
271        }
272        if !self.has_parser_backed_facts() {
273            return false;
274        }
275        directive_slot_for_prefix(&self.prefix, self.directive_prefix)
276            == DirectiveCompletionSlot::Interaction
277    }
278
279    pub fn is_comment_or_directive_line(&self) -> bool {
280        self.comment_or_directive_line
281    }
282
283    pub fn is_parser_controlled_payload(&self) -> bool {
284        self.expected_syntax == Some(FenceExpectedSyntaxKind::Payload)
285    }
286
287    pub fn directive_prefix(&self) -> Option<&'static str> {
288        self.directive_prefix
289    }
290
291    pub fn node_text_edit_range(&self) -> Option<Range> {
292        if matches!(
293            self.expected_syntax,
294            Some(FenceExpectedSyntaxKind::NodeIdentifier | FenceExpectedSyntaxKind::IdList)
295        ) && let Some((start, end)) = self.expected_syntax_span
296        {
297            return self.range_for_offsets(start, end);
298        }
299
300        if self.offer_directive_target_node_items() {
301            return self.current_token_range(is_directive_target_delimiter);
302        }
303
304        if let Some(range) = self.degraded_flowchart_target_range() {
305            return Some(range);
306        }
307
308        if self.offer_operator_items() {
309            None
310        } else {
311            self.prefix_range()
312        }
313    }
314
315    pub fn class_name_text_edit_range(&self) -> Option<Range> {
316        self.current_token_range(is_class_name_delimiter)
317    }
318
319    pub fn style_text_edit_range(&self) -> Option<Range> {
320        self.current_token_range(is_style_token_delimiter)
321    }
322
323    pub fn interaction_text_edit_range(&self) -> Option<Range> {
324        self.current_token_range(is_style_token_delimiter)
325    }
326
327    pub fn frontmatter_text_edit_range(&self) -> Option<Range> {
328        self.current_token_range(is_frontmatter_token_delimiter)
329    }
330
331    fn range_for_offsets(&self, start: usize, end: usize) -> Option<Range> {
332        let span = self.snapshot.source_map.span(start, end).ok()?;
333        Some(Range {
334            start: Position {
335                line: span.lsp_range.start.line,
336                character: span.lsp_range.start.character,
337            },
338            end: Position {
339                line: span.lsp_range.end.line,
340                character: span.lsp_range.end.character,
341            },
342        })
343    }
344
345    fn shape_value_edit_parts(&self) -> Option<(Range, bool, bool)> {
346        if self.expected_syntax == Some(FenceExpectedSyntaxKind::Shape) {
347            return self.shape_value_edit_parts_from_expected_span();
348        }
349
350        let prefix = self.prefix.as_str();
351        if let Some((range, has_separator_space)) = self.shape_value_edit_parts_from_prefix(prefix)
352        {
353            return Some((
354                range,
355                has_separator_space,
356                self.should_append_shape_closing_brace(self.cursor_offset),
357            ));
358        }
359
360        None
361    }
362
363    fn shape_value_edit_parts_from_prefix(&self, prefix: &str) -> Option<(Range, bool)> {
364        let shape_prefix = shape_object_value_prefix(prefix)?;
365        let range = self.range_for_offsets(
366            self.prefix_start_offset + shape_prefix.value_start,
367            self.cursor_offset,
368        )?;
369
370        Some((range, shape_prefix.has_separator_space))
371    }
372
373    fn shape_value_edit_parts_from_expected_span(&self) -> Option<(Range, bool, bool)> {
374        let (start, end) = self.expected_syntax_span?;
375        let range = self.range_for_offsets(start, end)?;
376        let has_separator_space = self.snapshot.text[..start]
377            .chars()
378            .next_back()
379            .is_some_and(|ch| ch.is_whitespace());
380        let append_closing_brace = self.should_append_shape_closing_brace(end);
381
382        Some((range, has_separator_space, append_closing_brace))
383    }
384
385    fn should_append_shape_closing_brace(&self, offset: usize) -> bool {
386        let Some(suffix) = self.snapshot.text.get(offset..self.fence.body_end) else {
387            return false;
388        };
389        !matches!(
390            suffix.chars().find(|ch| !ch.is_whitespace()),
391            Some('}' | ',')
392        )
393    }
394
395    fn offers(&self, kind: FenceCursorCompletionKind) -> bool {
396        self.completion_kinds.contains(&kind)
397    }
398
399    fn offer_directive_target_node_items(&self) -> bool {
400        directive_slot_for_prefix(&self.prefix, self.directive_prefix)
401            == DirectiveCompletionSlot::Target
402    }
403
404    fn degraded_flowchart_target_range(&self) -> Option<Range> {
405        if self.expected_syntax.is_some()
406            || self.comment_or_directive_line
407            || !self.source.is_parser_backed()
408            || self.source.has_source_mapped_spans()
409            || !is_flowchart_diagram_type(self.fence.diagram_type.as_deref())
410        {
411            return None;
412        }
413
414        let token_start = flowchart_target_token_start(&self.prefix)?;
415        self.range_for_offsets(self.prefix_start_offset + token_start, self.cursor_offset)
416    }
417
418    fn degraded_flowchart_payload_context(&self) -> bool {
419        self.expected_syntax.is_none()
420            && self.source.is_parser_backed()
421            && !self.source.has_source_mapped_spans()
422            && is_flowchart_diagram_type(self.fence.diagram_type.as_deref())
423            && flowchart_scan_line(&self.prefix).inside_payload
424    }
425
426    fn payload_completion_context(&self) -> bool {
427        self.is_parser_controlled_payload() || self.degraded_flowchart_payload_context()
428    }
429
430    fn current_token_range(&self, is_delimiter: fn(char) -> bool) -> Option<Range> {
431        let prefix = self.prefix.as_str();
432        let token_start = prefix
433            .char_indices()
434            .rev()
435            .find_map(|(idx, ch)| is_delimiter(ch).then_some(idx + ch.len_utf8()))
436            .unwrap_or(0);
437
438        self.range_for_offsets(self.prefix_start_offset + token_start, self.cursor_offset)
439    }
440}
441
442#[derive(Debug, Clone, PartialEq, Eq)]
443pub struct CompletionTextEditParts {
444    pub range: Range,
445    pub replacement: String,
446}
447
448fn operator_suffix_start(prefix: &str) -> Option<usize> {
449    let mut start = prefix.len();
450    let mut seen_operator = false;
451
452    for (idx, ch) in prefix.char_indices().rev() {
453        if matches!(ch, '-' | '>' | '.' | '=') {
454            start = idx;
455            seen_operator = true;
456        } else {
457            break;
458        }
459    }
460
461    seen_operator.then_some(start)
462}
463
464fn is_flowchart_diagram_type(diagram_type: Option<&str>) -> bool {
465    diagram_type.is_some_and(|diagram_type| diagram_type.starts_with("flowchart"))
466}
467
468fn flowchart_target_token_start(prefix: &str) -> Option<usize> {
469    let trimmed_end = prefix.trim_end().len();
470    let trimmed = &prefix[..trimmed_end];
471    let (_, operator_end) = last_flowchart_operator(trimmed)?;
472    let mut target_start = operator_end;
473
474    target_start += leading_whitespace_len(&prefix[target_start..]);
475    let mut replacement_start = target_start;
476    let operator_tail = prefix.get(target_start..)?;
477    if operator_tail.starts_with('|') {
478        target_start += flowchart_edge_label_len(operator_tail)?;
479        replacement_start = target_start;
480        target_start += leading_whitespace_len(&prefix[target_start..]);
481    }
482
483    let target = &prefix[target_start..];
484    if target.chars().any(char::is_whitespace) {
485        return None;
486    }
487    if !target.chars().all(is_flowchart_target_fragment_char) {
488        return None;
489    }
490
491    Some(replacement_start)
492}
493
494fn last_flowchart_operator(prefix: &str) -> Option<(usize, usize)> {
495    let scan = flowchart_scan_line(prefix);
496    if scan.inside_payload {
497        return None;
498    }
499    scan.last_operator
500}
501
502fn flowchart_top_level_shape_completion_context(prefix: &str) -> bool {
503    flowchart_top_level_shape_trigger(prefix) || flowchart_top_level_shape_object_value(prefix)
504}
505
506fn flowchart_top_level_shape_trigger(prefix: &str) -> bool {
507    let prefix = prefix.trim_end();
508    let trigger_len = if prefix.ends_with("((")
509        || prefix.ends_with("{{")
510        || prefix.ends_with("[/")
511        || prefix.ends_with("[\\")
512    {
513        2
514    } else if prefix.ends_with('[') || prefix.ends_with('>') {
515        1
516    } else {
517        return false;
518    };
519    let trigger_start = prefix.len().saturating_sub(trigger_len);
520
521    !flowchart_scan_line(&prefix[..trigger_start]).inside_payload
522}
523
524fn flowchart_top_level_shape_object_value(prefix: &str) -> bool {
525    let prefix = prefix.trim_end();
526    let mut search_end = prefix.len();
527
528    while let Some(marker) = prefix[..search_end].rfind("@{") {
529        if shape_object_value_prefix(&prefix[marker..search_end]).is_some()
530            && !flowchart_scan_line(&prefix[..marker]).inside_payload
531        {
532            return true;
533        }
534        search_end = marker;
535    }
536
537    false
538}
539
540#[derive(Debug, Default)]
541struct FlowchartLineScan {
542    last_operator: Option<(usize, usize)>,
543    inside_payload: bool,
544}
545
546fn flowchart_scan_line(prefix: &str) -> FlowchartLineScan {
547    let mut scan = FlowchartLineScan::default();
548    let mut state = FlowchartOperatorScanState::default();
549    let mut cursor = 0usize;
550
551    while cursor < prefix.len() {
552        if state.is_operator_site()
553            && let Some(operator) = flowchart_operator_at(prefix, cursor)
554        {
555            let start = cursor;
556            cursor = operator.end;
557            scan.last_operator = Some((start, cursor));
558
559            if operator.inside_payload {
560                scan.inside_payload = true;
561                return scan;
562            }
563
564            let label_start = cursor + leading_whitespace_len(&prefix[cursor..]);
565            if prefix[label_start..].starts_with('|') {
566                let Some(label_len) = flowchart_edge_label_len(&prefix[label_start..]) else {
567                    scan.inside_payload = true;
568                    return scan;
569                };
570                cursor = label_start + label_len;
571            }
572            continue;
573        }
574
575        let Some(ch) = prefix[cursor..].chars().next() else {
576            break;
577        };
578        state.accept(ch);
579        cursor += ch.len_utf8();
580    }
581
582    scan.inside_payload = !state.is_operator_site();
583    scan
584}
585
586#[derive(Debug, Default)]
587struct FlowchartOperatorScanState {
588    bracket_depth: usize,
589    paren_depth: usize,
590    brace_depth: usize,
591    quote: Option<char>,
592    escaped: bool,
593}
594
595impl FlowchartOperatorScanState {
596    fn is_operator_site(&self) -> bool {
597        self.quote.is_none()
598            && self.bracket_depth == 0
599            && self.paren_depth == 0
600            && self.brace_depth == 0
601    }
602
603    fn accept(&mut self, ch: char) {
604        if let Some(quote) = self.quote {
605            if self.escaped {
606                self.escaped = false;
607                return;
608            }
609            if ch == '\\' {
610                self.escaped = true;
611                return;
612            }
613            if ch == quote {
614                self.quote = None;
615            }
616            return;
617        }
618
619        match ch {
620            '"' | '\'' | '`' => self.quote = Some(ch),
621            '[' => self.bracket_depth += 1,
622            ']' => self.bracket_depth = self.bracket_depth.saturating_sub(1),
623            '(' => self.paren_depth += 1,
624            ')' => self.paren_depth = self.paren_depth.saturating_sub(1),
625            '{' => self.brace_depth += 1,
626            '}' => self.brace_depth = self.brace_depth.saturating_sub(1),
627            _ => {}
628        }
629    }
630}
631
632#[derive(Debug, Clone, Copy, PartialEq, Eq)]
633struct FlowchartOperatorMatch {
634    end: usize,
635    inside_payload: bool,
636}
637
638#[derive(Debug, Clone, Copy, PartialEq, Eq)]
639enum FlowchartLinkFamily {
640    Normal,
641    Thick,
642    Dotted,
643    Invisible,
644}
645
646#[derive(Debug, Clone, Copy, PartialEq, Eq)]
647enum FlowchartLinkMarker {
648    Arrow,
649    Circle,
650    Cross,
651}
652
653fn flowchart_operator_at(prefix: &str, offset: usize) -> Option<FlowchartOperatorMatch> {
654    flowchart_full_link_at(prefix, offset)
655        .or_else(|| flowchart_new_notation_link_at(prefix, offset))
656}
657
658fn flowchart_full_link_at(prefix: &str, offset: usize) -> Option<FlowchartOperatorMatch> {
659    [
660        FlowchartLinkFamily::Invisible,
661        FlowchartLinkFamily::Thick,
662        FlowchartLinkFamily::Normal,
663        FlowchartLinkFamily::Dotted,
664    ]
665    .into_iter()
666    .find_map(|family| {
667        let link = flowchart_link_end_at(prefix, offset, family, false, true)?;
668        Some(FlowchartOperatorMatch {
669            end: link.end,
670            inside_payload: false,
671        })
672    })
673}
674
675fn flowchart_new_notation_link_at(prefix: &str, offset: usize) -> Option<FlowchartOperatorMatch> {
676    let start = flowchart_start_link_at(prefix, offset)?;
677    let mut cursor = start.end;
678    while cursor < prefix.len() {
679        if let Some(link) = flowchart_link_end_at(prefix, cursor, start.family, true, false)
680            && flowchart_new_notation_markers_match(start.marker, link.marker)
681        {
682            return Some(FlowchartOperatorMatch {
683                end: link.end,
684                inside_payload: false,
685            });
686        }
687        cursor += prefix[cursor..].chars().next()?.len_utf8();
688    }
689
690    Some(FlowchartOperatorMatch {
691        end: prefix.len(),
692        inside_payload: true,
693    })
694}
695
696#[derive(Debug, Clone, Copy, PartialEq, Eq)]
697struct FlowchartStartLink {
698    family: FlowchartLinkFamily,
699    marker: Option<FlowchartLinkMarker>,
700    end: usize,
701}
702
703#[derive(Debug, Clone, Copy, PartialEq, Eq)]
704struct FlowchartLinkEnd {
705    marker: Option<FlowchartLinkMarker>,
706    end: usize,
707}
708
709fn flowchart_start_link_at(prefix: &str, offset: usize) -> Option<FlowchartStartLink> {
710    let bytes = prefix.as_bytes();
711    let mut cursor = offset;
712    let mut marker = None;
713    if cursor >= bytes.len() {
714        return None;
715    }
716    if let Some(next_marker) = flowchart_start_marker(bytes[cursor]) {
717        marker = Some(next_marker);
718        cursor += 1;
719        if cursor >= bytes.len() {
720            return None;
721        }
722    }
723
724    if bytes.get(cursor) == Some(&b'-') && bytes.get(cursor + 1) == Some(&b'-') {
725        return Some(FlowchartStartLink {
726            family: FlowchartLinkFamily::Normal,
727            marker,
728            end: cursor + 2,
729        });
730    }
731    if bytes.get(cursor) == Some(&b'=') && bytes.get(cursor + 1) == Some(&b'=') {
732        return Some(FlowchartStartLink {
733            family: FlowchartLinkFamily::Thick,
734            marker,
735            end: cursor + 2,
736        });
737    }
738    if bytes.get(cursor) == Some(&b'-') && bytes.get(cursor + 1) == Some(&b'.') {
739        return Some(FlowchartStartLink {
740            family: FlowchartLinkFamily::Dotted,
741            marker,
742            end: cursor + 2,
743        });
744    }
745
746    None
747}
748
749fn flowchart_link_end_at(
750    prefix: &str,
751    offset: usize,
752    family: FlowchartLinkFamily,
753    allow_leading_ws: bool,
754    allow_leading_marker: bool,
755) -> Option<FlowchartLinkEnd> {
756    let bytes = prefix.as_bytes();
757    let mut cursor = offset;
758    if allow_leading_ws {
759        while cursor < bytes.len() && is_flowchart_link_ws(bytes[cursor]) {
760            cursor += 1;
761        }
762    }
763    let token_start = cursor;
764    if token_start >= bytes.len() {
765        return None;
766    }
767
768    if allow_leading_marker && flowchart_start_marker(bytes[cursor]).is_some() {
769        cursor += 1;
770        if cursor >= bytes.len() {
771            return None;
772        }
773    }
774
775    let mut marker = None;
776    match family {
777        FlowchartLinkFamily::Invisible => {
778            cursor = token_start;
779            let tilde_start = cursor;
780            while cursor < bytes.len() && bytes[cursor] == b'~' {
781                cursor += 1;
782            }
783            if cursor - tilde_start < 3 {
784                return None;
785            }
786        }
787        FlowchartLinkFamily::Normal => {
788            let hyphen_start = cursor;
789            while cursor < bytes.len() && bytes[cursor] == b'-' {
790                cursor += 1;
791            }
792            let hyphens = cursor - hyphen_start;
793            if hyphens < 2 {
794                return None;
795            }
796            if cursor < bytes.len() {
797                match bytes[cursor] {
798                    b'x' | b'o' | b'>' => {
799                        marker = flowchart_end_marker(bytes[cursor]);
800                        cursor += 1;
801                    }
802                    _ if hyphens < 3 => return None,
803                    _ => {}
804                }
805            } else if hyphens < 3 {
806                return None;
807            }
808        }
809        FlowchartLinkFamily::Thick => {
810            let eq_start = cursor;
811            while cursor < bytes.len() && bytes[cursor] == b'=' {
812                cursor += 1;
813            }
814            let eqs = cursor - eq_start;
815            if eqs < 2 {
816                return None;
817            }
818            if cursor < bytes.len() {
819                match bytes[cursor] {
820                    b'x' | b'o' | b'>' => {
821                        marker = flowchart_end_marker(bytes[cursor]);
822                        cursor += 1;
823                    }
824                    _ if eqs < 3 => return None,
825                    _ => {}
826                }
827            } else if eqs < 3 {
828                return None;
829            }
830        }
831        FlowchartLinkFamily::Dotted => {
832            if cursor < bytes.len() && bytes[cursor] == b'-' {
833                cursor += 1;
834            }
835            let dot_start = cursor;
836            while cursor < bytes.len() && bytes[cursor] == b'.' {
837                cursor += 1;
838            }
839            if cursor == dot_start {
840                return None;
841            }
842            if cursor >= bytes.len() || bytes[cursor] != b'-' {
843                return None;
844            }
845            cursor += 1;
846            if cursor < bytes.len() && matches!(bytes[cursor], b'x' | b'o' | b'>') {
847                marker = flowchart_end_marker(bytes[cursor]);
848                cursor += 1;
849            }
850        }
851    }
852
853    Some(FlowchartLinkEnd {
854        marker,
855        end: cursor,
856    })
857}
858
859fn flowchart_start_marker(byte: u8) -> Option<FlowchartLinkMarker> {
860    match byte {
861        b'<' => Some(FlowchartLinkMarker::Arrow),
862        b'o' => Some(FlowchartLinkMarker::Circle),
863        b'x' => Some(FlowchartLinkMarker::Cross),
864        _ => None,
865    }
866}
867
868fn flowchart_end_marker(byte: u8) -> Option<FlowchartLinkMarker> {
869    match byte {
870        b'>' => Some(FlowchartLinkMarker::Arrow),
871        b'o' => Some(FlowchartLinkMarker::Circle),
872        b'x' => Some(FlowchartLinkMarker::Cross),
873        _ => None,
874    }
875}
876
877fn flowchart_new_notation_markers_match(
878    start: Option<FlowchartLinkMarker>,
879    end: Option<FlowchartLinkMarker>,
880) -> bool {
881    start.is_none_or(|start| end == Some(start))
882}
883
884fn flowchart_edge_label_len(tail: &str) -> Option<usize> {
885    let label = tail.strip_prefix('|')?;
886    let close = label.find('|')?;
887    Some(1 + close + 1)
888}
889
890fn leading_whitespace_len(input: &str) -> usize {
891    input
892        .chars()
893        .take_while(|ch| ch.is_whitespace())
894        .map(char::len_utf8)
895        .sum()
896}
897
898fn is_flowchart_link_ws(byte: u8) -> bool {
899    matches!(byte, b' ' | b'\t' | b'\r' | b'\n')
900}
901
902fn is_flowchart_target_fragment_char(ch: char) -> bool {
903    ch == '_' || ch == '-' || ch == '.' || ch == ':' || ch.is_alphanumeric()
904}
905
906const TEMPLATE_PREFIXES: &[&str] = &[
907    "flow", "seq", "icon", "acc", "class", "state", "er", "gantt", "pie", "journey", "mind",
908];
909
910#[derive(Debug, Clone, Copy, PartialEq, Eq)]
911enum DirectiveCompletionSlot {
912    Target,
913    ClassName,
914    Style,
915    Interaction,
916    None,
917}
918
919fn directive_slot_for_prefix(
920    prefix: &str,
921    directive_prefix: Option<&str>,
922) -> DirectiveCompletionSlot {
923    if prefix
924        .rfind(":::")
925        .is_some_and(|index| index + 3 <= prefix.len())
926    {
927        return DirectiveCompletionSlot::ClassName;
928    }
929
930    let Some(directive_prefix) = directive_prefix else {
931        return DirectiveCompletionSlot::None;
932    };
933    let Some(rest) = rest_after_keyword(prefix, directive_prefix) else {
934        return DirectiveCompletionSlot::None;
935    };
936
937    match directive_prefix {
938        "class" => {
939            if first_argument_is_complete(rest) {
940                DirectiveCompletionSlot::ClassName
941            } else {
942                DirectiveCompletionSlot::Target
943            }
944        }
945        "cssClass" => {
946            if first_argument_is_complete(rest) {
947                DirectiveCompletionSlot::ClassName
948            } else {
949                DirectiveCompletionSlot::Target
950            }
951        }
952        "classDef" => {
953            if first_argument_is_complete(rest) {
954                DirectiveCompletionSlot::Style
955            } else if first_argument_end(rest).is_some() {
956                DirectiveCompletionSlot::ClassName
957            } else {
958                DirectiveCompletionSlot::None
959            }
960        }
961        "style" => {
962            if first_argument_is_complete(rest) {
963                DirectiveCompletionSlot::Style
964            } else {
965                DirectiveCompletionSlot::Target
966            }
967        }
968        "click" | "link" | "callback" => {
969            if first_argument_is_complete(rest) {
970                DirectiveCompletionSlot::Interaction
971            } else {
972                DirectiveCompletionSlot::Target
973            }
974        }
975        _ => DirectiveCompletionSlot::None,
976    }
977}
978
979fn rest_after_keyword<'a>(prefix: &'a str, keyword: &str) -> Option<&'a str> {
980    let rest = prefix.strip_prefix(keyword)?;
981    if rest.chars().next().is_none_or(|ch| ch.is_whitespace()) {
982        Some(rest)
983    } else {
984        None
985    }
986}
987
988fn first_argument_is_complete(rest: &str) -> bool {
989    let Some(argument_end) = first_argument_end(rest) else {
990        return false;
991    };
992
993    rest[argument_end..].chars().any(char::is_whitespace)
994}
995
996fn first_argument_end(rest: &str) -> Option<usize> {
997    let leading = rest
998        .chars()
999        .take_while(|ch| ch.is_whitespace())
1000        .map(char::len_utf8)
1001        .sum::<usize>();
1002    let body = &rest[leading..];
1003    if body.is_empty() {
1004        return None;
1005    }
1006
1007    if let Some(quote) = body.chars().next().filter(|ch| matches!(ch, '"' | '\'')) {
1008        let close = body[quote.len_utf8()..].find(quote)?;
1009        return Some(leading + quote.len_utf8() + close + quote.len_utf8());
1010    }
1011
1012    let body_end = body
1013        .char_indices()
1014        .find_map(|(idx, ch)| ch.is_whitespace().then_some(idx))
1015        .unwrap_or(body.len());
1016    Some(leading + body_end)
1017}
1018
1019fn is_directive_target_delimiter(ch: char) -> bool {
1020    ch.is_whitespace() || matches!(ch, ',' | '"' | '\'')
1021}
1022
1023fn is_class_name_delimiter(ch: char) -> bool {
1024    is_directive_target_delimiter(ch) || ch == ':'
1025}
1026
1027fn is_style_token_delimiter(ch: char) -> bool {
1028    ch.is_whitespace() || matches!(ch, ',' | '"' | '\'')
1029}
1030
1031fn is_frontmatter_token_delimiter(ch: char) -> bool {
1032    ch.is_whitespace() || ch == ':'
1033}
1034
1035fn is_frontmatter_authoring_position(
1036    text: &str,
1037    cursor: usize,
1038    prefix: &str,
1039    source_start: bool,
1040) -> bool {
1041    let trimmed_prefix = prefix.trim_end();
1042    if let Some(frontmatter) = split_frontmatter_block(text) {
1043        return cursor <= frontmatter.body.end;
1044    }
1045    if starts_with_frontmatter_opening_line(text) {
1046        return true;
1047    }
1048    if !source_start {
1049        return false;
1050    }
1051
1052    cursor == 0
1053        || trimmed_prefix == "---"
1054        || (!trimmed_prefix.is_empty()
1055            && FRONTMATTER_PREFIXES
1056                .iter()
1057                .any(|frontmatter_prefix| frontmatter_prefix.starts_with(trimmed_prefix)))
1058}
1059
1060fn starts_with_frontmatter_opening_line(text: &str) -> bool {
1061    let first_line_end = text.find('\n').unwrap_or(text.len());
1062    let first_line = text[..first_line_end].trim_end_matches('\r');
1063    first_line.trim_start() == "---"
1064}
1065
1066const FRONTMATTER_PREFIXES: &[&str] = &[
1067    "config",
1068    "theme",
1069    "themeCSS",
1070    "themeVariables",
1071    "look",
1072    "layout",
1073];