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 {
format!("LIVE_DIFF_REPLACED_LINE {}", 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();
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_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(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
enable_live_diff_globally(&mut harness);
open_file(&mut harness, &repo.path, "code.rs");
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(" + 1").unwrap();
harness.render().unwrap();
let virtual_row_present = |screen: &str, marker: &str| {
screen
.lines()
.any(|l| l.contains(marker) && !l.contains(" + 1"))
};
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(" + 1").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 + 1"))
.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 + 1"))
.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(" + 1"))
.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(" + 1"))
.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})",
);
}