use crate::common::harness::{copy_plugin, copy_plugin_lib, EditorTestHarness};
use crate::common::tracing::init_tracing_from_env;
use crossterm::event::{KeyCode, KeyModifiers};
fn build_test_markdown() -> String {
let mut md = String::from("# Split View Test\n\n");
md.push_str("## Introduction\n\n");
md.push_str("This is a **bold text** and *italic text* with a [link](https://example.com) in the introduction paragraph.\n\n");
for i in 0..30 {
md.push_str(&format!(
"Paragraph {}: Here is **bold** and *italic* text with a [link](https://example.com/p{}) to test compose mode rendering.\n\n",
i, i
));
}
md.push_str("## Conclusion\n\n");
md.push_str("Final paragraph with **bold** and *italic* text.\n");
md
}
fn setup_split_compose_harness(width: u16, height: u16) -> (EditorTestHarness, tempfile::TempDir) {
let temp_dir = tempfile::TempDir::new().unwrap();
let project_root = temp_dir.path().join("project");
std::fs::create_dir(&project_root).unwrap();
let plugins_dir = project_root.join("plugins");
std::fs::create_dir(&plugins_dir).unwrap();
copy_plugin(&plugins_dir, "markdown_compose");
copy_plugin_lib(&plugins_dir);
let md_path = project_root.join("test.md");
std::fs::write(&md_path, build_test_markdown()).unwrap();
let mut harness = EditorTestHarness::with_config_and_working_dir(
width,
height,
Default::default(),
project_root,
)
.unwrap();
harness.open_file(&md_path).unwrap();
harness.render().unwrap();
harness.assert_screen_contains("test.md");
(harness, temp_dir)
}
fn create_vertical_split(harness: &mut EditorTestHarness) {
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.wait_for_prompt().unwrap();
harness.type_text("split vert").unwrap();
harness.wait_for_screen_contains("Split pane vert").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness.render().unwrap();
}
fn enable_compose_mode(harness: &mut EditorTestHarness) {
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.wait_for_prompt().unwrap();
harness.type_text("Toggle Compose").unwrap();
harness.wait_for_screen_contains("Toggle Compose").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.wait_for_prompt_closed().unwrap();
harness
.wait_until_stable(|h| {
let s = h.screen_to_string();
let sep_col = s
.lines()
.nth(2)
.and_then(|l| l.char_indices().find(|(_, c)| *c == '│').map(|(i, _)| i));
let Some(sep) = sep_col else {
return false;
};
let right_bold_lines = s
.lines()
.skip(2)
.filter(|l| {
if l.len() > sep + 1 {
l[sep + 1..].contains("**")
} else {
false
}
})
.count();
right_bold_lines <= 2 })
.unwrap();
}
fn enable_scroll_sync(harness: &mut EditorTestHarness) {
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.wait_for_prompt().unwrap();
harness.type_text("Toggle Scroll Sync").unwrap();
harness
.wait_for_screen_contains("Toggle Scroll Sync")
.unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.wait_for_prompt_closed().unwrap();
harness.render().unwrap();
}
fn switch_to_next_split(harness: &mut EditorTestHarness) {
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.wait_for_prompt().unwrap();
harness.type_text("next split").unwrap();
harness.wait_for_screen_contains("next split").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.wait_for_prompt_closed().unwrap();
harness.render().unwrap();
}
#[test]
#[ignore]
fn test_split_view_compose_only_in_one_panel() {
init_tracing_from_env();
let (mut harness, _temp) = setup_split_compose_harness(160, 40);
create_vertical_split(&mut harness);
enable_compose_mode(&mut harness);
let screen = harness.screen_to_string();
let first_content_line = screen.lines().nth(2).unwrap_or("");
let separator_col = first_content_line
.char_indices()
.find(|(_, c)| *c == '│')
.map(|(i, _)| i);
if let Some(sep) = separator_col {
let left_half: String = screen
.lines()
.skip(2)
.take(30)
.map(|l| if l.len() > sep { &l[..sep] } else { l })
.collect::<Vec<_>>()
.join("\n");
let right_half: String = screen
.lines()
.skip(2)
.take(30)
.map(|l| if l.len() > sep + 1 { &l[sep + 1..] } else { "" })
.collect::<Vec<_>>()
.join("\n");
let left_bold_count = left_half.matches("**").count();
let right_bold_count = right_half.matches("**").count();
assert!(
left_bold_count > right_bold_count,
"Source panel (left) should show more ** markers than compose panel (right).\n\
Left ** count: {}, Right ** count: {}\n\
Left panel:\n{}\n\nRight panel:\n{}",
left_bold_count,
right_bold_count,
left_half,
right_half,
);
}
}
#[test]
#[ignore]
fn test_split_view_line_numbers_per_split() {
init_tracing_from_env();
let (mut harness, _temp) = setup_split_compose_harness(160, 40);
create_vertical_split(&mut harness);
enable_compose_mode(&mut harness);
let screen = harness.screen_to_string();
let first_content_line = screen.lines().nth(2).unwrap_or("");
let separator_col = first_content_line
.char_indices()
.find(|(_, c)| *c == '│')
.map(|(i, _)| i);
if let Some(sep) = separator_col {
let left_half: String = screen
.lines()
.skip(2)
.take(20)
.map(|l| if l.len() > sep { &l[..sep] } else { l })
.collect::<Vec<_>>()
.join("\n");
let right_half: String = screen
.lines()
.skip(2)
.take(20)
.map(|l| if l.len() > sep + 1 { &l[sep + 1..] } else { "" })
.collect::<Vec<_>>()
.join("\n");
let left_has_line_numbers = left_half.lines().any(|l| {
let trimmed = l.trim_start();
trimmed.starts_with("1 ") || trimmed.starts_with("2 ") || trimmed.starts_with("3 ")
});
assert!(
left_has_line_numbers,
"Source panel (left) should show line numbers.\nLeft panel:\n{}",
left_half,
);
let right_has_line_numbers = right_half
.lines()
.filter(|l| !l.trim().is_empty())
.any(|l| {
let trimmed = l.trim_start();
trimmed.len() > 4
&& trimmed
.chars()
.take(4)
.all(|c| c.is_ascii_digit() || c == ' ')
&& trimmed.chars().nth(4) == Some('│')
});
assert!(
!right_has_line_numbers,
"Compose panel (right) should NOT show line numbers.\nRight panel:\n{}",
right_half,
);
}
}
#[test]
#[ignore]
fn test_split_view_scroll_sync() {
init_tracing_from_env();
let (mut harness, _temp) = setup_split_compose_harness(160, 40);
create_vertical_split(&mut harness);
enable_compose_mode(&mut harness);
enable_scroll_sync(&mut harness);
switch_to_next_split(&mut harness);
let before_screen = harness.screen_to_string();
for _ in 0..25 {
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
}
harness.render().unwrap();
harness.render().unwrap();
let after_screen = harness.screen_to_string();
assert_ne!(
before_screen, after_screen,
"Screen should change after scrolling down 25 lines"
);
let first_content_line = after_screen.lines().nth(2).unwrap_or("");
let separator_col = first_content_line
.char_indices()
.find(|(_, c)| *c == '│')
.map(|(i, _)| i);
if let Some(sep) = separator_col {
let right_after: String = after_screen
.lines()
.skip(2)
.take(20)
.map(|l| if l.len() > sep + 1 { &l[sep + 1..] } else { "" })
.collect::<Vec<_>>()
.join("\n");
let right_before: String = before_screen
.lines()
.skip(2)
.take(20)
.map(|l| if l.len() > sep + 1 { &l[sep + 1..] } else { "" })
.collect::<Vec<_>>()
.join("\n");
assert_ne!(
right_before, right_after,
"Right panel (compose) should also scroll when left panel (source) scrolls.\n\
Right panel before:\n{}\n\nRight panel after:\n{}",
right_before, right_after,
);
}
}