fix (most) doubled lines and hanging list markers (#2789)
This was mostly written by codex under heavy guidance via test cases drawn from logged session data and fuzzing. It also uncovered some bugs in tui_markdown, which will in some cases split a list marker from the list item content. We're not addressing those bugs for now.
This commit is contained in:
@@ -435,4 +435,148 @@ mod tests {
|
||||
"Hi! How can I help with codex-rs today? Want me to explore the repo, run tests, or work on a specific change?"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tui_markdown_splits_ordered_marker_and_text() {
|
||||
// With marker and content on the same line, tui_markdown keeps it as one line
|
||||
// even in the surrounding section context.
|
||||
let rendered = tui_markdown::from_str("Loose vs. tight list items:\n1. Tight item\n");
|
||||
let lines: Vec<String> = rendered
|
||||
.lines
|
||||
.iter()
|
||||
.map(|l| {
|
||||
l.spans
|
||||
.iter()
|
||||
.map(|s| s.content.clone())
|
||||
.collect::<String>()
|
||||
})
|
||||
.collect();
|
||||
assert!(
|
||||
lines.iter().any(|w| w == "1. Tight item"),
|
||||
"expected single line '1. Tight item' in context: {lines:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_markdown_matches_tui_markdown_for_ordered_item() {
|
||||
use codex_core::config_types::UriBasedFileOpener;
|
||||
use std::path::Path;
|
||||
let cwd = Path::new("/");
|
||||
let mut out = Vec::new();
|
||||
append_markdown_with_opener_and_cwd(
|
||||
"1. Tight item\n",
|
||||
&mut out,
|
||||
UriBasedFileOpener::None,
|
||||
cwd,
|
||||
);
|
||||
let lines: Vec<String> = out
|
||||
.iter()
|
||||
.map(|l| {
|
||||
l.spans
|
||||
.iter()
|
||||
.map(|s| s.content.clone())
|
||||
.collect::<String>()
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(lines, vec!["1. Tight item".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tui_markdown_shape_for_loose_tight_section() {
|
||||
// Use the exact source from the session deltas used in tests.
|
||||
let source = r#"
|
||||
Loose vs. tight list items:
|
||||
1. Tight item
|
||||
2. Another tight item
|
||||
|
||||
3.
|
||||
Loose item
|
||||
"#;
|
||||
|
||||
let rendered = tui_markdown::from_str(source);
|
||||
let lines: Vec<String> = rendered
|
||||
.lines
|
||||
.iter()
|
||||
.map(|l| {
|
||||
l.spans
|
||||
.iter()
|
||||
.map(|s| s.content.clone())
|
||||
.collect::<String>()
|
||||
})
|
||||
.collect();
|
||||
// Join into a single string and assert the exact shape we observe
|
||||
// from tui_markdown in this larger context (marker and content split).
|
||||
let joined = {
|
||||
let mut s = String::new();
|
||||
for (i, l) in lines.iter().enumerate() {
|
||||
s.push_str(l);
|
||||
if i + 1 < lines.len() {
|
||||
s.push('\n');
|
||||
}
|
||||
}
|
||||
s
|
||||
};
|
||||
let expected = r#"Loose vs. tight list items:
|
||||
|
||||
1.
|
||||
Tight item
|
||||
2.
|
||||
Another tight item
|
||||
3.
|
||||
Loose item"#;
|
||||
assert_eq!(
|
||||
joined, expected,
|
||||
"unexpected tui_markdown shape: {joined:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_text_and_fences_keeps_ordered_list_line_as_text() {
|
||||
// No fences here; expect a single Text segment containing the full input.
|
||||
let src = "Loose vs. tight list items:\n1. Tight item\n";
|
||||
let segs = super::split_text_and_fences(src);
|
||||
assert_eq!(
|
||||
segs.len(),
|
||||
1,
|
||||
"expected single text segment, got {}",
|
||||
segs.len()
|
||||
);
|
||||
match &segs[0] {
|
||||
super::Segment::Text(s) => assert_eq!(s, src),
|
||||
_ => panic!("expected Text segment for non-fence input"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_markdown_keeps_ordered_list_line_unsplit_in_context() {
|
||||
use codex_core::config_types::UriBasedFileOpener;
|
||||
use std::path::Path;
|
||||
let src = "Loose vs. tight list items:\n1. Tight item\n";
|
||||
let cwd = Path::new("/");
|
||||
let mut out = Vec::new();
|
||||
append_markdown_with_opener_and_cwd(src, &mut out, UriBasedFileOpener::None, cwd);
|
||||
|
||||
let lines: Vec<String> = out
|
||||
.iter()
|
||||
.map(|l| {
|
||||
l.spans
|
||||
.iter()
|
||||
.map(|s| s.content.clone())
|
||||
.collect::<String>()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Expect to find the ordered list line rendered as a single line,
|
||||
// not split into a marker-only line followed by the text.
|
||||
assert!(
|
||||
lines.iter().any(|s| s == "1. Tight item"),
|
||||
"expected '1. Tight item' rendered as a single line; got: {lines:?}"
|
||||
);
|
||||
assert!(
|
||||
!lines
|
||||
.windows(2)
|
||||
.any(|w| w[0].trim_end() == "1." && w[1] == "Tight item"),
|
||||
"did not expect a split into ['1.', 'Tight item']; got: {lines:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user