Normalize CRLF line endings in pasted text (#38704)

## Why

Replacing every carriage return with a line feed turns each CRLF pair into two
line breaks when text is pasted into the TUI composer.

## What changed

Normalize CRLF pairs before converting remaining bare carriage returns, so each
pasted line ending becomes a single line feed while existing line feeds remain
unchanged.

## Testing

Add a regression test covering mixed CRLF, bare CR, and LF line endings.

GitOrigin-RevId: 86c66d2a52fae52c9ae58c2f24a49eed685b60be
This commit is contained in:
kevinlin-openai
2026-08-15 05:43:44 +00:00
committed by copyberry
parent e5470f1bce
commit 3685a61dad
2 changed files with 27 additions and 3 deletions
+4 -3
View File
@@ -780,11 +780,12 @@ impl App {
self.handle_key_event(tui, app_server, key_event).await;
}
TuiEvent::Paste(pasted) => {
// Many terminals convert newlines to \r when pasting (e.g., iTerm2),
// but tui-textarea expects \n. Normalize CR to LF.
// Pasted text may contain CRLF pairs or bare CRs (e.g., from iTerm2),
// but tui-textarea expects LF. Normalize CRLF pairs before bare CRs so
// each pasted line break becomes one LF and existing LFs stay unchanged.
// [tui-textarea]: https://github.com/rhysd/tui-textarea/blob/4d18622eeac13b309e0ff6a55a46ac6706da68cf/src/textarea.rs#L782-L783
// [iTerm2]: https://github.com/gnachman/iTerm2/blob/5d0c0d9f68523cbd0494dad5422998964a2ecd8d/sources/iTermPasteHelper.m#L206-L216
let pasted = pasted.replace("\r", "\n");
let pasted = pasted.replace("\r\n", "\n").replace('\r', "\n");
self.chat_widget.handle_paste(pasted);
}
TuiEvent::Draw | TuiEvent::Resume | TuiEvent::Resize(_) => {
+23
View File
@@ -153,6 +153,29 @@ fn test_absolute_path(path: &str) -> AbsolutePathBuf {
AbsolutePathBuf::try_from(PathBuf::from(path)).expect("absolute test path")
}
#[tokio::test]
async fn pasted_text_normalizes_mixed_line_endings_at_app_boundary() -> Result<()> {
let mut app = make_test_app().await;
let mut app_server = start_config_write_test_app_server(&app).await?;
let mut tui = crate::tui::test_support::make_test_tui()?;
app.handle_tui_event(
&mut tui,
&mut app_server,
TuiEvent::Paste("line1\r\nline2\rline3\nline4".to_string()),
)
.await?;
assert_snapshot!(app.chat_widget.composer_text_with_pending(), @r"
line1
line2
line3
line4
");
Ok(())
}
#[tokio::test]
async fn chat_widget_frame_reuses_active_cell_height_across_frame_passes() {
#[derive(Debug)]