mirror of
https://github.com/openai/codex.git
synced 2026-11-06 20:20:34 +00:00
Fix tui2 code block wrapping and copy
- wrap preformatted markdown lines with joiners so code fences reflow instead of truncating - preserve code block copy fidelity by joining soft-wrap segments and trimming repeated indent - extend markdown/code snapshots with long fenced lines to cover wrapping
This commit is contained in:
@@ -3574,7 +3574,8 @@ printf 'fenced within fenced\n'
|
||||
{
|
||||
// comment allowed in jsonc
|
||||
"path": "C:\\Program Files\\App",
|
||||
"regex": "^foo.*(bar)?$"
|
||||
"regex": "^foo.*(bar)?$",
|
||||
"long": "this code line is intentionally very very long so that it wraps inside the viewport without being truncated on the right side"
|
||||
}
|
||||
```
|
||||
"#;
|
||||
|
||||
+37
-1
@@ -1,7 +1,41 @@
|
||||
---
|
||||
source: tui2/src/chatwidget/tests.rs
|
||||
assertion_line: 3280
|
||||
expression: term.backend().vt100().screen().contents()
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
• -- Indented code block (4 spaces)
|
||||
SELECT *
|
||||
FROM "users"
|
||||
@@ -14,5 +48,7 @@ expression: term.backend().vt100().screen().contents()
|
||||
{
|
||||
// comment allowed in jsonc
|
||||
"path": "C:\\Program Files\\App",
|
||||
"regex": "^foo.*(bar)?$"
|
||||
"regex": "^foo.*(bar)?$",
|
||||
"long": "this code line is intentionally very very long so that it wraps
|
||||
inside the viewport without being truncated on the right side"
|
||||
}
|
||||
|
||||
@@ -3226,7 +3226,8 @@ printf 'fenced within fenced\n'
|
||||
{
|
||||
// comment allowed in jsonc
|
||||
"path": "C:\\Program Files\\App",
|
||||
"regex": "^foo.*(bar)?$"
|
||||
"regex": "^foo.*(bar)?$",
|
||||
"long": "this code line is intentionally very very long so that it wraps inside the viewport without being truncated on the right side"
|
||||
}
|
||||
```
|
||||
"#;
|
||||
|
||||
@@ -442,24 +442,6 @@ impl HistoryCell for AgentMessageCell {
|
||||
Line::from(spans)
|
||||
};
|
||||
|
||||
// Preformatted lines are rendered as a single visual line (no wrapping).
|
||||
// This preserves code-block whitespace and keeps code copy behavior stable.
|
||||
if logical.is_preformatted {
|
||||
let mut spans = gutter_first_visual_line.spans.clone();
|
||||
spans.extend(logical.initial_indent.spans.iter().cloned());
|
||||
spans.extend(logical.content.spans.iter().cloned());
|
||||
out_lines.push(Line::from(spans).style(logical.line_style));
|
||||
joiner_before.push(None);
|
||||
at_cell_start = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Prose path: wrap to current width and capture joiners.
|
||||
//
|
||||
// `word_wrap_line_with_joiners` guarantees:
|
||||
// - `wrapped.len() == wrapped_joiners.len()`
|
||||
// - `wrapped_joiners[0] == None` (first visual segment of a logical line is a hard break)
|
||||
// - subsequent entries are `Some(joiner)` (soft-wrap continuations).
|
||||
let opts = RtOptions::new(width as usize)
|
||||
.initial_indent(compose_indent(
|
||||
&gutter_first_visual_line,
|
||||
@@ -470,6 +452,25 @@ impl HistoryCell for AgentMessageCell {
|
||||
&logical.subsequent_indent,
|
||||
));
|
||||
|
||||
// Preformatted lines still wrap visually, but we capture joiners so copy/paste can
|
||||
// reconstruct the original logical line without inserting hard breaks.
|
||||
if logical.is_preformatted {
|
||||
let (wrapped, wrapped_joiners) =
|
||||
crate::wrapping::word_wrap_line_with_joiners(&logical.content, opts);
|
||||
for (visual, joiner) in wrapped.into_iter().zip(wrapped_joiners) {
|
||||
out_lines.push(line_to_static(&visual).style(logical.line_style));
|
||||
joiner_before.push(joiner);
|
||||
at_cell_start = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Prose path: wrap to current width and capture joiners.
|
||||
//
|
||||
// `word_wrap_line_with_joiners` guarantees:
|
||||
// - `wrapped.len() == wrapped_joiners.len()`
|
||||
// - `wrapped_joiners[0] == None` (first visual segment of a logical line is a hard break)
|
||||
// - subsequent entries are `Some(joiner)` (soft-wrap continuations).
|
||||
let (wrapped, wrapped_joiners) =
|
||||
crate::wrapping::word_wrap_line_with_joiners(&logical.content, opts);
|
||||
for (visual, joiner) in wrapped.into_iter().zip(wrapped_joiners) {
|
||||
@@ -1917,6 +1918,44 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_message_cell_wraps_preformatted_lines_with_joiners() {
|
||||
let logical_lines = vec![crate::markdown_render::MarkdownLogicalLine {
|
||||
content: Line::from(
|
||||
"let long_code_line = call_with_many_arguments(1, 2, 3, 4); and_more_tokens();",
|
||||
),
|
||||
initial_indent: Line::from(" "),
|
||||
subsequent_indent: Line::from(" "),
|
||||
line_style: Style::new().fg(ratatui::style::Color::Cyan),
|
||||
is_preformatted: true,
|
||||
}];
|
||||
let cell = AgentMessageCell::new_logical(logical_lines, true);
|
||||
|
||||
let rendered = cell.transcript_lines_with_joiners(40);
|
||||
|
||||
assert!(
|
||||
rendered.lines.len() > 1,
|
||||
"expected wrapped output, got {:?}",
|
||||
render_lines(&rendered.lines)
|
||||
);
|
||||
assert_eq!(rendered.joiner_before.first(), Some(&None));
|
||||
assert!(
|
||||
rendered
|
||||
.joiner_before
|
||||
.iter()
|
||||
.skip(1)
|
||||
.all(std::option::Option::is_some),
|
||||
"expected soft-wrap joiners for continuations: {:?}",
|
||||
rendered.joiner_before
|
||||
);
|
||||
assert!(
|
||||
rendered
|
||||
.lines
|
||||
.iter()
|
||||
.all(|line| line.style.fg == Some(ratatui::style::Color::Cyan))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mcp_tools_output_masks_sensitive_values() {
|
||||
let mut config = test_config().await;
|
||||
|
||||
@@ -152,6 +152,7 @@ pub(crate) fn selection_to_copy_text(
|
||||
}
|
||||
|
||||
let line = lines.get(line_index)?;
|
||||
let joiner = joiner_before.get(line_index).cloned().unwrap_or(None);
|
||||
|
||||
// Code blocks (and other preformatted content) are detected via styling and copied as
|
||||
// "verbatim lines" (no inline Markdown re-encoding). This also enables special handling for
|
||||
@@ -213,7 +214,7 @@ pub(crate) fn selection_to_copy_text(
|
||||
// Convert the selected `Line` into Markdown source:
|
||||
// - For prose: wrap inline-code spans in backticks.
|
||||
// - For code blocks: return the raw flat text so we preserve indentation/spacing.
|
||||
let line_text = line_to_markdown(&selected_line, is_code_block_line);
|
||||
let mut line_text = line_to_markdown(&selected_line, is_code_block_line);
|
||||
|
||||
// Track transitions into/out of code/preformatted runs and emit triple-backtick fences.
|
||||
// We always separate a code run from prior prose with a newline.
|
||||
@@ -236,11 +237,23 @@ pub(crate) fn selection_to_copy_text(
|
||||
}
|
||||
|
||||
// When copying inside a code run, every selected visual line becomes a literal line inside
|
||||
// the fence (no soft-wrap joining). We preserve explicit blank lines by writing empty
|
||||
// strings as a line.
|
||||
// the fence. We preserve explicit blank lines by writing empty strings as a line. When the
|
||||
// on-screen line is a soft-wrap continuation, we join with the recorded wrap joiner instead
|
||||
// of inserting a hard newline so long logical lines stay intact.
|
||||
if in_code_run {
|
||||
if wrote_any && (!out.ends_with('\n') || prev_selected_line.is_some()) {
|
||||
out.push('\n');
|
||||
let is_continuation =
|
||||
prev_selected_line == Some(line_index.saturating_sub(1)) && joiner.is_some();
|
||||
if wrote_any {
|
||||
if prev_selected_line == Some(line_index.saturating_sub(1))
|
||||
&& let Some(joiner) = &joiner
|
||||
{
|
||||
out.push_str(joiner.as_str());
|
||||
} else if !out.ends_with('\n') || prev_selected_line.is_some() {
|
||||
out.push('\n');
|
||||
}
|
||||
}
|
||||
if is_continuation {
|
||||
line_text = line_text.trim_start_matches(' ').to_string();
|
||||
}
|
||||
out.push_str(line_text.as_str());
|
||||
prev_selected_line = Some(line_index);
|
||||
@@ -253,7 +266,6 @@ pub(crate) fn selection_to_copy_text(
|
||||
// recorded joiner (often spaces) instead of a newline.
|
||||
// - Otherwise, insert a newline to preserve hard breaks.
|
||||
if wrote_any {
|
||||
let joiner = joiner_before.get(line_index).cloned().unwrap_or(None);
|
||||
if prev_selected_line == Some(line_index.saturating_sub(1))
|
||||
&& let Some(joiner) = joiner
|
||||
{
|
||||
@@ -751,6 +763,34 @@ mod tests {
|
||||
assert_eq!(out, "```\n fn main() {}\n println!(\"hi\");\n```");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_selection_soft_wrapped_code_joins_segments() {
|
||||
let style = Style::new().fg(Color::Cyan);
|
||||
let lines = vec![
|
||||
Line::from("• let very_long_code_line = call_with_many_arguments(1, 2, 3, 4);")
|
||||
.style(style),
|
||||
Line::from(" // trailing comment continues here").style(style),
|
||||
];
|
||||
let joiner_before = vec![None, Some(" ".to_string())];
|
||||
|
||||
let start = TranscriptSelectionPoint {
|
||||
line_index: 0,
|
||||
column: 0,
|
||||
};
|
||||
let end = TranscriptSelectionPoint {
|
||||
line_index: 1,
|
||||
column: 120,
|
||||
};
|
||||
|
||||
let out = selection_to_copy_text(&lines, &joiner_before, start, end, 0, lines.len(), 80)
|
||||
.expect("expected text");
|
||||
|
||||
assert_eq!(
|
||||
out,
|
||||
"```\n let very_long_code_line = call_with_many_arguments(1, 2, 3, 4); // trailing comment continues here\n```"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_selection_code_block_end_col_at_viewport_edge_copies_full_line() {
|
||||
let style = Style::new().fg(Color::Cyan);
|
||||
|
||||
@@ -199,8 +199,8 @@ pub(crate) fn append_wrapped_transcript_cell(
|
||||
line_in_cell: visual_line_in_cell,
|
||||
});
|
||||
visual_line_in_cell = visual_line_in_cell.saturating_add(1);
|
||||
// Preformatted lines are treated as hard breaks; we keep the cell-provided joiner
|
||||
// (which is typically `None`).
|
||||
// Preformatted lines are treated as hard breaks; we keep the cell-provided joiner (which
|
||||
// may describe an earlier soft wrap inside the cell).
|
||||
out.joiner_before.push(
|
||||
rendered
|
||||
.joiner_before
|
||||
|
||||
Reference in New Issue
Block a user