use crate::common::git_test_helper::{DirGuard, GitTestRepo};
use crate::common::harness::EditorTestHarness;
use crossterm::event::{KeyCode, KeyModifiers};
use fresh::config::Config;
fn get_content_lines(screen: &str) -> Vec<&str> {
let lines: Vec<&str> = screen.lines().collect();
let content_start = 2;
let content_end = lines.len().saturating_sub(2);
if content_end > content_start {
lines[content_start..content_end].to_vec()
} else {
vec![]
}
}
fn has_gutter_indicator(screen: &str, symbol: &str) -> bool {
for line in get_content_lines(screen) {
if let Some(first_char) = line.chars().next() {
if first_char.to_string() == symbol {
return true;
}
}
}
false
}
fn count_gutter_indicators(screen: &str, symbol: &str) -> usize {
let mut count = 0;
for line in get_content_lines(screen) {
if let Some(first_char) = line.chars().next() {
if first_char.to_string() == symbol {
count += 1;
}
}
}
count
}
fn get_indicator_lines(screen: &str, symbol: &str) -> Vec<usize> {
let mut lines_with_indicator = Vec::new();
for (idx, line) in get_content_lines(screen).iter().enumerate() {
if let Some(first_char) = line.chars().next() {
if first_char.to_string() == symbol {
lines_with_indicator.push(idx);
}
}
}
lines_with_indicator
}
fn wait_for_indicator(harness: &mut EditorTestHarness, symbol: &str) {
let symbol = symbol.to_string();
harness
.wait_until(|h| has_gutter_indicator(&h.screen_to_string(), &symbol))
.unwrap();
}
fn wait_for_no_indicators(harness: &mut EditorTestHarness, symbol: &str) {
let symbol = symbol.to_string();
harness
.wait_until(|h| !has_gutter_indicator(&h.screen_to_string(), &symbol))
.unwrap();
}
fn wait_for_indicator_on_line(harness: &mut EditorTestHarness, symbol: &str, line: usize) {
let symbol = symbol.to_string();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
get_indicator_lines(&screen, &symbol).contains(&line)
})
.unwrap();
}
fn process_async_once(harness: &mut EditorTestHarness) {
let _ = harness.process_async_and_render();
}
fn trigger_git_gutter_refresh(harness: &mut EditorTestHarness) {
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Git Gutter").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
}
fn open_file(harness: &mut EditorTestHarness, repo_path: &std::path::Path, relative_path: &str) {
let full_path = repo_path.join(relative_path);
harness.open_file(&full_path).unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains(relative_path)
})
.unwrap();
}
fn save_file(harness: &mut EditorTestHarness) {
harness
.send_key(KeyCode::Char('s'), KeyModifiers::CONTROL)
.unwrap();
process_async_once(harness);
}
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_git_gutter_shows_on_file_open() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_git_gutter_plugin();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
repo.modify_file(
"src/main.rs",
r#"fn main() {
println!("Modified line!");
let config = load_config();
start_server(config);
}
fn load_config() -> Config {
Config::default()
}
fn start_server(config: Config) {
println!("Starting server...");
}
"#,
);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
open_file(&mut harness, &repo.path, "src/main.rs");
harness
.wait_until(|h| {
let screen = h.screen_to_string();
has_gutter_indicator(&screen, "│")
})
.unwrap();
let screen = harness.screen_to_string();
println!("Git gutter screen:\n{}", screen);
}
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_git_gutter_updates_after_save() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_git_gutter_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();
open_file(&mut harness, &repo.path, "src/main.rs");
harness.render().unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("main.rs") && screen.contains("fn main")
})
.unwrap();
for _ in 0..5 {
harness.process_async_and_render().unwrap();
harness.sleep(std::time::Duration::from_millis(50));
}
let screen = harness.screen_to_string();
let initial_indicators = count_gutter_indicators(&screen, "│");
harness.type_text("// New comment\n").unwrap();
harness.render().unwrap();
save_file(&mut harness);
harness
.wait_until(|h| {
let screen = h.screen_to_string();
count_gutter_indicators(&screen, "│") > initial_indicators
})
.unwrap();
let screen = harness.screen_to_string();
println!("After save screen:\n{}", screen);
}
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_git_gutter_added_lines() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_git_gutter_plugin();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
repo.modify_file(
"src/main.rs",
r#"fn main() {
println!("Hello, world!");
let config = load_config();
start_server(config);
}
// New function added
fn new_function() {
println!("This is new!");
}
fn load_config() -> Config {
Config::default()
}
fn start_server(config: Config) {
println!("Starting server...");
}
"#,
);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
open_file(&mut harness, &repo.path, "src/main.rs");
harness
.wait_until(|h| {
let screen = h.screen_to_string();
count_gutter_indicators(&screen, "│") >= 3
})
.unwrap();
let screen = harness.screen_to_string();
println!("Added lines screen:\n{}", screen);
}
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_git_gutter_deleted_lines() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_git_gutter_plugin();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
repo.modify_file(
"src/main.rs",
r#"fn main() {
start_server(Config::default());
}
fn start_server(config: Config) {
println!("Starting server...");
}
"#,
);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
open_file(&mut harness, &repo.path, "src/main.rs");
harness
.wait_until(|h| {
let screen = h.screen_to_string();
has_gutter_indicator(&screen, "▾") || has_gutter_indicator(&screen, "│")
})
.unwrap();
let screen = harness.screen_to_string();
println!("Deleted lines screen:\n{}", screen);
}
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_git_gutter_staged_changes() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_git_gutter_plugin();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
repo.modify_file(
"src/main.rs",
r#"fn main() {
println!("Staged change!");
let config = load_config();
start_server(config);
}
fn load_config() -> Config {
Config::default()
}
fn start_server(config: Config) {
println!("Starting server...");
}
"#,
);
repo.stage_file("src/main.rs");
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
open_file(&mut harness, &repo.path, "src/main.rs");
harness
.wait_until(|h| {
let screen = h.screen_to_string();
has_gutter_indicator(&screen, "│")
})
.unwrap();
let screen = harness.screen_to_string();
println!("Staged changes screen:\n{}", screen);
}
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_git_gutter_clears_after_commit() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_git_gutter_plugin();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
repo.modify_file(
"src/main.rs",
r#"fn main() {
println!("Committed change!");
let config = load_config();
start_server(config);
}
fn load_config() -> Config {
Config::default()
}
fn start_server(config: Config) {
println!("Starting server...");
}
"#,
);
repo.git_add_all();
repo.git_commit("Update main.rs");
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
open_file(&mut harness, &repo.path, "src/main.rs");
harness.sleep(std::time::Duration::from_millis(500));
harness.render().unwrap();
let screen = harness.screen_to_string();
println!("After commit screen:\n{}", screen);
let indicators = count_gutter_indicators(&screen, "│");
assert_eq!(
indicators, 0,
"Git gutter should have no indicators after changes are committed"
);
}
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_git_gutter_untracked_file() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_git_gutter_plugin();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
repo.create_file("src/new_file.rs", "fn new_function() {}\n");
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
open_file(&mut harness, &repo.path, "src/new_file.rs");
harness.sleep(std::time::Duration::from_millis(500));
harness.render().unwrap();
let screen = harness.screen_to_string();
println!("Untracked file screen:\n{}", screen);
let indicators = count_gutter_indicators(&screen, "│");
assert_eq!(
indicators, 0,
"Git gutter should have no indicators for untracked files"
);
}
#[test]
#[cfg_attr(windows, ignore)] fn test_buffer_modified_shows_on_edit() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_buffer_modified_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();
open_file(&mut harness, &repo.path, "src/main.rs");
let screen = harness.screen_to_string();
let initial_indicators = count_gutter_indicators(&screen, "│");
harness.type_text("// Unsaved change\n").unwrap();
harness.render().unwrap();
harness.sleep(std::time::Duration::from_millis(100));
harness.render().unwrap();
let screen = harness.screen_to_string();
println!("After edit screen:\n{}", screen);
let new_indicators = count_gutter_indicators(&screen, "│");
assert!(
new_indicators > initial_indicators,
"Buffer modified should show indicator for unsaved changes"
);
}
#[test]
#[cfg_attr(windows, ignore)] fn test_buffer_modified_clears_after_save() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_buffer_modified_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();
open_file(&mut harness, &repo.path, "src/main.rs");
harness.type_text("// Unsaved change\n").unwrap();
harness.render().unwrap();
harness.sleep(std::time::Duration::from_millis(100));
harness.render().unwrap();
let screen_before = harness.screen_to_string();
let indicators_before = count_gutter_indicators(&screen_before, "│");
save_file(&mut harness);
harness.sleep(std::time::Duration::from_millis(200));
harness.render().unwrap();
let screen_after = harness.screen_to_string();
println!("After save screen:\n{}", screen_after);
let indicators_after = count_gutter_indicators(&screen_after, "│");
assert!(
indicators_after < indicators_before || indicators_after == 0,
"Buffer modified indicators should clear after save"
);
}
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_both_plugins_coexist() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_gutter_plugins();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
repo.modify_file(
"src/main.rs",
r#"fn main() {
println!("Git change on disk!");
let config = load_config();
start_server(config);
}
fn load_config() -> Config {
Config::default()
}
fn start_server(config: Config) {
println!("Starting server...");
}
"#,
);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
open_file(&mut harness, &repo.path, "src/main.rs");
harness
.wait_until(|h| {
let screen = h.screen_to_string();
has_gutter_indicator(&screen, "│")
})
.unwrap();
harness
.send_key(KeyCode::End, KeyModifiers::CONTROL)
.unwrap();
harness.type_text("\n// Unsaved edit").unwrap();
harness.render().unwrap();
harness.sleep(std::time::Duration::from_millis(100));
harness.render().unwrap();
let screen = harness.screen_to_string();
println!("Both plugins screen:\n{}", screen);
let total_indicators = count_gutter_indicators(&screen, "│");
assert!(
total_indicators >= 1,
"Should have indicators from both git changes and unsaved changes"
);
}
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_git_gutter_priority_over_buffer_modified() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_gutter_plugins();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
repo.modify_file(
"src/main.rs",
r#"fn main() {
println!("Modified for git!");
let config = load_config();
start_server(config);
}
fn load_config() -> Config {
Config::default()
}
fn start_server(config: Config) {
println!("Starting server...");
}
"#,
);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
open_file(&mut harness, &repo.path, "src/main.rs");
harness
.wait_until(|h| {
let screen = h.screen_to_string();
has_gutter_indicator(&screen, "│")
})
.unwrap();
let screen = harness.screen_to_string();
println!("Priority test screen:\n{}", screen);
assert!(
has_gutter_indicator(&screen, "│"),
"Higher priority indicator should be visible"
);
}
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_gutter_indicators_comprehensive() {
use std::fs;
let repo = GitTestRepo::new();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let initial_content = r#"line 1: unchanged
line 2: unchanged
line 3: will be modified
line 4: unchanged
line 5: unchanged
"#;
repo.create_file("test.txt", initial_content);
repo.git_add_all();
repo.git_commit("Initial commit");
repo.setup_gutter_plugins();
let modified_content = r#"line 1: unchanged
line 2: unchanged
line 3: MODIFIED!
line 4: unchanged
line 5: unchanged
"#;
fs::write(repo.path.join("test.txt"), modified_content).unwrap();
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
open_file(&mut harness, &repo.path, "test.txt");
trigger_git_gutter_refresh(&mut harness);
wait_for_indicator_on_line(&mut harness, "│", 2);
let screen = harness.screen_to_string();
println!("=== After opening modified file ===\n{}", screen);
let indicator_lines = get_indicator_lines(&screen, "│");
println!("Indicator lines after open: {:?}", indicator_lines);
harness
.send_key(KeyCode::Char('g'), KeyModifiers::CONTROL)
.unwrap(); harness.render().unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness.type_text("NEW LINE INSERTED\n").unwrap();
harness.render().unwrap();
wait_for_indicator_on_line(&mut harness, "│", 2);
let screen_after_insert = harness.screen_to_string();
println!("=== After inserting new line ===\n{}", screen_after_insert);
let indicator_lines_after = get_indicator_lines(&screen_after_insert, "│");
println!("Indicator lines after insert: {:?}", indicator_lines_after);
let indicator_count = count_gutter_indicators(&screen_after_insert, "│");
println!("Total indicators after insert: {}", indicator_count);
save_file(&mut harness);
trigger_git_gutter_refresh(&mut harness);
wait_for_indicator(&mut harness, "│");
let screen_after_save = harness.screen_to_string();
println!("=== After save ===\n{}", screen_after_save);
let indicator_lines_after_save = get_indicator_lines(&screen_after_save, "│");
println!(
"Indicator lines after save: {:?}",
indicator_lines_after_save
);
println!("\n=== Test Summary ===");
println!(
"Initial indicator count: {}",
get_indicator_lines(&screen, "│").len()
);
println!("After insert indicator count: {}", indicator_count);
println!(
"After save indicator count: {}",
indicator_lines_after_save.len()
);
assert!(
indicator_count >= 1 || !indicator_lines_after_save.is_empty(),
"Should have at least one indicator after making changes. \
After insert: {}, After save: {}",
indicator_count,
indicator_lines_after_save.len()
);
}
#[test]
fn test_unsaved_changes_get_indicators() {
let repo = GitTestRepo::new();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let initial_content = "line 1\nline 2\nline 3\n";
repo.create_file("test.txt", initial_content);
repo.git_add_all();
repo.git_commit("Initial commit");
repo.setup_buffer_modified_plugin();
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
open_file(&mut harness, &repo.path, "test.txt");
let screen_before = harness.screen_to_string();
let indicators_before = count_gutter_indicators(&screen_before, "│");
println!("=== Before edit ===\n{}", screen_before);
println!("Indicators before edit: {}", indicators_before);
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap(); harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap(); harness.type_text(" MODIFIED").unwrap();
harness.render().unwrap();
wait_for_indicator_on_line(&mut harness, "│", 1);
let screen_after = harness.screen_to_string();
let indicators_after = count_gutter_indicators(&screen_after, "│");
println!("=== After edit ===\n{}", screen_after);
println!("Indicators after edit: {}", indicators_after);
assert!(
indicators_after > indicators_before,
"Should have more indicators after editing. Before: {}, After: {}",
indicators_before,
indicators_after
);
save_file(&mut harness);
wait_for_no_indicators(&mut harness, "│");
let screen_after_save = harness.screen_to_string();
let indicators_after_save = count_gutter_indicators(&screen_after_save, "│");
println!("=== After save ===\n{}", screen_after_save);
println!("Indicators after save: {}", indicators_after_save);
assert!(
indicators_after_save <= indicators_after,
"Indicators should not increase after save. After edit: {}, After save: {}",
indicators_after,
indicators_after_save
);
}
#[test]
fn test_buffer_modified_clears_after_undo_on_same_line() {
let repo = GitTestRepo::new();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let initial_content = (1..=15)
.map(|i| format!("line {:02}\n", i))
.collect::<String>();
repo.create_file("test.txt", &initial_content);
repo.git_add_all();
repo.git_commit("Initial commit");
repo.setup_buffer_modified_plugin();
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
open_file(&mut harness, &repo.path, "test.txt");
process_async_once(&mut harness);
harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness.type_text(" MOD").unwrap();
harness.render().unwrap();
wait_for_indicator_on_line(&mut harness, "│", 0);
let screen_after = harness.screen_to_string();
let indicators_after = get_indicator_lines(&screen_after, "│");
println!("=== After edit ===\n{}", screen_after);
assert_eq!(
indicators_after,
vec![0],
"Indicator should appear on edited line (line 0), got {:?}",
indicators_after
);
for _ in 0..4 {
harness
.send_key(KeyCode::Char('z'), KeyModifiers::CONTROL)
.unwrap();
}
harness.render().unwrap();
wait_for_no_indicators(&mut harness, "│");
let screen_after_undo = harness.screen_to_string();
let indicators_after_undo = get_indicator_lines(&screen_after_undo, "│");
println!("=== After undo ===\n{}", screen_after_undo);
assert!(
indicators_after_undo.is_empty(),
"Indicators should clear after undo to saved state, got {:?}",
indicators_after_undo
);
}
#[test]
fn test_buffer_modified_single_line_in_multi_line_file() {
let repo = GitTestRepo::new();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let initial_content = (1..=15)
.map(|i| format!("line {:02}\n", i))
.collect::<String>();
repo.create_file("test.txt", &initial_content);
repo.git_add_all();
repo.git_commit("Initial commit");
repo.setup_buffer_modified_plugin();
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
open_file(&mut harness, &repo.path, "test.txt");
process_async_once(&mut harness);
for _ in 0..9 {
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
}
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness.type_text(" MOD").unwrap();
harness.render().unwrap();
wait_for_indicator_on_line(&mut harness, "│", 9);
let screen_after = harness.screen_to_string();
let indicators_after = get_indicator_lines(&screen_after, "│");
println!("=== After edit (multi-line) ===\n{}", screen_after);
assert_eq!(
indicators_after,
vec![9],
"Only the edited line should have indicator, got {:?}",
indicators_after
);
for _ in 0..4 {
harness
.send_key(KeyCode::Char('z'), KeyModifiers::CONTROL)
.unwrap();
}
harness.render().unwrap();
wait_for_no_indicators(&mut harness, "│");
let screen_after_undo = harness.screen_to_string();
let indicators_after_undo = get_indicator_lines(&screen_after_undo, "│");
println!("=== After undo (multi-line) ===\n{}", screen_after_undo);
assert!(
indicators_after_undo.is_empty(),
"Indicators should clear after undo, got {:?}",
indicators_after_undo
);
}
#[test]
fn test_buffer_modified_newline_insert_only_marks_affected_lines() {
let repo = GitTestRepo::new();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let initial_content = "line 1\nline 2\nline 3\nline 4\nline 5\n";
repo.create_file("test.txt", initial_content);
repo.git_add_all();
repo.git_commit("Initial commit");
repo.setup_buffer_modified_plugin();
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
open_file(&mut harness, &repo.path, "test.txt");
process_async_once(&mut harness);
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap(); harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
wait_for_indicator(&mut harness, "│");
let screen = harness.screen_to_string();
let indicators = get_indicator_lines(&screen, "│");
println!("=== After inserting newline ===\n{}", screen);
println!("Indicator lines: {:?}", indicators);
assert!(
indicators.len() <= 2,
"Only the modified lines should have indicators, not the entire rest of the buffer. Got {:?}",
indicators
);
let has_line_3_plus = indicators.iter().any(|&line| line >= 3);
assert!(
!has_line_3_plus,
"Lines 3+ should not have indicators (they just shifted, content unchanged). Got {:?}",
indicators
);
}
#[test]
fn test_buffer_modified_clears_after_manual_delete_restores_content() {
fresh::services::signal_handler::install_signal_handlers();
let repo = GitTestRepo::new();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let initial_content = "line 01\nline 02\nline 03\nline 04\nline 05\n";
repo.create_file("test.txt", initial_content);
repo.git_add_all();
repo.git_commit("Initial commit");
repo.setup_buffer_modified_plugin();
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
open_file(&mut harness, &repo.path, "test.txt");
process_async_once(&mut harness);
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(" ADDED").unwrap();
harness.render().unwrap();
wait_for_indicator_on_line(&mut harness, "│", 2);
let screen_after_add = harness.screen_to_string();
let indicators_after_add = get_indicator_lines(&screen_after_add, "│");
println!("=== After adding text ===\n{}", screen_after_add);
assert!(
indicators_after_add.contains(&2),
"Line 2 (0-indexed) should have indicator after adding text, got {:?}",
indicators_after_add
);
for _ in 0..6 {
harness
.send_key(KeyCode::Backspace, KeyModifiers::NONE)
.unwrap();
}
harness.render().unwrap();
wait_for_no_indicators(&mut harness, "│");
let screen_after_delete = harness.screen_to_string();
let indicators_after_delete = get_indicator_lines(&screen_after_delete, "│");
println!(
"=== After manually deleting text ===\n{}",
screen_after_delete
);
assert!(
indicators_after_delete.is_empty(),
"Indicators should clear when content is manually restored to saved state, got {:?}",
indicators_after_delete
);
}
#[test]
#[ignore = "flaky test - times out intermittently"]
fn test_buffer_modified_clears_after_paste_restores_content() {
let repo = GitTestRepo::new();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let initial_content = "hello world\n";
repo.create_file("test.txt", initial_content);
repo.git_add_all();
repo.git_commit("Initial commit");
repo.setup_buffer_modified_plugin();
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
open_file(&mut harness, &repo.path, "test.txt");
process_async_once(&mut harness);
harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
for _ in 0..6 {
harness
.send_key(KeyCode::Right, KeyModifiers::NONE)
.unwrap();
}
for _ in 0..5 {
harness
.send_key(KeyCode::Right, KeyModifiers::SHIFT)
.unwrap();
}
harness
.send_key(KeyCode::Char('x'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
wait_for_indicator(&mut harness, "│");
let screen_after_cut = harness.screen_to_string();
let indicators_after_cut = get_indicator_lines(&screen_after_cut, "│");
println!("=== After cutting 'world' ===\n{}", screen_after_cut);
assert!(
!indicators_after_cut.is_empty(),
"Should have indicator after cutting text"
);
harness
.send_key(KeyCode::Char('v'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
wait_for_no_indicators(&mut harness, "│");
let screen_after_paste = harness.screen_to_string();
let indicators_after_paste = get_indicator_lines(&screen_after_paste, "│");
println!("=== After pasting 'world' back ===\n{}", screen_after_paste);
assert!(
indicators_after_paste.is_empty(),
"Indicators should clear when content is restored via paste, got {:?}",
indicators_after_paste
);
}
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_indicator_line_shifting() {
use std::fs;
let repo = GitTestRepo::new();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let initial_content = "line 1\nline 2\nline 3\nline 4\nline 5\n";
repo.create_file("test.txt", initial_content);
repo.git_add_all();
repo.git_commit("Initial commit");
let modified_content = "line 1\nline 2\nline 3 CHANGED\nline 4\nline 5\n";
fs::write(repo.path.join("test.txt"), modified_content).unwrap();
repo.setup_git_gutter_plugin();
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
open_file(&mut harness, &repo.path, "test.txt");
trigger_git_gutter_refresh(&mut harness);
wait_for_indicator(&mut harness, "│");
let screen_initial = harness.screen_to_string();
let lines_initial = get_indicator_lines(&screen_initial, "│");
println!("=== Initial state ===\n{}", screen_initial);
println!("Initial indicator lines: {:?}", lines_initial);
let content_lines = get_content_lines(&screen_initial);
println!("Content lines count: {}", content_lines.len());
harness
.send_key(KeyCode::Char('g'), KeyModifiers::CONTROL)
.unwrap(); harness.render().unwrap();
harness
.type_text("inserted line A\ninserted line B\n")
.unwrap();
harness.render().unwrap();
save_file(&mut harness);
trigger_git_gutter_refresh(&mut harness);
wait_for_indicator(&mut harness, "│");
let screen_after = harness.screen_to_string();
let lines_after = get_indicator_lines(&screen_after, "│");
println!(
"=== After inserting 2 lines at beginning ===\n{}",
screen_after
);
println!("Indicator lines after: {:?}", lines_after);
assert!(
!lines_after.is_empty() || lines_initial.is_empty(),
"After inserting lines and saving, git diff should show changes"
);
println!("\n=== Shift Test Summary ===");
println!("Initial indicators: {:?}", lines_initial);
println!("After shift indicators: {:?}", lines_after);
}