use crate::common::fixtures::TestFixture;
use crate::common::harness::{copy_plugin, copy_plugin_lib, EditorTestHarness};
use crate::common::tracing::init_tracing_from_env;
use crossterm::event::{KeyCode, KeyModifiers};
use std::fs;
use std::time::Duration;
fn markdown_source_harness(width: u16, height: u16) -> (EditorTestHarness, tempfile::TempDir) {
init_tracing_from_env();
let temp_dir = tempfile::TempDir::new().unwrap();
let project_root = temp_dir.path().join("project_root");
fs::create_dir(&project_root).unwrap();
let plugins_dir = project_root.join("plugins");
fs::create_dir(&plugins_dir).unwrap();
copy_plugin(&plugins_dir, "markdown_source");
copy_plugin_lib(&plugins_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
width,
height,
Default::default(),
project_root,
)
.unwrap();
let loaded = harness
.wait_for_async(
|h| h.editor().mode_registry().has_mode("markdown-source"),
10_000,
)
.unwrap();
assert!(
loaded,
"markdown_source plugin did not load within 10 seconds"
);
(harness, temp_dir)
}
fn open_md_and_wait_for_mode(harness: &mut EditorTestHarness, path: &std::path::Path) {
harness.open_file(path).unwrap();
harness.render().unwrap();
let activated = harness
.wait_for_async(
|h| h.editor().editor_mode() == Some("markdown-source".to_string()),
5_000,
)
.unwrap();
assert!(
activated,
"markdown-source mode did not activate for {:?}. Current mode: {:?}",
path,
harness.editor().editor_mode(),
);
}
fn buf(harness: &EditorTestHarness) -> String {
harness
.get_buffer_content()
.expect("buffer content should be available")
}
#[test]
fn test_markdown_source_mode_auto_activates() {
let (mut harness, _temp_dir) = markdown_source_harness(80, 24);
let fixture = TestFixture::new("readme.md", "# Hello\n\nWorld\n").unwrap();
open_md_and_wait_for_mode(&mut harness, &fixture.path);
assert_eq!(
harness.editor().editor_mode(),
Some("markdown-source".to_string()),
"markdown-source mode should auto-activate for .md files"
);
harness.assert_no_plugin_errors();
}
#[test]
fn test_markdown_source_mode_not_active_for_non_md() {
let (mut harness, _temp_dir) = markdown_source_harness(80, 24);
let fixture = TestFixture::new("main.rs", "fn main() {}\n").unwrap();
harness.open_file(&fixture.path).unwrap();
harness.render().unwrap();
for _ in 0..5 {
harness.process_async_and_render().unwrap();
harness.sleep(Duration::from_millis(50));
}
assert_eq!(
harness.editor().editor_mode(),
None,
"markdown-source mode should NOT activate for non-markdown files"
);
harness.assert_no_plugin_errors();
}
#[test]
fn test_enter_continues_unordered_list() {
let (mut harness, _temp_dir) = markdown_source_harness(80, 24);
let content = "- top\n - nested\n";
let fixture = TestFixture::new("list.md", content).unwrap();
open_md_and_wait_for_mode(&mut harness, &fixture.path);
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
let ok = harness
.wait_for_async(
|h| {
h.get_buffer_content()
.map_or(false, |c| c.contains(" - nested\n - "))
},
5_000,
)
.unwrap();
let content = buf(&harness);
assert!(
ok && content.contains(" - nested\n - "),
"Expected newline with continued bullet ' - ' after ' - nested'. Got:\n{:?}",
content,
);
harness.assert_no_plugin_errors();
}
#[test]
fn test_enter_continues_ordered_list() {
let (mut harness, _temp_dir) = markdown_source_harness(80, 24);
let content = "1. first\n2. second\n";
let fixture = TestFixture::new("ordered.md", content).unwrap();
open_md_and_wait_for_mode(&mut harness, &fixture.path);
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
let ok = harness
.wait_for_async(
|h| {
h.get_buffer_content()
.map_or(false, |c| c.contains("2. second\n3. "))
},
5_000,
)
.unwrap();
let content = buf(&harness);
assert!(
ok && content.contains("2. second\n3. "),
"Expected '3. ' after '2. second'. Got:\n{:?}",
content,
);
harness.assert_no_plugin_errors();
}
#[test]
fn test_enter_continues_checkbox() {
let (mut harness, _temp_dir) = markdown_source_harness(80, 24);
let content = "- [x] done task\n";
let fixture = TestFixture::new("checkbox.md", content).unwrap();
open_md_and_wait_for_mode(&mut harness, &fixture.path);
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
let ok = harness
.wait_for_async(
|h| {
h.get_buffer_content()
.map_or(false, |c| c.contains("- [x] done task\n- [ ] "))
},
5_000,
)
.unwrap();
let content = buf(&harness);
assert!(
ok && content.contains("- [x] done task\n- [ ] "),
"Expected unchecked checkbox continuation. Got:\n{:?}",
content,
);
harness.assert_no_plugin_errors();
}
#[test]
fn test_enter_clears_empty_bullet() {
let (mut harness, _temp_dir) = markdown_source_harness(80, 24);
let content = "- item\n- \n";
let fixture = TestFixture::new("empty.md", content).unwrap();
open_md_and_wait_for_mode(&mut harness, &fixture.path);
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
let ok = harness
.wait_for_async(
|h| {
h.get_buffer_content()
.map_or(false, |c| c.contains("- item\n\n"))
},
5_000,
)
.unwrap();
let content = buf(&harness);
assert!(
ok && content.contains("- item\n\n"),
"Expected empty bullet to be cleared. Got:\n{:?}",
content,
);
harness.assert_no_plugin_errors();
}
#[test]
fn test_enter_no_indent_on_unindented_line() {
let (mut harness, _temp_dir) = markdown_source_harness(80, 24);
let content = "Hello world\n";
let fixture = TestFixture::new("plain.md", content).unwrap();
open_md_and_wait_for_mode(&mut harness, &fixture.path);
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
let ok = harness
.wait_for_async(
|h| {
h.get_buffer_content()
.map_or(false, |c| c.contains("Hello world\n\n"))
},
5_000,
)
.unwrap();
assert!(ok, "Enter should have been processed");
let content = buf(&harness);
let lines: Vec<&str> = content.lines().collect();
assert!(
lines.len() >= 2,
"Expected at least 2 lines after Enter. Got:\n{:?}",
content,
);
assert_eq!(
lines[1], "",
"Expected empty line (no indent) after unindented line. Got: {:?}",
lines[1],
);
harness.assert_no_plugin_errors();
}
#[test]
fn test_enter_deep_indent() {
let (mut harness, _temp_dir) = markdown_source_harness(80, 24);
let content = " deep indent text\n";
let fixture = TestFixture::new("deep.md", content).unwrap();
open_md_and_wait_for_mode(&mut harness, &fixture.path);
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
let ok = harness
.wait_for_async(
|h| {
h.get_buffer_content()
.map_or(false, |c| c.contains("deep indent text\n "))
},
5_000,
)
.unwrap();
let content = buf(&harness);
assert!(
ok && content.contains("deep indent text\n "),
"Expected 8-space indent on new line. Got:\n{:?}",
content,
);
harness.assert_no_plugin_errors();
}
#[test]
fn test_tab_inserts_spaces() {
let (mut harness, _temp_dir) = markdown_source_harness(80, 24);
let content = "text\n";
let fixture = TestFixture::new("tab.md", content).unwrap();
open_md_and_wait_for_mode(&mut harness, &fixture.path);
harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::Tab, KeyModifiers::NONE).unwrap();
let ok = harness
.wait_for_async(
|h| {
h.get_buffer_content()
.map_or(false, |c| c.starts_with(" text"))
},
5_000,
)
.unwrap();
let content = buf(&harness);
assert!(
ok && content.starts_with(" text"),
"Expected 4 spaces before 'text' after Tab. Got:\n{:?}",
content,
);
harness.assert_no_plugin_errors();
}
#[test]
fn test_multiple_tabs() {
let (mut harness, _temp_dir) = markdown_source_harness(80, 24);
let content = "x\n";
let fixture = TestFixture::new("tabs.md", content).unwrap();
open_md_and_wait_for_mode(&mut harness, &fixture.path);
harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::Tab, KeyModifiers::NONE).unwrap();
harness.send_key(KeyCode::Tab, KeyModifiers::NONE).unwrap();
let ok = harness
.wait_for_async(
|h| {
h.get_buffer_content()
.map_or(false, |c| c.starts_with(" x"))
},
5_000,
)
.unwrap();
let content = buf(&harness);
assert!(
ok && content.starts_with(" x"),
"Expected 8 spaces (two tabs) before 'x'. Got:\n{:?}",
content,
);
harness.assert_no_plugin_errors();
}
#[test]
fn test_tab_cycles_bullet_on_blank_item() {
let (mut harness, _temp_dir) = markdown_source_harness(80, 24);
let content = "* \n";
let fixture = TestFixture::new("cycle.md", content).unwrap();
open_md_and_wait_for_mode(&mut harness, &fixture.path);
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::Tab, KeyModifiers::NONE).unwrap();
let ok = harness
.wait_for_async(
|h| {
h.get_buffer_content()
.map_or(false, |c| c.starts_with(" - "))
},
5_000,
)
.unwrap();
let content = buf(&harness);
assert!(
ok && content.starts_with(" - "),
"Expected ' - ' after Tab on '* '. Got:\n{:?}",
content,
);
harness.assert_no_plugin_errors();
}
#[test]
fn test_shift_tab_reverse_cycles_bullet_on_blank_item() {
let (mut harness, _temp_dir) = markdown_source_harness(80, 24);
let content = " - \n";
let fixture = TestFixture::new("reverse_cycle.md", content).unwrap();
open_md_and_wait_for_mode(&mut harness, &fixture.path);
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::BackTab, KeyModifiers::SHIFT)
.unwrap();
harness
.wait_until(|h| {
h.get_buffer_content()
.map_or(false, |c| c.starts_with("* \n"))
})
.unwrap();
harness.assert_no_plugin_errors();
}
#[test]
fn test_tab_shift_tab_full_round_trip() {
let (mut harness, _temp_dir) = markdown_source_harness(80, 24);
let content = "* \n";
let fixture = TestFixture::new("roundtrip.md", content).unwrap();
open_md_and_wait_for_mode(&mut harness, &fixture.path);
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
let drain = |h: &mut EditorTestHarness| {
for _ in 0..3 {
h.process_async_and_render().unwrap();
}
};
harness.send_key(KeyCode::Tab, KeyModifiers::NONE).unwrap();
harness
.wait_until(|h| {
h.get_buffer_content()
.map_or(false, |c| c.starts_with(" - \n"))
})
.unwrap();
drain(&mut harness);
harness.send_key(KeyCode::Tab, KeyModifiers::NONE).unwrap();
harness
.wait_until(|h| {
h.get_buffer_content()
.map_or(false, |c| c.starts_with(" + \n"))
})
.unwrap();
drain(&mut harness);
harness.send_key(KeyCode::Tab, KeyModifiers::NONE).unwrap();
harness
.wait_until(|h| {
h.get_buffer_content()
.map_or(false, |c| c.starts_with(" * \n"))
})
.unwrap();
drain(&mut harness);
harness
.send_key(KeyCode::BackTab, KeyModifiers::SHIFT)
.unwrap();
harness
.wait_until(|h| {
h.get_buffer_content()
.map_or(false, |c| c.starts_with(" + \n"))
})
.unwrap();
drain(&mut harness);
harness
.send_key(KeyCode::BackTab, KeyModifiers::SHIFT)
.unwrap();
harness
.wait_until(|h| {
h.get_buffer_content()
.map_or(false, |c| c.starts_with(" - \n"))
})
.unwrap();
drain(&mut harness);
harness
.send_key(KeyCode::BackTab, KeyModifiers::SHIFT)
.unwrap();
harness
.wait_until(|h| {
h.get_buffer_content()
.map_or(false, |c| c.starts_with("* \n"))
})
.unwrap();
harness.assert_no_plugin_errors();
}
#[test]
fn test_normal_typing_works() {
let (mut harness, _temp_dir) = markdown_source_harness(80, 24);
let content = "\n";
let fixture = TestFixture::new("type.md", content).unwrap();
open_md_and_wait_for_mode(&mut harness, &fixture.path);
harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness.type_text("Hello markdown").unwrap();
harness.render().unwrap();
let content = buf(&harness);
assert!(
content.contains("Hello markdown"),
"Typed text should appear in buffer. Got:\n{:?}",
content,
);
harness.assert_no_plugin_errors();
}
#[test]
fn test_mode_deactivates_on_buffer_switch() {
let (mut harness, temp_dir) = markdown_source_harness(80, 24);
let md_fixture = TestFixture::new("doc.md", "# Doc\n").unwrap();
open_md_and_wait_for_mode(&mut harness, &md_fixture.path);
assert_eq!(
harness.editor().editor_mode(),
Some("markdown-source".to_string()),
);
let txt_path = temp_dir.path().join("project_root").join("notes.txt");
fs::write(&txt_path, "plain text\n").unwrap();
harness.open_file(&txt_path).unwrap();
harness.render().unwrap();
let deactivated = harness
.wait_for_async(|h| h.editor().editor_mode().is_none(), 5_000)
.unwrap();
assert!(
deactivated,
"markdown-source mode should deactivate when switching to a non-md file. Current mode: {:?}",
harness.editor().editor_mode(),
);
harness.assert_no_plugin_errors();
}
#[test]
fn test_activates_for_mdx() {
let (mut harness, _temp_dir) = markdown_source_harness(80, 24);
let fixture = TestFixture::new("component.mdx", "# MDX file\n").unwrap();
open_md_and_wait_for_mode(&mut harness, &fixture.path);
assert_eq!(
harness.editor().editor_mode(),
Some("markdown-source".to_string()),
"Should activate for .mdx files"
);
harness.assert_no_plugin_errors();
}
#[test]
fn test_activates_for_markdown_extension() {
let (mut harness, _temp_dir) = markdown_source_harness(80, 24);
let fixture = TestFixture::new("readme.markdown", "# Readme\n").unwrap();
open_md_and_wait_for_mode(&mut harness, &fixture.path);
assert_eq!(
harness.editor().editor_mode(),
Some("markdown-source".to_string()),
"Should activate for .markdown files"
);
harness.assert_no_plugin_errors();
}
#[test]
fn test_enter_then_type_workflow() {
let (mut harness, _temp_dir) = markdown_source_harness(80, 24);
let content = "- item\n";
let fixture = TestFixture::new("workflow.md", content).unwrap();
open_md_and_wait_for_mode(&mut harness, &fixture.path);
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
let ok = harness
.wait_for_async(
|h| {
h.get_buffer_content()
.map_or(false, |c| c.contains("- item\n- "))
},
5_000,
)
.unwrap();
assert!(ok, "Enter should continue bullet");
harness.type_text("next").unwrap();
harness.render().unwrap();
let content = buf(&harness);
assert!(
content.contains("- item\n- next"),
"Expected '- item\\n- next'. Got:\n{:?}",
content,
);
harness.assert_no_plugin_errors();
}