use crate::common::git_test_helper::{DirGuard, GitTestRepo};
use crate::common::harness::EditorTestHarness;
use crossterm::event::{KeyCode, KeyModifiers};
use fresh::config::Config;
fn content_lines(screen: &str) -> Vec<&str> {
let lines: Vec<&str> = screen.lines().collect();
let start = 2;
let end = lines.len().saturating_sub(2);
if end > start {
lines[start..end].to_vec()
} else {
Vec::new()
}
}
fn has_glyph(screen: &str, glyph: char) -> bool {
for line in content_lines(screen) {
if line.chars().any(|c| c == glyph) {
return true;
}
}
false
}
fn has_text(screen: &str, text: &str) -> bool {
content_lines(screen).iter().any(|l| l.contains(text))
}
fn open_file(harness: &mut EditorTestHarness, repo_path: &std::path::Path, relative: &str) {
let full = repo_path.join(relative);
harness.open_file(&full).unwrap();
harness
.wait_until(|h| h.screen_to_string().contains(relative))
.unwrap();
}
fn enable_live_diff_globally(harness: &mut EditorTestHarness) {
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Live Diff: Toggle (Global)").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
}
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_live_diff_added_line_shows_plus_in_gutter() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_live_diff_plugin();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
repo.modify_file(
"src/utils.rs",
r#"// brand new top line added by the agent
pub fn format_output(msg: &str) -> String {
format!("[INFO] {}", msg)
}
pub fn validate_config(config: &Config) -> bool {
config.port > 0 && !config.host.is_empty()
}
"#,
);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
enable_live_diff_globally(&mut harness);
open_file(&mut harness, &repo.path, "src/utils.rs");
harness
.wait_until(|h| has_glyph(&h.screen_to_string(), '+'))
.unwrap();
}
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_live_diff_modified_line_shows_old_content_above() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_live_diff_plugin();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
repo.modify_file(
"src/utils.rs",
r#"pub fn format_output(msg: &str) -> String {
panic!("EXTENSIVELY_REWRITTEN_BODY_LIVE_DIFF_REPLACED_LINE_WITH_LONG_UNIQUE_PAYLOAD_TO_DROP_SIMILARITY");
}
pub fn validate_config(config: &Config) -> bool {
config.port > 0 && !config.host.is_empty()
}
"#,
);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
enable_live_diff_globally(&mut harness);
open_file(&mut harness, &repo.path, "src/utils.rs");
harness
.wait_until(|h| has_glyph(&h.screen_to_string(), '-'))
.unwrap();
harness
.wait_until(|h| has_text(&h.screen_to_string(), "[INFO]"))
.unwrap();
let screen = harness.screen_to_string();
assert!(
has_text(&screen, "LIVE_DIFF_REPLACED_LINE"),
"expected new content visible:\n{screen}"
);
}
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_live_diff_updates_on_buffer_edit() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_live_diff_plugin();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
enable_live_diff_globally(&mut harness);
open_file(&mut harness, &repo.path, "src/utils.rs");
harness
.wait_until(|h| h.screen_to_string().contains("format_output"))
.unwrap();
harness.type_text("// LIVE_DIFF_TYPED_INSERT\n").unwrap();
harness.render().unwrap();
harness
.wait_until(|h| {
let s = h.screen_to_string();
has_glyph(&s, '+') && has_text(&s, "LIVE_DIFF_TYPED_INSERT")
})
.unwrap();
}
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_live_diff_handles_surrogate_pair_content() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_live_diff_plugin();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
repo.modify_file(
"src/utils.rs",
"pub fn format_output(msg: &str) -> String {\n \
format!(\"\u{1F389} {}\", msg)\n}\n\n\
pub fn validate_config(config: &Config) -> bool {\n \
config.port > 0 && !config.host.is_empty()\n}\n",
);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
enable_live_diff_globally(&mut harness);
open_file(&mut harness, &repo.path, "src/utils.rs");
harness
.wait_until(|h| has_glyph(&h.screen_to_string(), '~'))
.unwrap();
}
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_live_diff_highlights_empty_added_line() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_live_diff_plugin();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
repo.modify_file(
"src/utils.rs",
"pub fn format_output(msg: &str) -> String {\n \
format!(\"[INFO] {}\", msg)\n}\n\n\
pub fn validate_config(config: &Config) -> bool {\n \
config.port > 0 && !config.host.is_empty()\n}\n\
\n\
pub fn UNIQUE_NEW_FN_MARKER() {}\n\
\n",
);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
enable_live_diff_globally(&mut harness);
open_file(&mut harness, &repo.path, "src/utils.rs");
harness
.wait_until(|h| {
let s = h.screen_to_string();
s.contains("UNIQUE_NEW_FN_MARKER") && has_glyph(&s, '+')
})
.unwrap();
let buf = harness.buffer();
let mut marker_row: Option<u16> = None;
for y in 0..buf.area.height {
let mut row = String::new();
for x in 0..buf.area.width {
row.push_str(buf[(x, y)].symbol());
}
if row.contains("UNIQUE_NEW_FN_MARKER") {
marker_row = Some(y);
break;
}
}
let marker_row = marker_row.expect("never found new fn on screen");
assert!(
marker_row > 0,
"expected an empty added line above the new fn",
);
let empty_row = marker_row - 1;
let bg = buf[(40, empty_row)].style().bg;
assert_eq!(
bg,
Some(ratatui::style::Color::Rgb(0, 80, 0)),
"empty added line at row {empty_row} should have the green \
diff_add_bg out to col 40; saw {bg:?}",
);
}
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_live_diff_highlights_empty_removed_line() {
let repo = GitTestRepo::new();
repo.setup_live_diff_plugin();
repo.create_file("src/utils.rs", "head\n\ntail\n");
repo.git_add(&["src/utils.rs"]);
repo.git_commit("init");
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
repo.modify_file("src/utils.rs", "head\ntail\n");
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
20,
Config::default(),
repo.path.clone(),
)
.unwrap();
enable_live_diff_globally(&mut harness);
open_file(&mut harness, &repo.path, "src/utils.rs");
harness
.wait_until(|h| {
let s = h.screen_to_string();
has_glyph(&s, '-') && has_text(&s, "tail")
})
.unwrap();
let buf = harness.buffer();
let mut empty_del_row: Option<u16> = None;
for y in 0..buf.area.height {
let mut row = String::new();
for x in 0..buf.area.width {
row.push_str(buf[(x, y)].symbol());
}
let trimmed = row.trim_end();
if trimmed.contains('-')
&& trimmed.contains('│')
&& trimmed
.split('│')
.nth(1)
.is_some_and(|body| body.chars().all(|c| c.is_whitespace()))
{
empty_del_row = Some(y);
break;
}
}
let empty_del_row = empty_del_row.expect("never found an empty deletion virtual row on screen");
let bg = buf[(40, empty_del_row)].style().bg;
assert_eq!(
bg,
Some(ratatui::style::Color::Rgb(100, 0, 0)),
"empty deletion virtual row at y={empty_del_row} should be \
filled with diff_remove_bg (100, 0, 0) at col 40; saw {bg:?}",
);
}
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_live_diff_does_not_skip_empty_lines_on_arrow_keys() {
use crossterm::event::{KeyCode, KeyModifiers};
let repo = GitTestRepo::new();
repo.setup_live_diff_plugin();
repo.create_file("src/utils.rs", "head\n");
repo.git_add(&["src/utils.rs"]);
repo.git_commit("init");
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
repo.modify_file("src/utils.rs", "head\n\n\ntail\n");
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
enable_live_diff_globally(&mut harness);
open_file(&mut harness, &repo.path, "src/utils.rs");
harness
.wait_until(|h| has_glyph(&h.screen_to_string(), '+'))
.unwrap();
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
let pos0 = harness.cursor_position();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
let pos1 = harness.cursor_position();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
let pos2 = harness.cursor_position();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
let pos3 = harness.cursor_position();
assert_eq!(pos0, 0, "expected cursor at start");
assert_eq!(
pos1, 5,
"Down once should land at first empty line (byte 5); saw byte {pos1}",
);
assert_eq!(
pos2, 6,
"Down twice should land at second empty line (byte 6); saw byte {pos2}",
);
assert_eq!(
pos3, 7,
"Down thrice should land on 'tail' (byte 7); saw byte {pos3}",
);
}
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_live_diff_virtual_line_anchored_to_correct_modified_line() {
let repo = GitTestRepo::new();
repo.setup_live_diff_plugin();
let mut head_lines = Vec::with_capacity(1290);
for i in 1..=1280 {
head_lines.push(format!("FILLER_LINE_NUMBER_{i:04}_unique"));
}
head_lines.push(" let goal = if cond {".into());
head_lines.push(" UNIQUE_IF_BODY_OLD_MARKER".into());
head_lines.push(" } else {".into());
head_lines.push(" UNIQUE_ELSE_BODY_OLD_MARKER".into());
head_lines.push(" };".into());
let head_text = head_lines.join("\n") + "\n";
repo.create_file("code.rs", &head_text);
repo.git_add(&["code.rs"]);
repo.git_commit("init");
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
220,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
enable_live_diff_globally(&mut harness);
open_file(&mut harness, &repo.path, "code.rs");
const APPEND_PAYLOAD: &str =
" + REWRITE_PAYLOAD_DROPS_SIMILARITY_BELOW_HALF_THRESHOLD_LIVE_DIFF_REGRESSION_PADDING_xyz_ABC_DEF_GHI_001";
use crossterm::event::{KeyCode, KeyModifiers};
harness
.send_key(KeyCode::End, KeyModifiers::CONTROL)
.unwrap();
for _ in 0..4 {
harness.send_key(KeyCode::Up, KeyModifiers::NONE).unwrap();
}
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness.type_text(APPEND_PAYLOAD).unwrap();
harness.render().unwrap();
let virtual_row_present = |screen: &str, marker: &str| {
screen
.lines()
.any(|l| l.contains(marker) && !l.contains(APPEND_PAYLOAD))
};
harness
.wait_until(|h| virtual_row_present(&h.screen_to_string(), "UNIQUE_IF_BODY_OLD_MARKER"))
.unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness.type_text(APPEND_PAYLOAD).unwrap();
harness.render().unwrap();
harness
.wait_until(|h| {
let s = h.screen_to_string();
virtual_row_present(&s, "UNIQUE_IF_BODY_OLD_MARKER")
&& virtual_row_present(&s, "UNIQUE_ELSE_BODY_OLD_MARKER")
})
.unwrap();
let buf = harness.buffer();
let rows: Vec<String> = (0..buf.area.height)
.map(|y| {
(0..buf.area.width)
.map(|x| buf[(x, y)].symbol().to_string())
.collect::<String>()
})
.collect();
let dump = || {
rows.iter()
.enumerate()
.map(|(i, r)| format!("{i:3} | {}", r.trim_end()))
.collect::<Vec<_>>()
.join("\n")
};
let row_new_top = rows
.iter()
.position(|r| r.contains("UNIQUE_IF_BODY_OLD_MARKER") && r.contains(APPEND_PAYLOAD))
.unwrap_or_else(|| panic!("new top line not on screen. screen:\n{}", dump()));
let row_else = rows
.iter()
.position(|r| r.contains("} else {"))
.unwrap_or_else(|| panic!("unchanged else line not on screen. screen:\n{}", dump()));
let row_new_bot = rows
.iter()
.position(|r| r.contains("UNIQUE_ELSE_BODY_OLD_MARKER") && r.contains(APPEND_PAYLOAD))
.unwrap_or_else(|| panic!("new bot line not on screen. screen:\n{}", dump()));
let row_old_top = rows
.iter()
.position(|r| r.contains("UNIQUE_IF_BODY_OLD_MARKER") && !r.contains(APPEND_PAYLOAD))
.unwrap_or_else(|| panic!("old top virtual line not on screen. screen:\n{}", dump()));
let row_old_bot = rows
.iter()
.position(|r| r.contains("UNIQUE_ELSE_BODY_OLD_MARKER") && !r.contains(APPEND_PAYLOAD))
.unwrap_or_else(|| panic!("old bot virtual line not on screen. screen:\n{}", dump()));
assert_eq!(
row_old_top + 1,
row_new_top,
"OLD top virtual line ({row_old_top}) should be directly above NEW top ({row_new_top})",
);
assert!(
row_new_top < row_else,
"NEW top row ({row_new_top}) should come before the unchanged else row ({row_else})",
);
assert_eq!(
row_old_bot + 1,
row_new_bot,
"OLD bot virtual line ({row_old_bot}) should be directly above NEW bot ({row_new_bot}); \
the user-reported bug puts it above the unchanged 'else' line instead",
);
assert!(
row_else < row_old_bot,
"unchanged 'else' row ({row_else}) should come before OLD bot virtual line ({row_old_bot})",
);
}
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_live_diff_down_arrow_traverses_deletion_block() {
use crossterm::event::{KeyCode, KeyModifiers};
let repo = GitTestRepo::new();
repo.setup_live_diff_plugin();
let mut head_lines = Vec::new();
for i in 1..=5 {
head_lines.push(format!("before_{i:02}"));
}
head_lines.push(String::new());
head_lines.push("DELETED_LINE_02".into());
head_lines.push(String::new());
head_lines.push("DELETED_LINE_04".into());
head_lines.push(String::new());
head_lines.push("DELETED_LINE_06".into());
head_lines.push(String::new());
for i in 1..=5 {
head_lines.push(format!("after_{i:02}"));
}
let head_text = head_lines.join("\n") + "\n";
repo.create_file("src/utils.rs", &head_text);
repo.git_add(&["src/utils.rs"]);
repo.git_commit("init");
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut work_lines = Vec::new();
for i in 1..=5 {
work_lines.push(format!("before_{i:02}"));
}
for i in 1..=5 {
work_lines.push(format!("after_{i:02}"));
}
let work_text = work_lines.join("\n") + "\n";
repo.modify_file("src/utils.rs", &work_text);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
18,
Config::default(),
repo.path.clone(),
)
.unwrap();
enable_live_diff_globally(&mut harness);
open_file(&mut harness, &repo.path, "src/utils.rs");
harness
.wait_until(|h| {
let s = h.screen_to_string();
has_glyph(&s, '-') && has_text(&s, "DELETED_LINE_02")
})
.unwrap();
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
for _ in 0..4 {
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
}
harness.render().unwrap();
let pos_before = harness.cursor_position();
assert_eq!(
pos_before,
40,
"expected cursor at start of `before_05` (byte 40); saw byte \
{pos_before}. Screen:\n{}",
harness.screen_to_string()
);
let screen_before_down = harness.screen_to_string();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
let pos_after = harness.cursor_position();
assert_eq!(
pos_after, 50,
"Down from `before_05` should reach `after_01` (byte 50); saw \
byte {pos_after} — deletion virtual block is blocking cursor \
motion (cursor snaps to EOL of `before_05` instead). \
Screen at moment of Down:\n{screen_before_down}",
);
}
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_live_diff_removed_line_not_split_per_char_at_tiny_width() {
let repo = GitTestRepo::new();
repo.create_file(
"src/note.rs",
"pub fn greet() {\n println!(\"LIVEDIFF_OLD_DISTINCTIVE_TOKEN hello there friend\");\n}\n",
);
repo.git_add_all();
repo.git_commit("baseline");
repo.setup_live_diff_plugin();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
repo.modify_file(
"src/note.rs",
"pub fn greet() {\n panic!(\"COMPLETELY_DIFFERENT_REWRITE_PAYLOAD_DROPPING_SIMILARITY_FAR_BELOW_THE_THRESHOLD\");\n}\n",
);
let mut config = Config::default();
config.editor.line_wrap = true;
config.editor.wrap_column = Some(2);
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 40, config, repo.path.clone()).unwrap();
enable_live_diff_globally(&mut harness);
open_file(&mut harness, &repo.path, "src/note.rs");
harness
.wait_until(|h| has_glyph(&h.screen_to_string(), '-'))
.unwrap();
let screen = harness.screen_to_string();
assert!(
has_text(&screen, "LIVEDIFF_OLD_DISTINCTIVE_TOKEN"),
"removed line should render on a single row, not one char per \
row (#2177). Screen:\n{screen}"
);
}
fn find_text_cell(buf: &ratatui::buffer::Buffer, needle: &str) -> Option<(u16, u16)> {
for y in 0..buf.area.height {
let mut row = String::new();
for x in 0..buf.area.width {
row.push_str(buf[(x, y)].symbol());
}
if let Some(idx) = row.find(needle) {
return Some((y, idx as u16));
}
}
None
}
fn is_word_diff_emphasized(buf: &ratatui::buffer::Buffer, x: u16, y: u16) -> bool {
use ratatui::style::Modifier;
let m = buf[(x, y)].style().add_modifier;
m.contains(Modifier::BOLD) && m.contains(Modifier::UNDERLINED)
}
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_live_diff_word_highlight_on_low_similarity_removed_added_pair() {
let repo = GitTestRepo::new();
repo.setup_live_diff_plugin();
repo.create_file(
"note.txt",
"SHARED_HEAD_TOKEN OLD_REMOVED_WORD_PAYLOAD_ZZZZ SHARED_TAIL_TOKEN\n",
);
repo.git_add(&["note.txt"]);
repo.git_commit("baseline");
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
repo.modify_file(
"note.txt",
"SHARED_HEAD_TOKEN NEW_ADDED_REPLACEMENT_QQQQ SHARED_TAIL_TOKEN\n",
);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
enable_live_diff_globally(&mut harness);
open_file(&mut harness, &repo.path, "note.txt");
harness
.wait_until(|h| {
let s = h.screen_to_string();
has_glyph(&s, '-')
&& has_text(&s, "OLD_REMOVED_WORD_PAYLOAD_ZZZZ")
&& has_text(&s, "NEW_ADDED_REPLACEMENT_QQQQ")
})
.unwrap();
let buf = harness.buffer();
let (old_y, old_x) = find_text_cell(buf, "OLD_REMOVED_WORD_PAYLOAD_ZZZZ")
.expect("deletion virtual line not found on screen");
assert!(
is_word_diff_emphasized(buf, old_x, old_y),
"removed word on the deletion virtual line (row {old_y}, col {old_x}) \
should be bold + underlined",
);
let (head_y, head_x) =
find_text_cell(buf, "SHARED_HEAD_TOKEN").expect("shared head token not found on screen");
assert_eq!(
head_y, old_y,
"expected the first SHARED_HEAD_TOKEN occurrence on the deletion \
virtual line (it renders above the added line)",
);
assert!(
!is_word_diff_emphasized(buf, head_x, head_y),
"shared token on the deletion virtual line (row {head_y}, col \
{head_x}) should NOT be bold + underlined",
);
let (new_y, new_x) =
find_text_cell(buf, "NEW_ADDED_REPLACEMENT_QQQQ").expect("added line not found on screen");
assert!(
is_word_diff_emphasized(buf, new_x, new_y),
"added word on the new line (row {new_y}, col {new_x}) should be \
bold + underlined",
);
assert!(
!is_word_diff_emphasized(buf, head_x, new_y),
"shared token on the added line (row {new_y}, col {head_x}) should \
NOT be bold + underlined",
);
}