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];