Skip to content

Commit ca47dae

Browse files
fix(parser): guarantee non-inverted content_position for comment-only bindings
For a binding whose content is only a comment after leading whitespace, the modified doc (comments replaced by whitespace) is all whitespace, so its trailing trim spans the whole content while the start bound trims the original text: content_position ended up with start > end (e.g. "---\n#b1\n /* c */\n" gave [10, 8]). Trim the end bound against the original text in that case so the range stays valid and covers the comment, matching pre-#114-fix behavior for comment-only bindings while keeping the comment-before-next-binding exclusion for bindings with real content. Adds mutation-validated tests for the comment-only case (all four repro shapes) and for preservation of a comment at the start of a binding's own content. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 24a86b5 commit ca47dae

2 files changed

Lines changed: 81 additions & 1 deletion

File tree

crates/dotrain/src/parser/raindocument/logic.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -790,15 +790,24 @@ impl RainDocument {
790790
// at the start of a binding's content are preserved (not skipped).
791791
// For content_position end: use trailing trim from modified doc (raw_trimmed.2)
792792
// so comments before the next #-binding are excluded from this binding.
793+
// When the modified content is all whitespace (comment-only binding),
794+
// raw_trimmed.2 spans the whole content, which would put the end before
795+
// the start; trim the end against the original text instead so the range
796+
// stays valid and covers the comment.
793797
content_position = if trimmed_orig.0.is_empty() {
794798
[
795799
parsed_binding.1[0] + boundry_offset + 1,
796800
parsed_binding.1[1],
797801
]
798802
} else {
803+
let end_trim = if raw_trimmed.0.is_empty() {
804+
trimmed_orig.2
805+
} else {
806+
raw_trimmed.2
807+
};
799808
[
800809
parsed_binding.1[0] + boundry_offset + 1 + trimmed_orig.1,
801-
parsed_binding.1[1] - raw_trimmed.2,
810+
parsed_binding.1[1] - end_trim,
802811
]
803812
};
804813
content = self

crates/dotrain/src/parser/raindocument/mod.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -991,4 +991,75 @@ _: opcode-1(0xabcd 456);
991991
.expect("binding b2 not found");
992992
assert_eq!(b2.content, "! elided2");
993993
}
994+
995+
// Mutation-validated: make the content_position end bound always use the
996+
// modified-doc trailing trim (raw_trimmed.2) even for comment-only content and
997+
// this test fails because start > end.
998+
#[test]
999+
fn test_comment_only_binding_content_position_not_inverted() {
1000+
let meta_store = Arc::new(RwLock::new(Store::new()));
1001+
1002+
// Comment-only bindings with leading whitespace: the range must stay valid
1003+
// (start <= end) and cover the comment.
1004+
let cases: [(&str, [usize; 2], &str); 4] = [
1005+
("---\n#b1\n /* c */\n#b2\n! e2\n", [10, 17], "/* c */"),
1006+
("---\n#b1\n /* c */\n", [10, 17], "/* c */"),
1007+
(
1008+
"---\n#b1\n /* xxxxx */\n#b2\n! e2\n",
1009+
[16, 27],
1010+
"/* xxxxx */",
1011+
),
1012+
("---\n#b1\n /* c */", [11, 18], "/* c */"),
1013+
];
1014+
for (text, expected_position, expected_content) in cases {
1015+
let rain_document =
1016+
RainDocument::create(text.to_owned(), Some(meta_store.clone()), None, None);
1017+
let b1 = rain_document
1018+
.bindings()
1019+
.iter()
1020+
.find(|b| b.name == "b1")
1021+
.expect("binding b1 not found");
1022+
1023+
assert!(
1024+
b1.content_position[0] <= b1.content_position[1],
1025+
"inverted content_position {:?} for {text:?}",
1026+
b1.content_position
1027+
);
1028+
assert_eq!(b1.content_position, expected_position, "for {text:?}");
1029+
assert_eq!(b1.content, expected_content, "for {text:?}");
1030+
assert_eq!(
1031+
text.get(b1.content_position[0]..b1.content_position[1])
1032+
.unwrap(),
1033+
expected_content,
1034+
"for {text:?}"
1035+
);
1036+
}
1037+
}
1038+
1039+
// Mutation-validated: make the content_position start bound use the modified-doc
1040+
// leading trim (raw_trimmed.1) instead of the original text's and this test fails
1041+
// because the doc comment is skipped.
1042+
#[test]
1043+
fn test_comment_at_start_of_binding_content_preserved() {
1044+
let meta_store = Arc::new(RwLock::new(Store::new()));
1045+
1046+
// A comment at the start of a binding's own content area belongs to that
1047+
// binding: it stays inside content and content_position.
1048+
let text = "---\n#b1\n/* doc */ _: 1;";
1049+
let rain_document =
1050+
RainDocument::create(text.to_owned(), Some(meta_store.clone()), None, None);
1051+
let b1 = rain_document
1052+
.bindings()
1053+
.iter()
1054+
.find(|b| b.name == "b1")
1055+
.expect("binding b1 not found");
1056+
1057+
assert_eq!(b1.content, "/* doc */ _: 1;");
1058+
assert_eq!(b1.content_position, [8, 23]);
1059+
assert_eq!(
1060+
text.get(b1.content_position[0]..b1.content_position[1])
1061+
.unwrap(),
1062+
"/* doc */ _: 1;"
1063+
);
1064+
}
9941065
}

0 commit comments

Comments
 (0)