use crate::common::fake_lsp::FakeLspServer;
use crate::common::fixtures::TestFixture;
use crate::common::harness::EditorTestHarness;
use crate::common::tracing::init_tracing_from_env;
use crossterm::event::{KeyCode, KeyModifiers};
use fresh::config::Config;
use fresh::services::lsp::client::LspServerConfig;
use fresh::services::process_limits::ProcessLimits;
use std::fs;
use std::time::Duration;
#[test]
fn test_render_line_hook_with_args() {
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();
let test_plugin = r###"
// Test plugin to verify render-line hook receives args
let line_count = 0;
let found_marker = false;
globalThis.onRenderLine = function(args: {
buffer_id: number;
line_number: number;
byte_start: number;
byte_end: number;
content: string;
}): boolean {
editor.debug("render-line hook called!");
// Verify args are present
if (args && args.buffer_id !== undefined && args.line_number !== undefined && args.content !== undefined) {
line_count = line_count + 1;
editor.debug(`Line ${args.line_number}: ${args.content}`);
// Look for "TEST_MARKER" in the content
if (args.content.includes("TEST_MARKER")) {
found_marker = true;
editor.debug("Found TEST_MARKER!");
editor.setStatus(`Found TEST_MARKER on line ${args.line_number} at byte ${args.byte_start}`);
}
} else {
editor.debug("ERROR: args is nil or missing fields!");
}
return true;
};
editor.on("render_line", "onRenderLine");
globalThis.test_show_count = function(): void {
editor.setStatus(`Rendered ${line_count} lines, found=${found_marker}`);
line_count = 0; // Reset counter
found_marker = false;
};
editor.registerCommand(
"Test: Show Line Count",
"Show how many lines were rendered",
"test_show_count",
"normal"
);
editor.setStatus("Test plugin loaded!");
"###;
let test_plugin_path = plugins_dir.join("test_render_hook.ts");
fs::write(&test_plugin_path, test_plugin).unwrap();
let test_file_content = "Line 1\nLine 2\nTEST_MARKER line\nLine 4\n";
let fixture = TestFixture::new("test_render.txt", test_file_content).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(80, 24, Default::default(), project_root)
.unwrap();
harness.open_file(&fixture.path).unwrap();
harness.render().unwrap();
harness.assert_screen_contains("TEST_MARKER");
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("Test: Show Line Count").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness.assert_screen_contains("TEST_MARKER");
}
#[test]
fn test_todo_highlighter_plugin() {
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();
let plugin_source = std::env::current_dir()
.unwrap()
.join("plugins/todo_highlighter.ts");
let plugin_dest = plugins_dir.join("todo_highlighter.ts");
fs::copy(&plugin_source, &plugin_dest).unwrap();
let test_file_content = r#"// This is a test file for the TODO Highlighter plugin
// TODO: Implement user authentication
// FIXME: Memory leak in connection pool
// HACK: Temporary workaround for parser bug
// NOTE: This function is performance-critical
// XXX: Needs review before production
// BUG: Off-by-one error in loop counter
# Python-style comments
# TODO: Add type hints to all functions
# FIXME: Handle edge case when list is empty
Regular text without keywords should not be highlighted:
TODO FIXME HACK NOTE XXX BUG (not in comments)
"#;
let fixture = TestFixture::new("test_todo.txt", test_file_content).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(80, 24, Default::default(), project_root)
.unwrap();
harness.open_file(&fixture.path).unwrap();
harness.render().unwrap();
harness.assert_screen_contains("TODO: Implement user authentication");
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("TODO Highlighter: Enable").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.process_async_and_render().unwrap();
harness.process_async_and_render().unwrap();
harness.process_async_and_render().unwrap();
let screen = harness.screen_to_string();
println!("Screen after enabling TODO highlighter:\n{}", screen);
let lines: Vec<&str> = screen.lines().collect();
let mut found_highlighted_todo = false;
for (y, line) in lines.iter().enumerate() {
if let Some(x) = line.find("TODO") {
if line[..x].contains("//") {
if let Some(style) = harness.get_cell_style(x as u16, y as u16) {
if let Some(fg) = style.fg {
println!(
"Found TODO at ({}, {}) with foreground color: {:?}",
x, y, fg
);
if matches!(fg, ratatui::style::Color::Rgb(_, _, _)) {
found_highlighted_todo = true;
break;
}
}
}
}
}
}
assert!(
found_highlighted_todo,
"Expected to find at least one highlighted TODO keyword"
);
}
#[test]
fn test_todo_highlighter_disable() {
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();
let plugin_source = std::env::current_dir()
.unwrap()
.join("plugins/todo_highlighter.ts");
let plugin_dest = plugins_dir.join("todo_highlighter.ts");
fs::copy(&plugin_source, &plugin_dest).unwrap();
let test_file_content = "// TODO: Test comment\n";
let fixture = TestFixture::new("test_todo.txt", test_file_content).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(80, 24, Default::default(), project_root)
.unwrap();
harness.open_file(&fixture.path).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("TODO Highlighter: Enable").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("TODO Highlighter: Disable").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness.assert_screen_contains("TODO: Test comment");
}
#[test]
fn test_todo_highlighter_toggle() {
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();
let plugin_source = std::env::current_dir()
.unwrap()
.join("plugins/todo_highlighter.ts");
let plugin_dest = plugins_dir.join("todo_highlighter.ts");
fs::copy(&plugin_source, &plugin_dest).unwrap();
let test_file_content = "// TODO: Test comment\n";
let fixture = TestFixture::new("test_todo.txt", test_file_content).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(80, 24, Default::default(), project_root)
.unwrap();
harness.open_file(&fixture.path).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("TODO Highlighter: Toggle").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.process_async_and_render().unwrap();
harness.process_async_and_render().unwrap();
harness.process_async_and_render().unwrap();
let screen = harness.screen_to_string();
let lines: Vec<&str> = screen.lines().collect();
let mut found_highlighted = false;
for (y, line) in lines.iter().enumerate() {
if let Some(x) = line.find("TODO") {
if line[..x].contains("//") {
if let Some(style) = harness.get_cell_style(x as u16, y as u16) {
if let Some(fg) = style.fg {
if matches!(fg, ratatui::style::Color::Rgb(_, _, _)) {
found_highlighted = true;
break;
}
}
}
}
}
}
assert!(
found_highlighted,
"Expected TODO to be highlighted after toggle on"
);
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("TODO Highlighter: Toggle").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness.assert_screen_contains("TODO: Test comment");
}
#[test]
#[ignore = "Overlays don't update positions when buffer changes - needs overlay position tracking fix"]
fn test_todo_highlighter_updates_on_edit() {
use tracing_subscriber::{fmt, EnvFilter};
let _ = fmt()
.with_env_filter(
EnvFilter::from_default_env().add_directive("fresh=trace".parse().unwrap()),
)
.with_test_writer()
.try_init();
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();
let plugin_source = std::env::current_dir()
.unwrap()
.join("plugins/todo_highlighter.ts");
let plugin_dest = plugins_dir.join("todo_highlighter.ts");
fs::copy(&plugin_source, &plugin_dest).unwrap();
let test_file_content = "// TODO: Original comment\n";
let fixture = TestFixture::new("test_todo.txt", test_file_content).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(80, 24, Default::default(), project_root)
.unwrap();
harness.open_file(&fixture.path).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("TODO Highlighter: Enable").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
let screen_before = harness.screen_to_string();
println!("Screen before edit:\n{}", screen_before);
let lines: Vec<&str> = screen_before.lines().collect();
let mut found_original_todo = false;
for (y, line) in lines.iter().enumerate() {
if line.contains("TODO: Original") {
if let Some(x) = line.find("TODO") {
if let Some(style) = harness.get_cell_style(x as u16, y as u16) {
if let Some(bg) = style.bg {
println!("Found TODO at ({}, {}) with background: {:?}", x, y, bg);
found_original_todo = true;
break;
}
}
}
}
}
assert!(
found_original_todo,
"Expected to find highlighted 'TODO: Original' before edit"
);
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("// FIXME: New comment\n").unwrap();
harness.render().unwrap();
let screen_after = harness.screen_to_string();
println!("Screen after adding FIXME:\n{}", screen_after);
let lines: Vec<&str> = screen_after.lines().collect();
let mut found_fixme = false;
let mut found_todo_on_line_2 = false;
for (y, line) in lines.iter().enumerate() {
if line.contains("FIXME: New") {
if let Some(x) = line.find("FIXME") {
if let Some(style) = harness.get_cell_style(x as u16, y as u16) {
if let Some(bg) = style.bg {
println!("Found FIXME at ({}, {}) with background: {:?}", x, y, bg);
found_fixme = true;
}
}
}
}
if line.contains("TODO: Original") {
if let Some(x) = line.find("TODO") {
if let Some(style) = harness.get_cell_style(x as u16, y as u16) {
if let Some(bg) = style.bg {
println!(
"Found TODO on line 2 at ({}, {}) with background: {:?}",
x, y, bg
);
if matches!(bg, ratatui::style::Color::Rgb(_, _, _)) {
found_todo_on_line_2 = true;
}
}
}
}
}
}
assert!(
found_fixme,
"Expected to find highlighted FIXME after inserting new line"
);
assert!(
found_todo_on_line_2,
"BUG REPRODUCED: TODO on line 2 is not highlighted! The old overlay at byte 3-7 \
now highlights FIXME (which happens to be at those bytes), but TODO moved to a \
new byte position and didn't get a new overlay. Overlays need to update when buffer changes!"
);
}
#[test]
fn test_todo_highlighter_updates_on_delete() {
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();
let plugin_source = std::env::current_dir()
.unwrap()
.join("plugins/todo_highlighter.ts");
let plugin_dest = plugins_dir.join("todo_highlighter.ts");
fs::copy(&plugin_source, &plugin_dest).unwrap();
let test_file_content = "// FIXME: Delete this line\n// TODO: Keep this one\n";
let fixture = TestFixture::new("test_todo.txt", test_file_content).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(80, 24, Default::default(), project_root)
.unwrap();
harness.open_file(&fixture.path).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("TODO Highlighter: Enable").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness.render().unwrap();
let screen_before = harness.screen_to_string();
println!("Screen before delete:\n{}", screen_before);
let mut found_fixme_before = false;
let mut found_todo_before = false;
for (y, line) in screen_before.lines().enumerate() {
if line.contains("FIXME") && line[..line.find("FIXME").unwrap()].contains("//") {
if let Some(x) = line.find("FIXME") {
if let Some(style) = harness.get_cell_style(x as u16, y as u16) {
if let Some(bg) = style.bg {
println!(
"Found FIXME highlighted at ({}, {}) before delete with bg: {:?}",
x, y, bg
);
found_fixme_before = true;
}
}
}
}
if line.contains("TODO") && line[..line.find("TODO").unwrap()].contains("//") {
if let Some(x) = line.find("TODO") {
if let Some(style) = harness.get_cell_style(x as u16, y as u16) {
if let Some(bg) = style.bg {
println!(
"Found TODO highlighted at ({}, {}) before delete with bg: {:?}",
x, y, bg
);
found_todo_before = true;
}
}
}
}
}
assert!(found_fixme_before, "FIXME should be highlighted initially");
assert!(found_todo_before, "TODO should be highlighted initially");
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::End, KeyModifiers::SHIFT).unwrap();
harness
.send_key(KeyCode::Right, KeyModifiers::SHIFT)
.unwrap(); harness.render().unwrap();
harness
.send_key(KeyCode::Backspace, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
let screen_after = harness.screen_to_string();
println!("Screen after deleting FIXME line:\n{}", screen_after);
let mut found_todo_after = false;
for (y, line) in screen_after.lines().enumerate() {
if line.contains("TODO") && line[..line.find("TODO").unwrap()].contains("//") {
if let Some(x) = line.find("TODO") {
if let Some(style) = harness.get_cell_style(x as u16, y as u16) {
if let Some(bg) = style.bg {
println!(
"Found TODO at ({}, {}) after delete with background: {:?}",
x, y, bg
);
found_todo_after = true;
}
}
}
}
}
assert!(
found_todo_after,
"BUG: TODO should still be highlighted after deleting the line above it! \
Instead, the highlight either disappeared or shifted to the wrong position."
);
}
#[test]
fn test_diagnostics_panel_plugin_loads() {
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();
let plugin_source = std::env::current_dir()
.unwrap()
.join("plugins/diagnostics_panel.ts");
let plugin_dest = plugins_dir.join("diagnostics_panel.ts");
fs::copy(&plugin_source, &plugin_dest).unwrap();
let test_file_content = "fn main() {\n println!(\"test\");\n}\n";
let fixture = TestFixture::new("test_diagnostics.rs", test_file_content).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(80, 24, Default::default(), project_root)
.unwrap();
harness.open_file(&fixture.path).unwrap();
harness.render().unwrap();
harness.assert_screen_contains("fn main()");
let screen = harness.screen_to_string();
println!("Screen after plugin load:\n{}", screen);
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Show Diagnostics").unwrap();
harness.render().unwrap();
let palette_screen = harness.screen_to_string();
println!("Command palette screen:\n{}", palette_screen);
assert!(
palette_screen.contains("Show Diagnostics")
|| palette_screen.contains("diagnostics")
|| palette_screen.contains("Diagnostics"),
"The 'Show Diagnostics' command should be registered by the plugin. \
If the plugin had Lua scoping errors, it wouldn't load and the command wouldn't exist."
);
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
let final_screen = harness.screen_to_string();
println!("Final screen after executing command:\n{}", final_screen);
assert!(
final_screen.contains("LSP Diagnostics"),
"Expected to see 'LSP Diagnostics' header in the panel"
);
assert!(
final_screen.contains("[E]") || final_screen.contains("[W]"),
"Expected to see severity icons like [E] or [W] in the diagnostics"
);
assert!(
final_screen.contains(">"),
"Expected to see '>' marker for selected diagnostic"
);
assert!(
final_screen.contains("*Diagnostics*"),
"Expected to see buffer name '*Diagnostics*' in status bar"
);
assert!(
final_screen.contains("───") || final_screen.contains("---"),
"Expected to see horizontal split separator"
);
assert!(
final_screen.contains("fn main()"),
"Expected to see original code buffer in upper split"
);
}
#[test]
fn test_plugin_message_queue_architecture() {
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();
let test_plugin = r#"
// Test plugin for message queue architecture
// This plugin exercises the bidirectional message flow
// Register a command that will create a virtual buffer
editor.registerCommand(
"Test: Create Virtual Buffer",
"Create a virtual buffer and verify buffer ID is returned",
"test_create_virtual_buffer",
"normal"
);
// Counter to track executions
let executionCount = 0;
globalThis.test_create_virtual_buffer = async function(): Promise<void> {
executionCount++;
editor.setStatus(`Starting execution ${executionCount}...`);
// Create entries for the virtual buffer
const entries = [
{
text: `Test entry ${executionCount}\n`,
properties: {
index: executionCount,
},
},
];
try {
// This is the critical async operation that tests:
// 1. Plugin sends CreateVirtualBufferInSplit command
// 2. Editor creates buffer and sends response
// 3. Plugin receives buffer ID via async channel
const bufferId = await editor.createVirtualBufferInSplit({
name: "*Test Buffer*",
mode: "normal",
read_only: true,
entries: entries,
ratio: 0.5,
panel_id: "test-panel",
show_line_numbers: false,
show_cursors: true,
});
// Verify we got a valid buffer ID
if (typeof bufferId === 'number' && bufferId > 0) {
editor.setStatus(`Success: Created buffer ID ${bufferId}`);
} else {
editor.setStatus(`Error: Invalid buffer ID: ${bufferId}`);
}
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
editor.setStatus(`Error: ${msg}`);
}
};
editor.setStatus("Message queue test plugin loaded!");
"#;
let test_plugin_path = plugins_dir.join("test_message_queue.ts");
fs::write(&test_plugin_path, test_plugin).unwrap();
let test_file_content = "Test file content\nLine 2\nLine 3\n";
let fixture = TestFixture::new("test_file.txt", test_file_content).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(80, 24, Default::default(), project_root)
.unwrap();
harness.open_file(&fixture.path).unwrap();
harness.render().unwrap();
harness.assert_screen_contains("Test file content");
let screen = harness.screen_to_string();
println!("Screen after plugin load:\n{}", screen);
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Test: Create Virtual Buffer").unwrap();
harness.render().unwrap();
let palette_screen = harness.screen_to_string();
println!("Command palette screen:\n{}", palette_screen);
assert!(
palette_screen.contains("Create Virtual Buffer"),
"Command should be registered and visible in palette"
);
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
let completed = harness
.wait_for_async(
|h| {
let screen = h.screen_to_string();
screen.contains("*Test Buffer*") || screen.contains("Test entry")
},
5000,
)
.unwrap();
let final_screen = harness.screen_to_string();
println!("Final screen after command execution:\n{}", final_screen);
assert!(
completed,
"Async operation should complete within timeout. Got:\n{}",
final_screen
);
assert!(
final_screen.contains("*Test Buffer*") || final_screen.contains("Test entry"),
"Expected to see the virtual buffer content. \
The split should show either the buffer name or entry content. \
Got screen:\n{}",
final_screen
);
assert!(
final_screen.contains("Test file content"),
"Expected original file content to still be visible in split view"
);
}
#[test]
fn test_plugin_multiple_actions_no_deadlock() {
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();
let test_plugin = r#"
// Test plugin for multiple concurrent actions
editor.registerCommand("Action A", "Set status to A", "action_a", "normal");
editor.registerCommand("Action B", "Set status to B", "action_b", "normal");
editor.registerCommand("Action C", "Set status to C", "action_c", "normal");
globalThis.action_a = function(): void {
editor.setStatus("Status: A executed");
};
globalThis.action_b = function(): void {
editor.setStatus("Status: B executed");
};
globalThis.action_c = function(): void {
editor.setStatus("Status: C executed");
};
editor.setStatus("Multi-action plugin loaded");
"#;
let test_plugin_path = plugins_dir.join("test_multi_action.ts");
fs::write(&test_plugin_path, test_plugin).unwrap();
let test_file_content = "Test content\n";
let fixture = TestFixture::new("test.txt", test_file_content).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(80, 24, Default::default(), project_root)
.unwrap();
harness.open_file(&fixture.path).unwrap();
harness.render().unwrap();
let start = std::time::Instant::now();
for action_name in ["Action A", "Action B", "Action C"] {
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text(action_name).unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
for _ in 0..3 {
harness.process_async_and_render().unwrap();
std::thread::sleep(Duration::from_millis(20));
}
}
let elapsed = start.elapsed();
assert!(
elapsed < Duration::from_secs(2),
"Multiple actions should complete without deadlock. Took {:?}",
elapsed
);
let screen = harness.screen_to_string();
assert!(
screen.contains("Test content"),
"Editor should still show content after multiple actions. Got:\n{}",
screen
);
}
#[test]
fn test_plugin_action_nonblocking() {
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();
let test_plugin = r#"
// Test plugin to verify non-blocking action execution
editor.registerCommand(
"Slow Action",
"An action that does some computation",
"slow_action",
"normal"
);
globalThis.slow_action = function(): void {
// Simulate some work (this is synchronous but should not block editor)
let sum = 0;
for (let i = 0; i < 1000; i++) {
sum += i;
}
editor.setStatus(`Completed: sum = ${sum}`);
};
editor.setStatus("Nonblocking test plugin loaded");
"#;
let test_plugin_path = plugins_dir.join("test_nonblocking.ts");
fs::write(&test_plugin_path, test_plugin).unwrap();
let test_file_content = "Test\n";
let fixture = TestFixture::new("test.txt", test_file_content).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(80, 24, Default::default(), project_root)
.unwrap();
harness.open_file(&fixture.path).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Slow Action").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
let start = std::time::Instant::now();
for _ in 0..5 {
harness.process_async_and_render().unwrap();
std::thread::sleep(Duration::from_millis(50));
}
let elapsed = start.elapsed();
assert!(
elapsed < Duration::from_secs(1),
"Rendering should complete quickly even with action running. Took {:?}",
elapsed
);
let screen = harness.screen_to_string();
assert!(
screen.contains("Test"),
"Editor should show file content. Got:\n{}",
screen
);
}
#[test]
fn test_todo_highlighter_cursor_perf() {
let _ = tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::from_default_env()
.add_directive("fresh=trace".parse().unwrap()),
)
.with_test_writer()
.try_init();
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();
let plugin_source = std::env::current_dir()
.unwrap()
.join("plugins/todo_highlighter.ts");
let plugin_dest = plugins_dir.join("todo_highlighter.ts");
fs::copy(&plugin_source, &plugin_dest).unwrap();
let mut test_content = String::new();
for i in 0..100 {
if i % 5 == 0 {
test_content.push_str(&format!("// TODO: Task number {}\n", i));
} else if i % 7 == 0 {
test_content.push_str(&format!("// FIXME: Issue number {}\n", i));
} else {
test_content.push_str(&format!("Line {} of test content\n", i));
}
}
let fixture = TestFixture::new("test_perf.txt", &test_content).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(80, 40, Default::default(), project_root)
.unwrap();
harness.open_file(&fixture.path).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("TODO Highlighter: Enable").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
let num_moves = 20;
println!("\n=== TODO Highlighter Cursor Movement Performance Test ===");
println!("Moving cursor down {} times...", num_moves);
let down_start = std::time::Instant::now();
for _ in 0..num_moves {
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
}
let down_elapsed = down_start.elapsed();
println!("Moving cursor up {} times...", num_moves);
let up_start = std::time::Instant::now();
for _ in 0..num_moves {
harness.send_key(KeyCode::Up, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
}
let up_elapsed = up_start.elapsed();
println!("\n=== Results ===");
println!(
"Down: {:?} total, {:?} per move",
down_elapsed,
down_elapsed / num_moves
);
println!(
"Up: {:?} total, {:?} per move",
up_elapsed,
up_elapsed / num_moves
);
println!("Total: {:?}", down_elapsed + up_elapsed);
println!("================\n");
}
#[test]
#[ignore]
fn test_color_highlighter_plugin() {
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();
let plugin_source = std::env::current_dir()
.unwrap()
.join("plugins/color_highlighter.ts");
let plugin_dest = plugins_dir.join("color_highlighter.ts");
fs::copy(&plugin_source, &plugin_dest).unwrap();
let test_file_content = r###"// Test file for Color Highlighter
// CSS hex colors
let red = "#ff0000";
let green = "#0f0";
let blue = "#0000ff";
let transparent = "#ff000080";
// CSS rgb/rgba
background: rgb(255, 128, 0);
color: rgba(0, 255, 128, 0.5);
// CSS hsl/hsla
hsl(120, 100%, 50%);
hsla(240, 100%, 50%, 0.8);
// Rust colors
Color::Rgb(255, 255, 0)
Color::Rgb(128, 0, 255)
"###;
let fixture = TestFixture::new("test_colors.txt", test_file_content).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(80, 24, Default::default(), project_root)
.unwrap();
harness.open_file(&fixture.path).unwrap();
harness.render().unwrap();
harness.assert_screen_contains("#ff0000");
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("Color Highlighter: Enable").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness.render().unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
println!("Screen after enabling Color highlighter:\n{}", screen);
let swatch_count = screen.matches('â–ˆ').count();
println!("Found {} color swatches", swatch_count);
assert!(
swatch_count > 0,
"Expected to find color swatch characters (â–ˆ) after enabling Color Highlighter. \
This indicates virtual text is being rendered."
);
let lines: Vec<&str> = screen.lines().collect();
let mut found_colored_swatch = false;
for (y, line) in lines.iter().enumerate() {
for (char_idx, ch) in line.char_indices() {
if ch == 'â–ˆ' {
let x = line[..char_idx].chars().count();
if x >= 80 {
continue;
}
if let Some(style) = harness.get_cell_style(x as u16, y as u16) {
if let Some(fg) = style.fg {
println!(
"Found swatch at ({}, {}) with foreground color: {:?}",
x, y, fg
);
if matches!(fg, ratatui::style::Color::Rgb(_, _, _)) {
found_colored_swatch = true;
break;
}
}
}
}
}
if found_colored_swatch {
break;
}
}
assert!(
found_colored_swatch,
"Expected to find at least one color swatch with RGB foreground color"
);
}
#[test]
fn test_color_highlighter_disable() {
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();
let plugin_source = std::env::current_dir()
.unwrap()
.join("plugins/color_highlighter.ts");
let plugin_dest = plugins_dir.join("color_highlighter.ts");
fs::copy(&plugin_source, &plugin_dest).unwrap();
let test_file_content = "let color = \"#ff0000\";\n";
let fixture = TestFixture::new("test_colors.txt", test_file_content).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(80, 24, Default::default(), project_root)
.unwrap();
harness.open_file(&fixture.path).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("Color Highlighter: Enable").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.process_async_and_render().unwrap();
harness.process_async_and_render().unwrap();
harness.process_async_and_render().unwrap();
fn count_color_swatches(screen: &str) -> usize {
screen
.lines()
.flat_map(|line| {
line.char_indices().filter(|&(char_idx, ch)| {
if ch != 'â–ˆ' {
return false;
}
let x = line[..char_idx].chars().count();
x < 79
})
})
.count()
}
let screen_enabled = harness.screen_to_string();
let swatches_enabled = count_color_swatches(&screen_enabled);
assert!(
swatches_enabled > 0,
"Expected swatches when enabled. Got:\n{}",
screen_enabled
);
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("Color Highlighter: Disable").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness.assert_screen_contains("#ff0000");
let screen_disabled = harness.screen_to_string();
let swatches_disabled = count_color_swatches(&screen_disabled);
assert!(
swatches_disabled < swatches_enabled,
"Expected fewer swatches after disabling. Before: {}, After: {}",
swatches_enabled,
swatches_disabled
);
}
#[test]
fn test_color_highlighter_toggle() {
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();
let plugin_source = std::env::current_dir()
.unwrap()
.join("plugins/color_highlighter.ts");
let plugin_dest = plugins_dir.join("color_highlighter.ts");
fs::copy(&plugin_source, &plugin_dest).unwrap();
let test_file_content = "rgb(128, 64, 255)\n";
let fixture = TestFixture::new("test_colors.txt", test_file_content).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(80, 24, Default::default(), project_root)
.unwrap();
harness.open_file(&fixture.path).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("Color Highlighter: Toggle").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness.render().unwrap();
let screen_on = harness.screen_to_string();
let swatches_on = screen_on.matches('â–ˆ').count();
assert!(
swatches_on > 0,
"Expected swatches after toggle on. Got:\n{}",
screen_on
);
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("Color Highlighter: Toggle").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness.assert_screen_contains("rgb(128, 64, 255)");
}
#[test]
fn test_panel_id_cleanup_after_buffer_close() {
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();
let test_plugin = r###"
// Test plugin for panel_id cleanup after buffer close
// This reproduces the find references bug where closing and reopening fails
let panelBufferId: number | null = null;
let panelSplitId: number | null = null;
let executionCount = 0;
editor.registerCommand(
"Test: Create Panel",
"Create a virtual buffer panel with panel_id",
"test_create_panel",
"normal"
);
editor.registerCommand(
"Test: Close Panel",
"Close the panel buffer",
"test_close_panel",
"normal"
);
// Create panel with panel_id
globalThis.test_create_panel = async function(): Promise<void> {
executionCount++;
editor.setStatus(`Creating panel (attempt ${executionCount})...`);
const entries = [
{
text: `Panel content ${executionCount}\n`,
properties: { index: executionCount },
},
{
text: `Created at ${Date.now()}\n`,
properties: { type: "info" },
},
];
try {
panelBufferId = await editor.createVirtualBufferInSplit({
name: "*Test Panel*",
mode: "normal",
read_only: true,
entries: entries,
ratio: 0.7,
panel_id: "test-reusable-panel", // Same panel_id every time
show_line_numbers: false,
show_cursors: true,
});
panelSplitId = editor.getActiveSplitId();
editor.setStatus(`Panel created (attempt ${executionCount}): buffer ID ${panelBufferId}`);
editor.debug(`Panel created with buffer ID ${panelBufferId}, split ID ${panelSplitId}`);
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
editor.setStatus(`Panel creation FAILED (attempt ${executionCount}): ${msg}`);
editor.debug(`ERROR: Panel creation failed: ${msg}`);
}
};
// Close the panel
globalThis.test_close_panel = function(): void {
if (panelBufferId !== null) {
editor.closeBuffer(panelBufferId);
editor.setStatus(`Panel closed (buffer ID was ${panelBufferId})`);
editor.debug(`Closed panel buffer ID ${panelBufferId}`);
if (panelSplitId !== null) {
editor.closeSplit(panelSplitId);
}
panelBufferId = null;
panelSplitId = null;
} else {
editor.setStatus("No panel to close");
}
};
editor.setStatus("Panel cleanup test plugin loaded");
"###;
let test_plugin_path = plugins_dir.join("test_panel_cleanup.ts");
fs::write(&test_plugin_path, test_plugin).unwrap();
let test_file_content = "Test file for panel cleanup\n";
let fixture = TestFixture::new("test.txt", test_file_content).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(80, 24, Default::default(), project_root)
.unwrap();
harness.open_file(&fixture.path).unwrap();
harness.render().unwrap();
for _ in 0..5 {
harness.render().unwrap();
std::thread::sleep(Duration::from_millis(50));
}
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Test: Create Panel").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
for _ in 0..10 {
harness.process_async_and_render().unwrap();
std::thread::sleep(Duration::from_millis(50));
}
let screen1 = harness.screen_to_string();
println!("Screen after first panel creation:\n{}", screen1);
assert!(
screen1.contains("Panel content 1"),
"First panel content should be visible. Got:\n{}",
screen1
);
assert!(
screen1.contains("*Test Panel*"),
"First panel tab should be visible. Got:\n{}",
screen1
);
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Test: Close Panel").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
for _ in 0..5 {
harness.process_async_and_render().unwrap();
std::thread::sleep(Duration::from_millis(50));
}
let screen2 = harness.screen_to_string();
println!("Screen after closing panel:\n{}", screen2);
assert!(
!screen2.contains("Panel content 1"),
"Panel content should be hidden after close. Got:\n{}",
screen2
);
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Test: Create Panel").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
for _ in 0..10 {
harness.process_async_and_render().unwrap();
std::thread::sleep(Duration::from_millis(50));
}
let screen3 = harness.screen_to_string();
println!("Screen after second panel creation:\n{}", screen3);
assert!(
screen3.contains("Panel content 2"),
"BUG: Second panel creation should succeed, but it failed. \
The panel_ids mapping wasn't cleaned up when the buffer was closed, \
causing the editor to try to reuse a non-existent buffer ID. Got:\n{}",
screen3
);
assert!(
screen3.contains("*Test Panel*"),
"Second panel tab should be visible. Got:\n{}",
screen3
);
}
#[test]
#[ignore]
fn test_clangd_plugin_file_status_notification() -> std::io::Result<()> {
init_tracing_from_env();
let _fake_server = FakeLspServer::spawn()?;
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();
let plugin_source = std::env::current_dir()
.unwrap()
.join("plugins/clangd_support.ts");
fs::copy(&plugin_source, plugins_dir.join("clangd_support.ts")).unwrap();
let lib_source_dir = std::env::current_dir().unwrap().join("plugins/lib");
let lib_dest_dir = plugins_dir.join("lib");
fs::create_dir(&lib_dest_dir).unwrap();
for entry in fs::read_dir(&lib_source_dir).unwrap() {
let entry = entry.unwrap();
if entry.path().extension().map(|e| e == "ts").unwrap_or(false) {
fs::copy(entry.path(), lib_dest_dir.join(entry.file_name())).unwrap();
}
}
let src_dir = project_root.join("src");
fs::create_dir_all(&src_dir).unwrap();
let source_file = src_dir.join("main.cpp");
fs::write(&source_file, "int main() { return 0; }\n").unwrap();
let mut config = Config::default();
config.lsp.insert(
"cpp".to_string(),
LspServerConfig {
command: FakeLspServer::script_path().to_string_lossy().to_string(),
args: vec![],
enabled: true,
auto_start: false,
process_limits: ProcessLimits::default(),
},
);
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 30, config, project_root.clone())
.unwrap();
harness.open_file(&source_file)?;
harness.render()?;
for _ in 0..10 {
std::thread::sleep(Duration::from_millis(100));
let _ = harness.editor_mut().process_async_messages();
harness.render()?;
}
let mut seen_status = false;
for _ in 0..20 {
std::thread::sleep(Duration::from_millis(50));
let _ = harness.editor_mut().process_async_messages();
harness.render()?;
if let Some(msg) = harness.editor().get_status_message() {
if msg == "Clangd file status: ready" {
seen_status = true;
break;
}
}
}
assert!(
seen_status,
"Expected clangd file status notification to set the plugin status"
);
Ok(())
}
#[test]
fn test_clangd_plugin_switch_source_header() -> std::io::Result<()> {
init_tracing_from_env();
let _fake_server = FakeLspServer::spawn()?;
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();
let plugin_source = std::env::current_dir()
.unwrap()
.join("plugins/clangd_support.ts");
fs::copy(&plugin_source, plugins_dir.join("clangd_support.ts")).unwrap();
let lib_source_dir = std::env::current_dir().unwrap().join("plugins/lib");
let lib_dest_dir = plugins_dir.join("lib");
fs::create_dir(&lib_dest_dir).unwrap();
for entry in fs::read_dir(&lib_source_dir).unwrap() {
let entry = entry.unwrap();
if entry.path().extension().map(|e| e == "ts").unwrap_or(false) {
fs::copy(entry.path(), lib_dest_dir.join(entry.file_name())).unwrap();
}
}
let src_dir = project_root.join("src");
fs::create_dir_all(&src_dir).unwrap();
let source_file = src_dir.join("main.cpp");
fs::write(&source_file, "int main() { return 0; }\n").unwrap();
let header_file = src_dir.join("main.h");
fs::write(&header_file, "// header content\n").unwrap();
let mut config = Config::default();
config.lsp.insert(
"cpp".to_string(),
LspServerConfig {
command: FakeLspServer::script_path().to_string_lossy().to_string(),
args: vec![],
enabled: true,
auto_start: false,
process_limits: ProcessLimits::default(),
},
);
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 30, config, project_root.clone())
.unwrap();
harness.open_file(&source_file)?;
harness.render()?;
for _ in 0..10 {
std::thread::sleep(Duration::from_millis(100));
let _ = harness.editor_mut().process_async_messages();
harness.render()?;
}
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("Clangd: Switch Source/Header").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
for _ in 0..5 {
std::thread::sleep(Duration::from_millis(50));
let _ = harness.editor_mut().process_async_messages();
harness.render()?;
}
let screen = harness.screen_to_string();
assert!(
screen.contains("header content"),
"Expected header file to be visible"
);
assert!(
screen.contains("Clangd: opened corresponding file"),
"Expected clangd status message"
);
assert_eq!(
harness
.editor()
.get_status_message()
.map(|msg| msg.as_str()),
Some("Clangd: opened corresponding file")
);
Ok(())
}