use crate::common::harness::EditorTestHarness;
use fresh::view::virtual_text::{VirtualTextNamespace, VirtualTextPosition};
use ratatui::style::{Color, Style};
use tempfile::TempDir;
fn virtual_line_style() -> Style {
Style::default().fg(Color::DarkGray)
}
#[test]
fn test_virtual_line_above() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.txt");
std::fs::write(&file_path, "Line 1\nLine 2\nLine 3").unwrap();
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
{
let state = harness.editor_mut().active_state_mut();
state.virtual_texts.add_line(
&mut state.marker_list,
7, "--- Header Above Line 2 ---".to_string(),
virtual_line_style(),
VirtualTextPosition::LineAbove,
VirtualTextNamespace::from_string("test".to_string()),
0,
);
}
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(
screen.contains("--- Header Above Line 2 ---"),
"Virtual line should be visible. Screen:\n{screen}"
);
assert!(screen.contains("Line 1"), "Line 1 should be visible");
assert!(screen.contains("Line 2"), "Line 2 should be visible");
assert!(screen.contains("Line 3"), "Line 3 should be visible");
}
#[test]
fn test_virtual_line_below() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.txt");
std::fs::write(&file_path, "Line 1\nLine 2\nLine 3").unwrap();
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
{
let state = harness.editor_mut().active_state_mut();
state.virtual_texts.add_line(
&mut state.marker_list,
0, "--- Footer Below Line 1 ---".to_string(),
virtual_line_style(),
VirtualTextPosition::LineBelow,
VirtualTextNamespace::from_string("test".to_string()),
0,
);
}
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(
screen.contains("--- Footer Below Line 1 ---"),
"Virtual line should be visible. Screen:\n{screen}"
);
}
#[test]
fn test_multiple_virtual_lines_same_position() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.txt");
std::fs::write(&file_path, "Line 1\nLine 2").unwrap();
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
{
let state = harness.editor_mut().active_state_mut();
state.virtual_texts.add_line(
&mut state.marker_list,
0,
"First Header".to_string(),
virtual_line_style(),
VirtualTextPosition::LineAbove,
VirtualTextNamespace::from_string("test".to_string()),
0, );
state.virtual_texts.add_line(
&mut state.marker_list,
0,
"Second Header".to_string(),
virtual_line_style(),
VirtualTextPosition::LineAbove,
VirtualTextNamespace::from_string("test".to_string()),
10, );
}
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(
screen.contains("First Header"),
"First header should be visible"
);
assert!(
screen.contains("Second Header"),
"Second header should be visible"
);
}
#[test]
fn test_clear_namespace() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.txt");
std::fs::write(&file_path, "Line 1\nLine 2").unwrap();
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
{
let state = harness.editor_mut().active_state_mut();
state.virtual_texts.add_line(
&mut state.marker_list,
0,
"Git Blame Header".to_string(),
virtual_line_style(),
VirtualTextPosition::LineAbove,
VirtualTextNamespace::from_string("git-blame".to_string()),
0,
);
state.virtual_texts.add_line(
&mut state.marker_list,
0,
"LSP Diagnostic".to_string(),
virtual_line_style(),
VirtualTextPosition::LineAbove,
VirtualTextNamespace::from_string("lsp".to_string()),
0,
);
}
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(screen.contains("Git Blame Header"));
assert!(screen.contains("LSP Diagnostic"));
{
let state = harness.editor_mut().active_state_mut();
state.virtual_texts.clear_namespace(
&mut state.marker_list,
&VirtualTextNamespace::from_string("git-blame".to_string()),
);
}
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(
!screen.contains("Git Blame Header"),
"Git blame header should be cleared"
);
assert!(
screen.contains("LSP Diagnostic"),
"LSP diagnostic should remain"
);
}
#[test]
fn test_virtual_lines_no_line_numbers() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.txt");
std::fs::write(&file_path, "Line 1\nLine 2\nLine 3").unwrap();
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
{
let state = harness.editor_mut().active_state_mut();
state.virtual_texts.add_line(
&mut state.marker_list,
7, "VIRTUAL".to_string(),
virtual_line_style(),
VirtualTextPosition::LineAbove,
VirtualTextNamespace::from_string("test".to_string()),
0,
);
}
harness.render().unwrap();
let screen = harness.screen_to_string();
let lines: Vec<&str> = screen.lines().collect();
for line in &lines {
if line.contains("VIRTUAL") {
assert!(
!line.trim_start().starts_with(|c: char| c.is_ascii_digit()),
"Virtual line should not start with a line number: {line}"
);
}
}
}
#[test]
fn test_virtual_line_position_tracking() {
use crossterm::event::{KeyCode, KeyModifiers};
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.txt");
std::fs::write(&file_path, "AAA\nBBB\nCCC").unwrap();
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
{
let state = harness.editor_mut().active_state_mut();
state.virtual_texts.add_line(
&mut state.marker_list,
4, "--- Above BBB ---".to_string(),
virtual_line_style(),
VirtualTextPosition::LineAbove,
VirtualTextNamespace::from_string("test".to_string()),
0,
);
}
harness.render().unwrap();
let screen_before = harness.screen_to_string();
assert!(
screen_before.contains("--- Above BBB ---"),
"Virtual line should be visible before edit"
);
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.type_text("NEW LINE\n").unwrap();
harness.render().unwrap();
let screen_after = harness.screen_to_string();
assert!(
screen_after.contains("--- Above BBB ---"),
"Virtual line should still be visible after edit. Screen:\n{screen_after}"
);
assert!(
screen_after.contains("NEW LINE"),
"New line should be visible"
);
assert!(screen_after.contains("BBB"), "BBB should still be visible");
}
#[test]
fn test_virtual_lines_above_and_below_same_line() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.txt");
std::fs::write(&file_path, "Source Line").unwrap();
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
{
let state = harness.editor_mut().active_state_mut();
state.virtual_texts.add_line(
&mut state.marker_list,
0,
"=== ABOVE ===".to_string(),
virtual_line_style(),
VirtualTextPosition::LineAbove,
VirtualTextNamespace::from_string("test".to_string()),
0,
);
state.virtual_texts.add_line(
&mut state.marker_list,
0,
"=== BELOW ===".to_string(),
virtual_line_style(),
VirtualTextPosition::LineBelow,
VirtualTextNamespace::from_string("test".to_string()),
0,
);
}
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(screen.contains("=== ABOVE ==="), "Above line missing");
assert!(screen.contains("Source Line"), "Source line missing");
assert!(screen.contains("=== BELOW ==="), "Below line missing");
let above_pos = screen.find("=== ABOVE ===").unwrap();
let source_pos = screen.find("Source Line").unwrap();
let below_pos = screen.find("=== BELOW ===").unwrap();
assert!(
above_pos < source_pos,
"ABOVE should appear before Source Line"
);
assert!(
source_pos < below_pos,
"Source Line should appear before BELOW"
);
}
#[test]
fn test_virtual_text_count() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.txt");
std::fs::write(&file_path, "Content").unwrap();
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.open_file(&file_path).unwrap();
{
let state = harness.editor().active_state();
assert_eq!(state.virtual_texts.len(), 0);
assert!(state.virtual_texts.is_empty());
}
{
let state = harness.editor_mut().active_state_mut();
state.virtual_texts.add_line(
&mut state.marker_list,
0,
"Line 1".to_string(),
virtual_line_style(),
VirtualTextPosition::LineAbove,
VirtualTextNamespace::from_string("ns1".to_string()),
0,
);
state.virtual_texts.add_line(
&mut state.marker_list,
0,
"Line 2".to_string(),
virtual_line_style(),
VirtualTextPosition::LineAbove,
VirtualTextNamespace::from_string("ns1".to_string()),
0,
);
}
{
let state = harness.editor().active_state();
assert_eq!(state.virtual_texts.len(), 2);
assert!(!state.virtual_texts.is_empty());
}
{
let state = harness.editor_mut().active_state_mut();
state.virtual_texts.clear_namespace(
&mut state.marker_list,
&VirtualTextNamespace::from_string("ns1".to_string()),
);
}
{
let state = harness.editor().active_state();
assert_eq!(state.virtual_texts.len(), 0);
assert!(state.virtual_texts.is_empty());
}
}