mirror of
https://github.com/openai/codex.git
synced 2026-05-05 05:42:33 +03:00
Preserve TUI markdown list spacing after code blocks (#19706)
## Why Fixes #19702. The TUI markdown renderer could visually attach the next list marker to a fenced code block inside the previous list item, even when the source markdown included a blank line before the next item. That made block-heavy loose lists harder to read, while the desired behavior is still to keep simple lists compact. ## What changed - Track whether the current rendered list item contains a code block. - Preserve one blank separator before the following list marker only when the previous item contained a code block. - Add regression coverage for both paths: code-block list items keep the separator, and simple loose list items stay compact. ## Verification - `cargo test -p codex-tui markdown_render` I also manually verified that the bug exists before and is fixed after. ## Before <img width="437" height="240" alt="Screenshot 2026-04-26 at 1 19 01 PM" src="https://github.com/user-attachments/assets/3bc9d64d-2dba-40d9-9d6b-a1d0b3c0f728" /> ## After <img width="410" height="269" alt="Screenshot 2026-04-26 at 1 18 54 PM" src="https://github.com/user-attachments/assets/19c15bee-da32-455e-a7cb-e05eb85f4ea0" />
This commit is contained in:
@@ -154,6 +154,8 @@ where
|
||||
inline_styles: Vec<Style>,
|
||||
indent_stack: Vec<IndentContext>,
|
||||
list_indices: Vec<Option<u64>>,
|
||||
list_needs_blank_before_next_item: Vec<bool>,
|
||||
list_item_contains_code_block: Vec<bool>,
|
||||
link: Option<LinkState>,
|
||||
needs_newline: bool,
|
||||
pending_marker_line: bool,
|
||||
@@ -184,6 +186,8 @@ where
|
||||
inline_styles: Vec::new(),
|
||||
indent_stack: Vec::new(),
|
||||
list_indices: Vec::new(),
|
||||
list_needs_blank_before_next_item: Vec::new(),
|
||||
list_item_contains_code_block: Vec::new(),
|
||||
link: None,
|
||||
needs_newline: false,
|
||||
pending_marker_line: false,
|
||||
@@ -292,6 +296,11 @@ where
|
||||
TagEnd::CodeBlock => self.end_codeblock(),
|
||||
TagEnd::List(_) => self.end_list(),
|
||||
TagEnd::Item => {
|
||||
if self.list_item_contains_code_block.pop().unwrap_or(false)
|
||||
&& let Some(needs_blank) = self.list_needs_blank_before_next_item.last_mut()
|
||||
{
|
||||
*needs_blank = true;
|
||||
}
|
||||
self.indent_stack.pop();
|
||||
self.pending_marker_line = false;
|
||||
}
|
||||
@@ -476,15 +485,26 @@ where
|
||||
self.push_line(Line::default());
|
||||
}
|
||||
self.list_indices.push(index);
|
||||
self.list_needs_blank_before_next_item.push(false);
|
||||
}
|
||||
|
||||
fn end_list(&mut self) {
|
||||
self.list_indices.pop();
|
||||
self.list_needs_blank_before_next_item.pop();
|
||||
self.needs_newline = true;
|
||||
}
|
||||
|
||||
fn start_item(&mut self) {
|
||||
if self
|
||||
.list_needs_blank_before_next_item
|
||||
.last_mut()
|
||||
.map(std::mem::take)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
self.push_blank_line();
|
||||
}
|
||||
self.pending_marker_line = true;
|
||||
self.list_item_contains_code_block.push(false);
|
||||
let depth = self.list_indices.len();
|
||||
let is_ordered = self
|
||||
.list_indices
|
||||
@@ -524,6 +544,9 @@ where
|
||||
}
|
||||
|
||||
fn start_codeblock(&mut self, lang: Option<String>, indent: Option<Span<'static>>) {
|
||||
for item_contains_code_block in &mut self.list_item_contains_code_block {
|
||||
*item_contains_code_block = true;
|
||||
}
|
||||
self.flush_current_line();
|
||||
if !self.text.lines.is_empty() {
|
||||
self.push_blank_line();
|
||||
|
||||
@@ -15,6 +15,18 @@ fn render_markdown_text_for_cwd(input: &str, cwd: &Path) -> Text<'static> {
|
||||
render_markdown_text_with_width_and_cwd(input, /*width*/ None, Some(cwd))
|
||||
}
|
||||
|
||||
fn plain_lines(text: &Text<'_>) -> Vec<String> {
|
||||
text.lines
|
||||
.iter()
|
||||
.map(|line| {
|
||||
line.spans
|
||||
.iter()
|
||||
.map(|span| span.content.clone())
|
||||
.collect::<String>()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
assert_eq!(render_markdown_text(""), Text::default());
|
||||
@@ -1128,6 +1140,46 @@ fn code_block_inside_unordered_list_item_multiple_lines() {
|
||||
assert_eq!(lines, vec!["- Item", "", " first", " second"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_item_after_code_block_keeps_blank_separator() {
|
||||
let md = "1. First:\n\n ```rust\n fn first() {}\n ```\n\n2. Second:\n";
|
||||
let text = render_markdown_text(md);
|
||||
let lines = plain_lines(&text);
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec!["1. First:", "", " fn first() {}", "", "2. Second:"]
|
||||
);
|
||||
assert_snapshot!(
|
||||
"list_item_after_code_block_keeps_blank_separator",
|
||||
lines.join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outer_list_item_after_nested_code_block_keeps_blank_separator() {
|
||||
let md = "1. First:\n - Nested:\n\n ```rust\n fn first() {}\n ```\n\n2. Second:\n";
|
||||
let text = render_markdown_text(md);
|
||||
let lines = plain_lines(&text);
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
"1. First:",
|
||||
" - Nested:",
|
||||
"",
|
||||
" fn first() {}",
|
||||
"",
|
||||
"2. Second:",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_item_after_simple_item_stays_compact() {
|
||||
let md = "1. First\n\n2. Second\n";
|
||||
let text = render_markdown_text(md);
|
||||
assert_eq!(plain_lines(&text), vec!["1. First", "2. Second"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn markdown_render_complex_snapshot() {
|
||||
let md = r#"# H1: Markdown Streaming Test
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
source: tui/src/markdown_render_tests.rs
|
||||
expression: "lines.join(\"\\n\")"
|
||||
---
|
||||
1. First:
|
||||
|
||||
fn first() {}
|
||||
|
||||
2. Second:
|
||||
Reference in New Issue
Block a user