Skip to content

Commit 3db71c7

Browse files
nightt5879Hmbown
authored andcommitted
fix(tui): preserve exact edit preview boundaries
Render Alt+V edit preview chunks with explicit escapes for spaces, tabs, quotes, backslashes, LF, and CRLF so PagerView wrapping cannot collapse source-significant whitespace. Exercise the rendered pager path with mixed indentation and line endings. Addresses Codex review on #4722. Signed-off-by: Nightt <87569709+nightt5879@users.noreply.github.com> (cherry picked from commit a475b65)
1 parent 92087c1 commit 3db71c7

1 file changed

Lines changed: 77 additions & 17 deletions

File tree

crates/tui/src/tui/approval.rs

Lines changed: 77 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,56 @@ fn edit_file_preview_lines(params: &Value, max_lines: usize) -> Option<Vec<Strin
818818
Some(lines)
819819
}
820820

821+
fn exact_edit_file_preview_lines(params: &Value) -> Option<Vec<String>> {
822+
let search = param_text(params, &["search"])?;
823+
let replace = param_text(params, &["replace"])?;
824+
let mut lines = vec!["replace this".to_string()];
825+
lines.extend(exact_preview_body_lines("- ", &search));
826+
lines.push("with this".to_string());
827+
lines.extend(exact_preview_body_lines("+ ", &replace));
828+
Some(lines)
829+
}
830+
831+
fn exact_preview_body_lines(prefix: &str, content: &str) -> Vec<String> {
832+
if content.is_empty() {
833+
return vec![format!("{prefix}\"\"")];
834+
}
835+
836+
content
837+
.split_inclusive('\n')
838+
.map(|chunk| {
839+
let (body, ending) = if let Some(body) = chunk.strip_suffix("\r\n") {
840+
(body, "\\r\\n")
841+
} else if let Some(body) = chunk.strip_suffix('\n') {
842+
(body, "\\n")
843+
} else {
844+
(chunk, "")
845+
};
846+
exact_preview_body_line(prefix, body, ending)
847+
})
848+
.collect()
849+
}
850+
851+
fn exact_preview_body_line(prefix: &str, body: &str, ending: &str) -> String {
852+
let mut line = String::with_capacity(prefix.len() + body.len() + ending.len() + 2);
853+
line.push_str(prefix);
854+
line.push('"');
855+
for ch in body.chars() {
856+
match ch {
857+
'\\' => line.push_str("\\\\"),
858+
'"' => line.push_str("\\\""),
859+
' ' => line.push_str("\\x20"),
860+
'\t' => line.push_str("\\t"),
861+
'\r' => line.push_str("\\r"),
862+
ch if ch.is_whitespace() || ch.is_control() => line.extend(ch.escape_unicode()),
863+
ch => line.push(ch),
864+
}
865+
}
866+
line.push_str(ending);
867+
line.push('"');
868+
line
869+
}
870+
821871
fn prefixed_preview_lines(
822872
header: &str,
823873
prefix: &str,
@@ -1359,7 +1409,7 @@ impl ApprovalView {
13591409
}
13601410
content.push('\n');
13611411
if canonical_action_alias(&self.request.tool_name, &self.request.params) == "edit_file"
1362-
&& let Some(preview_lines) = edit_file_preview_lines(&self.request.params, usize::MAX)
1412+
&& let Some(preview_lines) = exact_edit_file_preview_lines(&self.request.params)
13631413
{
13641414
content.push_str(&localize_detail_label("Preview", locale));
13651415
content.push_str(":\n");
@@ -3052,8 +3102,8 @@ diff --git a/src/b.rs b/src/b.rs
30523102
"Edit a file on disk",
30533103
&json!({
30543104
"path": "src/lib.rs",
3055-
"search": "old_1();\nold_2();\nold_3();\nold_4();\nold_5();",
3056-
"replace": "new_1();\nnew_2();\nnew_3();\nnew_4();\nnew_5();"
3105+
"search": " old_1();\r\n\told_2();\nold 3();\nold_4();\nold_5();\n",
3106+
"replace": "\tnew_1();\nnew 2();\r\nnew_3();\nnew_4();\nnew_5();"
30573107
}),
30583108
"tool:edit_file",
30593109
);
@@ -3064,23 +3114,33 @@ diff --git a/src/b.rs b/src/b.rs
30643114
panic!("Alt+V should open the edit details pager");
30653115
};
30663116

3067-
let expected_preview = "Preview:\n\
3068-
replace this\n\
3069-
- old_1();\n\
3070-
- old_2();\n\
3071-
- old_3();\n\
3072-
- old_4();\n\
3073-
- old_5();\n\
3074-
with this\n\
3075-
+ new_1();\n\
3076-
+ new_2();\n\
3077-
+ new_3();\n\
3078-
+ new_4();\n\
3079-
+ new_5();";
3117+
let expected_preview = [
3118+
"Preview:",
3119+
"replace this",
3120+
"- \"\\x20\\x20old_1();\\r\\n\"",
3121+
"- \"\\told_2();\\n\"",
3122+
"- \"old\\x20\\x203();\\n\"",
3123+
"- \"old_4();\\n\"",
3124+
"- \"old_5();\\n\"",
3125+
"with this",
3126+
"+ \"\\tnew_1();\\n\"",
3127+
"+ \"new\\x20\\x202();\\r\\n\"",
3128+
"+ \"new_3();\\n\"",
3129+
"+ \"new_4();\\n\"",
3130+
"+ \"new_5();\"",
3131+
]
3132+
.join("\n");
30803133
assert!(
3081-
content.contains(expected_preview),
3134+
content.contains(&expected_preview),
30823135
"details pager omitted part of the edit preview:\n{content}"
30833136
);
3137+
3138+
let pager = crate::tui::pager::PagerView::from_text("Tool Params", &content, 200);
3139+
let displayed = pager.body_text();
3140+
assert!(
3141+
displayed.contains(&expected_preview),
3142+
"details pager display changed exact whitespace or line endings:\n{displayed}"
3143+
);
30843144
}
30853145

30863146
#[test]

0 commit comments

Comments
 (0)