1use crate::options::ParserOptions;
7use crate::parser::inlines::refdef_map::{RefdefMap, collect_refdef_labels};
8use crate::range_utils::find_incremental_restart_offset;
9use crate::syntax::{SyntaxKind, SyntaxNode};
10use rowan::{GreenNode, GreenToken, NodeOrToken};
11
12pub mod blocks;
13pub mod inlines;
14pub mod utils;
15pub mod yaml;
16
17mod block_dispatcher;
18mod core;
19
20pub use core::Parser;
22
23pub fn parse(input: &str, config: Option<ParserOptions>) -> SyntaxNode {
47 let mut config = config.unwrap_or_default();
48 populate_refdef_labels(input, &mut config);
49 Parser::new(input, &config).parse()
50}
51
52pub fn parse_with_refdefs(
62 input: &str,
63 options: Option<ParserOptions>,
64 refdefs: RefdefMap,
65) -> SyntaxNode {
66 let mut options = options.unwrap_or_default();
67 options.refdef_labels = Some(refdefs);
68 Parser::new(input, &options).parse()
69}
70
71fn populate_refdef_labels(input: &str, config: &mut ParserOptions) {
88 if config.refdef_labels.is_some() {
89 return;
90 }
91 config.refdef_labels = Some(collect_refdef_labels(input, config.dialect));
92}
93
94pub struct IncrementalParseResult {
95 pub tree: SyntaxNode,
96 pub reparse_range: (usize, usize),
97 pub strategy: &'static str,
98}
99
100pub fn parse_incremental_suffix(
108 input: &str,
109 config: Option<ParserOptions>,
110 old_tree: &SyntaxNode,
111 old_edit_range: (usize, usize),
112 new_edit_range: (usize, usize),
113) -> IncrementalParseResult {
114 let mut config = config.unwrap_or_default();
115 populate_refdef_labels(input, &mut config);
116 parse_incremental_suffix_inner(input, config, old_tree, old_edit_range, new_edit_range)
117}
118
119pub fn parse_incremental_suffix_with_refdefs(
125 input: &str,
126 options: Option<ParserOptions>,
127 refdefs: RefdefMap,
128 old_tree: &SyntaxNode,
129 old_edit_range: (usize, usize),
130 new_edit_range: (usize, usize),
131) -> IncrementalParseResult {
132 let mut options = options.unwrap_or_default();
133 options.refdef_labels = Some(refdefs);
134 parse_incremental_suffix_inner(input, options, old_tree, old_edit_range, new_edit_range)
135}
136
137fn parse_incremental_suffix_inner(
138 input: &str,
139 config: ParserOptions,
140 old_tree: &SyntaxNode,
141 old_edit_range: (usize, usize),
142 new_edit_range: (usize, usize),
143) -> IncrementalParseResult {
144 let input_len = input.len();
145
146 let Some(old_edit) = normalize_range(old_edit_range) else {
147 return full_reparse_result(input, &config);
148 };
149 let Some(new_edit) = normalize_range(new_edit_range) else {
150 return full_reparse_result(input, &config);
151 };
152 if new_edit.1 > input_len {
153 return full_reparse_result(input, &config);
154 }
155
156 if old_tree.kind() != SyntaxKind::DOCUMENT {
157 return full_reparse_result(input, &config);
158 }
159
160 if let Some(section_window) =
161 find_top_level_heading_section_window(old_tree, old_edit, new_edit, input_len)
162 && let Some(result) = reparse_section_window(input, &config, old_tree, section_window)
163 {
164 return result;
165 }
166
167 let restart = find_incremental_restart_offset(old_tree, old_edit.0, old_edit.1);
168 let old_restart = align_to_document_child_start(old_tree, restart);
169
170 if (old_edit.0..old_edit.1).contains(&old_restart) {
171 return full_reparse_result(input, &config);
172 }
173
174 let new_restart = map_old_offset_to_new(old_restart, old_edit, new_edit, input_len);
175 if !input.is_char_boundary(new_restart) {
176 return full_reparse_result(input, &config);
177 }
178
179 let suffix_text = &input[new_restart..];
180 let suffix_tree = Parser::new(suffix_text, &config).parse();
181
182 let mut children: Vec<NodeOrToken<GreenNode, GreenToken>> = old_tree
183 .children_with_tokens()
184 .filter_map(|element| {
185 let range = element.text_range();
186 let end: usize = range.end().into();
187 if end <= old_restart {
188 Some(element_to_green(element))
189 } else {
190 None
191 }
192 })
193 .collect();
194 children.extend(suffix_tree.children_with_tokens().map(element_to_green));
195
196 let tree = SyntaxNode::new_root(GreenNode::new(SyntaxKind::DOCUMENT.into(), children));
197 let len: usize = tree.text_range().end().into();
198
199 IncrementalParseResult {
200 tree,
201 reparse_range: (new_restart, len),
202 strategy: "suffix_window",
203 }
204}
205
206fn normalize_range(range: (usize, usize)) -> Option<(usize, usize)> {
207 (range.0 <= range.1).then_some(range)
208}
209
210fn full_reparse_result(input: &str, config: &ParserOptions) -> IncrementalParseResult {
211 let tree = Parser::new(input, config).parse();
212 let len: usize = tree.text_range().end().into();
213 IncrementalParseResult {
214 tree,
215 reparse_range: (0, len),
216 strategy: "full_reparse",
217 }
218}
219
220fn align_to_document_child_start(tree: &SyntaxNode, offset: usize) -> usize {
221 for child in tree.children_with_tokens() {
222 let range = child.text_range();
223 let start: usize = range.start().into();
224 let end: usize = range.end().into();
225 if offset <= start {
226 return start;
227 }
228 if offset < end {
229 return start;
230 }
231 }
232 let len: usize = tree.text_range().end().into();
233 len
234}
235
236fn map_old_offset_to_new(
237 old_offset: usize,
238 old_edit: (usize, usize),
239 new_edit: (usize, usize),
240 new_len: usize,
241) -> usize {
242 if old_offset <= old_edit.0 {
243 return old_offset;
244 }
245 if old_offset >= old_edit.1 {
246 let old_span = old_edit.1 - old_edit.0;
247 let new_span = new_edit.1 - new_edit.0;
248 let delta = new_span as isize - old_span as isize;
249 return old_offset.saturating_add_signed(delta).min(new_len);
250 }
251 new_edit.1.min(new_len)
252}
253
254fn element_to_green(element: crate::syntax::SyntaxElement) -> NodeOrToken<GreenNode, GreenToken> {
255 match element {
256 NodeOrToken::Node(node) => NodeOrToken::Node(node.green().into_owned()),
257 NodeOrToken::Token(token) => NodeOrToken::Token(token.green().to_owned()),
258 }
259}
260
261#[derive(Debug, Clone, Copy)]
262struct SectionWindow {
263 old_start: usize,
264 old_end: usize,
265 new_start: usize,
266 new_end: usize,
267}
268
269fn find_top_level_heading_section_window(
270 old_tree: &SyntaxNode,
271 old_edit: (usize, usize),
272 new_edit: (usize, usize),
273 new_len: usize,
274) -> Option<SectionWindow> {
275 let old_len: usize = old_tree.text_range().end().into();
276 let mut previous_heading: Option<(usize, usize)> = None;
277 let mut next_heading: Option<(usize, usize)> = None;
278
279 for child in old_tree.children() {
280 if child.kind() != SyntaxKind::HEADING {
281 continue;
282 }
283
284 let range = child.text_range();
285 let start: usize = range.start().into();
286 let end: usize = range.end().into();
287
288 if start <= old_edit.0 {
289 previous_heading = Some((start, end));
290 } else {
291 next_heading = Some((start, end));
292 break;
293 }
294 }
295
296 let (previous_start, previous_end) = previous_heading?;
297 let (next_start, next_end) = next_heading.unwrap_or((old_len, old_len));
298
299 if ranges_intersect(old_edit, (previous_start, previous_end))
300 || ranges_intersect(old_edit, (next_start, next_end))
301 {
302 return None;
303 }
304
305 if old_edit.0 <= previous_end || old_edit.1 >= next_start {
308 return None;
309 }
310
311 let new_start = map_old_offset_to_new(previous_start, old_edit, new_edit, new_len);
312 let new_end = map_old_offset_to_new(next_start, old_edit, new_edit, new_len);
313 if new_start >= new_end || new_end > new_len {
314 return None;
315 }
316
317 Some(SectionWindow {
318 old_start: previous_start,
319 old_end: next_start,
320 new_start,
321 new_end,
322 })
323}
324
325fn ranges_intersect(a: (usize, usize), b: (usize, usize)) -> bool {
326 a.0 < b.1 && b.0 < a.1
327}
328
329fn reparse_section_window(
330 input: &str,
331 config: &ParserOptions,
332 old_tree: &SyntaxNode,
333 section_window: SectionWindow,
334) -> Option<IncrementalParseResult> {
335 if !input.is_char_boundary(section_window.new_start)
336 || !input.is_char_boundary(section_window.new_end)
337 {
338 return None;
339 }
340
341 let reparsed_window = Parser::new(
342 &input[section_window.new_start..section_window.new_end],
343 config,
344 )
345 .parse();
346
347 let mut children: Vec<NodeOrToken<GreenNode, GreenToken>> = Vec::new();
348 let mut inserted_window = false;
349
350 for element in old_tree.children_with_tokens() {
351 let range = element.text_range();
352 let start: usize = range.start().into();
353 let end: usize = range.end().into();
354
355 if end <= section_window.old_start {
356 children.push(element_to_green(element));
357 continue;
358 }
359
360 if start >= section_window.old_end {
361 if !inserted_window {
362 children.extend(reparsed_window.children_with_tokens().map(element_to_green));
363 inserted_window = true;
364 }
365 children.push(element_to_green(element));
366 continue;
367 }
368
369 }
371
372 if !inserted_window {
373 children.extend(reparsed_window.children_with_tokens().map(element_to_green));
374 }
375
376 let tree = SyntaxNode::new_root(GreenNode::new(SyntaxKind::DOCUMENT.into(), children));
377 Some(IncrementalParseResult {
378 tree,
379 reparse_range: (section_window.new_start, section_window.new_end),
380 strategy: "section_window",
381 })
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387
388 fn apply_edit(text: &str, old: (usize, usize), insert: &str) -> String {
389 let mut out = String::with_capacity(text.len() - (old.1 - old.0) + insert.len());
390 out.push_str(&text[..old.0]);
391 out.push_str(insert);
392 out.push_str(&text[old.1..]);
393 out
394 }
395
396 #[test]
397 fn incremental_suffix_matches_full_parse_for_tail_edit() {
398 let input = "# H\n\npara one\n\npara two\n\npara three\n";
399 let old_tree = parse(input, None);
400 let old_edit = (30, 35);
401 let updated = apply_edit(input, old_edit, "tail section");
402 let new_edit = (30, 42);
403
404 let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit).tree;
405 let full = parse(&updated, None);
406 assert_eq!(inc.to_string(), full.to_string());
407 }
408
409 #[test]
410 fn incremental_suffix_matches_full_parse_for_middle_edit() {
411 let input = "# H\n\n- a\n- b\n\nfinal para\n";
412 let old_tree = parse(input, None);
413 let old_edit = (10, 11);
414 let updated = apply_edit(input, old_edit, "alpha");
415 let new_edit = (10, 15);
416
417 let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit).tree;
418 let full = parse(&updated, None);
419 assert_eq!(inc.to_string(), full.to_string());
420 }
421
422 #[test]
423 fn incremental_suffix_matches_full_parse_for_setext_transition() {
424 let input = "Intro\nSecond\n\nTail\n";
425 let old_tree = parse(input, None);
426 let old_edit = (5, 5);
427 let updated = apply_edit(input, old_edit, "\n-----");
428 let new_edit = (5, 11);
429
430 let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit).tree;
431 let full = parse(&updated, None);
432 assert_eq!(inc.to_string(), full.to_string());
433 }
434
435 #[test]
436 fn incremental_suffix_matches_full_parse_for_lazy_blockquote_change() {
437 let input = "> quoted\nlazy\n\nnext\n";
438 let old_tree = parse(input, None);
439 let old_edit = (9, 13);
440 let updated = apply_edit(input, old_edit, "> line");
441 let new_edit = (9, 15);
442
443 let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit).tree;
444 let full = parse(&updated, None);
445 assert_eq!(inc.to_string(), full.to_string());
446 }
447
448 #[test]
449 fn incremental_uses_heading_section_window_when_available() {
450 let input = "# Intro\n\nalpha\n\n# Middle\n\nbeta section\n\n# End\n\nomega\n";
451 let old_tree = parse(input, None);
452 let start = input.find("beta").expect("beta in test input");
453 let old_edit = (start, start + 4);
454 let updated = apply_edit(input, old_edit, "BETA");
455 let new_edit = (start, start + 4);
456
457 let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit);
458 let full = parse(&updated, None);
459 assert_eq!(inc.tree.to_string(), full.to_string());
460 assert!(
461 inc.reparse_range.0 > 0,
462 "section reparse should not start at 0"
463 );
464 assert!(
465 inc.reparse_range.1 < updated.len(),
466 "section reparse should stop before EOF"
467 );
468 }
469
470 #[test]
471 fn incremental_uses_section_window_for_last_section() {
472 let input = "# Intro\n\nalpha\n\n# Last\n\nbeta section\n";
473 let old_tree = parse(input, None);
474 let start = input.find("beta").expect("beta in test input");
475 let old_edit = (start, start + 4);
476 let updated = apply_edit(input, old_edit, "BETA");
477 let new_edit = (start, start + 4);
478
479 let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit);
480 let full = parse(&updated, None);
481 assert_eq!(inc.tree.to_string(), full.to_string());
482 assert!(
483 inc.reparse_range.0 > 0,
484 "last section should start at the last heading boundary"
485 );
486 assert_eq!(
487 inc.reparse_range.1,
488 updated.len(),
489 "last section should end at EOF"
490 );
491 }
492
493 #[test]
494 fn incremental_does_not_use_section_window_when_edit_touches_heading() {
495 let input = "# Intro\n\nalpha\n\n# Middle\n\nbeta\n\n# End\n\nomega\n";
496 let old_tree = parse(input, None);
497 let middle_start = input
498 .find("# Middle")
499 .expect("middle heading in test input");
500 let old_edit = (middle_start, middle_start + 1);
501 let updated = apply_edit(input, old_edit, "#");
502 let new_edit = (middle_start, middle_start + 1);
503
504 let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit);
505 let full = parse(&updated, None);
506 assert_eq!(inc.tree.to_string(), full.to_string());
507 assert_eq!(
508 inc.reparse_range.1,
509 updated.len(),
510 "edits on headings should avoid section-window reparsing"
511 );
512 }
513
514 #[test]
515 fn incremental_does_not_use_section_window_when_edit_crosses_next_heading() {
516 let input = "# Intro\n\nalpha\n\n# Middle\n\nbeta\n\n# End\n\nomega\n";
517 let old_tree = parse(input, None);
518 let beta_start = input.find("beta").expect("beta in test input");
519 let end_start = input.find("# End").expect("end heading in test input");
520 let old_edit = (beta_start, end_start + 2);
521 let updated = apply_edit(input, old_edit, "beta\n\n# ");
522 let new_edit = (beta_start, beta_start + 8);
523
524 let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit);
525 let full = parse(&updated, None);
526 assert_eq!(inc.tree.to_string(), full.to_string());
527 assert_eq!(
528 inc.reparse_range.1,
529 updated.len(),
530 "cross-heading edits should avoid section-window reparsing"
531 );
532 }
533
534 #[test]
535 fn incremental_ignores_nested_headings_for_window_boundaries() {
536 let input = "# Intro\n\n> ## Nested\n> quote body\n\n# End\n\nomega\n";
537 let old_tree = parse(input, None);
538 let quote_start = input.find("quote body").expect("quote body in test input");
539 let old_edit = (quote_start, quote_start + 5);
540 let updated = apply_edit(input, old_edit, "QUOTE");
541 let new_edit = (quote_start, quote_start + 5);
542
543 let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit);
544 let full = parse(&updated, None);
545 assert_eq!(inc.tree.to_string(), full.to_string());
546 assert!(
547 inc.reparse_range.1 < updated.len(),
548 "window boundary should be the next top-level heading, not nested heading"
549 );
550 }
551
552 #[test]
553 fn incremental_section_window_handles_list_tight_loose_transition() {
554 let input = "# Intro\n\nprelude\n\n# Middle\n\n- one\n- two\n\n# End\n\nomega\n";
555 let old_tree = parse(input, None);
556 let two_start = input.find("- two").expect("list item in test input");
557 let old_edit = (two_start, two_start);
558 let updated = apply_edit(input, old_edit, "\n");
559 let new_edit = (two_start, two_start + 1);
560
561 let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit);
562 let full = parse(&updated, None);
563 assert_eq!(inc.tree.to_string(), full.to_string());
564 assert!(
565 inc.reparse_range.0 > 0 && inc.reparse_range.1 < updated.len(),
566 "list transition inside section should remain section-bounded"
567 );
568 }
569
570 #[test]
571 fn incremental_section_window_handles_blockquote_lazy_transition() {
572 let input = "# Intro\n\nprelude\n\n# Middle\n\n> quoted\nlazy line\n\n# End\n\nomega\n";
573 let old_tree = parse(input, None);
574 let lazy_start = input.find("lazy line").expect("lazy line in test input");
575 let old_edit = (lazy_start, lazy_start);
576 let updated = apply_edit(input, old_edit, "> ");
577 let new_edit = (lazy_start, lazy_start + 2);
578
579 let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit);
580 let full = parse(&updated, None);
581 assert_eq!(inc.tree.to_string(), full.to_string());
582 assert!(
583 inc.reparse_range.0 > 0 && inc.reparse_range.1 < updated.len(),
584 "blockquote continuation change inside section should remain section-bounded"
585 );
586 }
587
588 #[test]
589 fn incremental_section_window_handles_fenced_div_with_nested_heading() {
590 let input = "# Intro\n\nprelude\n\n# Middle\n\n::: {.callout-note}\n## Nested\nbody text\n:::\n\n# End\n\nomega\n";
591 let old_tree = parse(input, None);
592 let body_start = input.find("body text").expect("body text in test input");
593 let old_edit = (body_start, body_start + 4);
594 let updated = apply_edit(input, old_edit, "BODY");
595 let new_edit = (body_start, body_start + 4);
596
597 let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit);
598 let full = parse(&updated, None);
599 assert_eq!(inc.tree.to_string(), full.to_string());
600 assert!(
601 inc.reparse_range.0 > 0 && inc.reparse_range.1 < updated.len(),
602 "fenced div edits should use top-level heading boundaries"
603 );
604 }
605
606 #[test]
607 fn incremental_handles_inserting_heading_inside_section_window() {
608 let input = "# Intro\n\nalpha\n\n# Middle\n\nbeta\n\n# End\n\nomega\n";
609 let old_tree = parse(input, None);
610 let beta_start = input.find("beta").expect("beta in test input");
611 let old_edit = (beta_start, beta_start);
612 let updated = apply_edit(input, old_edit, "## Inserted\n\n");
613 let new_edit = (beta_start, beta_start + "## Inserted\n\n".len());
614
615 let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit);
616 let full = parse(&updated, None);
617 assert_eq!(inc.tree.to_string(), full.to_string());
618 assert_eq!(
619 inc.strategy, "section_window",
620 "heading insertions within a bounded section should remain section-window mode"
621 );
622 }
623
624 #[test]
625 fn incremental_falls_back_when_deleting_next_heading_boundary() {
626 let input = "# Intro\n\nalpha\n\n# Middle\n\nbeta\n\n# End\n\nomega\n";
627 let old_tree = parse(input, None);
628 let end_start = input.find("# End\n").expect("end heading in test input");
629 let old_edit = (end_start, end_start + "# End\n\n".len());
630 let updated = apply_edit(input, old_edit, "");
631 let new_edit = (end_start, end_start);
632
633 let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit);
634 let full = parse(&updated, None);
635 assert_eq!(inc.tree.to_string(), full.to_string());
636 assert_ne!(
637 inc.strategy, "section_window",
638 "heading deletions across boundaries should avoid section-window mode"
639 );
640 }
641
642 #[test]
643 fn incremental_falls_back_when_editing_blank_line_after_heading() {
644 let input = "# Intro\n\nalpha\n\n# Middle\n\nbeta\n\n# End\n\nomega\n";
645 let old_tree = parse(input, None);
646 let boundary = input
647 .find("# Middle\n\n")
648 .expect("middle heading boundary in test input");
649 let blank_line_start = boundary + "# Middle\n".len();
650 let old_edit = (blank_line_start, blank_line_start + 1);
651 let updated = apply_edit(input, old_edit, "");
652 let new_edit = (blank_line_start, blank_line_start);
653
654 let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit);
655 let full = parse(&updated, None);
656 assert_eq!(inc.tree.to_string(), full.to_string());
657 assert_ne!(
658 inc.strategy, "section_window",
659 "heading-adjacent blank line edits should avoid section-window mode"
660 );
661 }
662
663 #[test]
664 fn incremental_handles_frontmatter_to_first_heading_edit() {
665 let input = "---\ntitle: Demo\n---\n\n# Intro\n\nalpha\n\n# Next\n\nomega\n";
666 let old_tree = parse(input, None);
667 let title_start = input.find("Demo").expect("frontmatter value in test input");
668 let old_edit = (title_start, title_start + 4);
669 let updated = apply_edit(input, old_edit, "Updated Demo");
670 let new_edit = (title_start, title_start + "Updated Demo".len());
671
672 let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit);
673 let full = parse(&updated, None);
674 assert_eq!(inc.tree.to_string(), full.to_string());
675 assert_ne!(
676 inc.strategy, "section_window",
677 "frontmatter edits before first heading should use conservative mode"
678 );
679 }
680
681 #[test]
682 fn incremental_handles_frontmatter_delimiter_edit() {
683 let input = "---\ntitle: Demo\n---\n\n# Intro\n\nalpha\n";
684 let old_tree = parse(input, None);
685 let first_delim_start = 0;
686 let old_edit = (first_delim_start, first_delim_start + 3);
687 let updated = apply_edit(input, old_edit, "----");
688 let new_edit = (first_delim_start, first_delim_start + 4);
689
690 let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit);
691 let full = parse(&updated, None);
692 assert_eq!(inc.tree.to_string(), full.to_string());
693 assert_ne!(
694 inc.strategy, "section_window",
695 "frontmatter delimiter edits should stay in conservative mode"
696 );
697 }
698}