pub mod batteries;
pub mod buffer;
pub mod context_menu;
pub mod coordinates;
pub mod effect;
pub mod error;
pub mod history;
pub mod hook;
pub mod keycode;
#[cfg(feature = "markdown")]
#[path = "batteries/markdown/mod.rs"]
pub mod markdown;
pub mod movement;
pub mod prompt;
pub mod search;
pub mod search_hook;
pub mod selection;
pub mod syntax;
pub use buffer::EditorBuffer;
pub use context_menu::{
COPY_ID, CUT_ID, ContextMenuCaps, ContextMenuContext, ContextMenuItem, ContextMenuState,
DELETE_ID, KeyHint, PASTE_ID, REDO_ID, SELECT_ALL_ID, UNDO_ID, collect_context_items,
default_context_items,
};
pub use coordinates::Point;
pub use effect::HookEffect;
pub use error::{EditorError, Result as EditorResult};
pub use hook::{
AutoPairsHook, CursorStyle, EditorHook, HookContext, HookOutcome, KeyEvent, Modifiers,
SearchSnapshot,
};
pub use keycode::KeyCode;
#[cfg(feature = "markdown")]
pub use markdown::{
ConcealMode, MarkdownConfig, MarkdownHighlighter, MarkdownHook, TABLE_CELL_TAG,
TABLE_DELIMITER_TAG, TABLE_HEADER_TAG, TableAlignment, TableBlock, TableLayout, TableRowKind,
fence_rows, find_unescaped_pipes, is_fenced_row, parse_delimiter_row, split_table_cells,
table_block_at, table_block_at_with_fences, table_layouts, table_layouts_with_fences,
};
pub use movement::{
CharKind, classify_char, find_line_end, find_line_range_at, find_line_start,
find_next_word_end, find_prev_word_start, find_word_range_at,
};
pub use prompt::{
PromptAction, PromptItem, PromptPlacement, PromptSpec, PromptState, fuzzy_filter, fuzzy_score,
};
pub use search::{SearchQuery, SearchState, find_matches, find_next, find_prev, replace_all_query};
pub use search::{collect_replacements, replace_one_query};
pub use search_hook::{REPLACE_PROMPT_ID, SEARCH_PROMPT_ID, SearchAction, SearchHook};
pub use selection::Selection;
pub use syntax::{
ConcealedLine, DisplayPad, HighlightTag, Rgba, StyleSpan, StyleValue, StyledSegment,
SyntaxHighlighter, TextStyle, UnderlineDecoration, display_width, split_line_intervals,
};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn delete_removes_character_at_cursor() {
let mut buffer = EditorBuffer::new("hello");
buffer.set_cursor_offset(1);
buffer.delete();
assert_eq!(buffer.text().to_string(), "hllo");
assert_eq!(buffer.cursor_offset(), 1);
}
#[test]
fn delete_at_end_does_nothing() {
let mut buffer = EditorBuffer::new("hello");
buffer.set_cursor_offset(5);
buffer.delete();
assert_eq!(buffer.text().to_string(), "hello");
assert_eq!(buffer.cursor_offset(), 5);
}
#[test]
fn delete_from_empty_buffer_does_nothing() {
let mut buffer = EditorBuffer::new("");
buffer.delete();
assert_eq!(buffer.text().to_string(), "");
assert_eq!(buffer.cursor_offset(), 0);
}
#[test]
fn delete_handles_unicode() {
let mut buffer = EditorBuffer::new("héllo");
buffer.set_cursor_offset(1);
assert_eq!(buffer.cursor_offset(), 1);
assert_eq!(buffer.text().to_string(), "héllo");
buffer.delete();
assert_eq!(buffer.text().to_string(), "hllo");
assert_eq!(buffer.cursor_offset(), 1);
}
#[test]
fn delete_records_undo_history() {
let mut buffer = EditorBuffer::new("hello");
buffer.set_cursor_offset(1);
buffer.delete();
buffer.undo();
assert_eq!(buffer.text().to_string(), "hello");
assert_eq!(buffer.cursor_offset(), 1);
}
#[test]
fn delete_can_be_redone() {
let mut buffer = EditorBuffer::new("hello");
buffer.set_cursor_offset(1);
buffer.delete();
buffer.undo();
buffer.redo();
assert_eq!(buffer.text().to_string(), "hllo");
assert_eq!(buffer.cursor_offset(), 1);
}
#[test]
fn delete_does_not_move_cursor() {
let mut buffer = EditorBuffer::new("hello");
buffer.set_cursor_offset(2);
buffer.delete();
assert_eq!(buffer.text().to_string(), "helo");
assert_eq!(buffer.cursor_offset(), 2);
}
#[test]
fn delete_removes_newline() {
let mut buffer = EditorBuffer::new("hello\nworld");
buffer.set_cursor_offset(5);
buffer.delete();
assert_eq!(buffer.text().to_string(), "helloworld");
assert_eq!(buffer.cursor_offset(), 5);
}
#[test]
fn delete_range_removes_text_and_sets_cursor() {
let mut buffer = EditorBuffer::new("hello world");
buffer.delete_range(5..11);
assert_eq!(buffer.text().to_string(), "hello");
assert_eq!(buffer.cursor_offset(), 5);
buffer.undo();
assert_eq!(buffer.text().to_string(), "hello world");
buffer.redo();
assert_eq!(buffer.text().to_string(), "hello");
}
#[test]
fn delete_range_all() {
let mut buffer = EditorBuffer::new("hello world");
buffer.delete_range(0..buffer.len_bytes());
assert_eq!(buffer.text().to_string(), "");
assert_eq!(buffer.cursor_offset(), 0);
buffer.undo();
assert_eq!(buffer.text().to_string(), "hello world");
}
#[test]
fn replace_range_works_and_is_undoable() {
let mut buffer = EditorBuffer::new("hello world");
buffer.replace_range(6..11, "there");
assert_eq!(buffer.text().to_string(), "hello there");
assert_eq!(buffer.cursor_offset(), 11);
buffer.undo();
assert_eq!(buffer.text().to_string(), "hello world");
buffer.redo();
assert_eq!(buffer.text().to_string(), "hello there");
}
#[test]
fn delete_prev_word_removes_word_and_is_undoable() {
let mut buffer = EditorBuffer::new("hello world");
buffer.set_cursor_offset(11);
assert!(buffer.delete_prev_word());
assert_eq!(buffer.text().to_string(), "hello ");
assert_eq!(buffer.cursor_offset(), 6);
buffer.undo();
assert_eq!(buffer.text().to_string(), "hello world");
assert_eq!(buffer.cursor_offset(), 11);
buffer.redo();
assert_eq!(buffer.text().to_string(), "hello ");
assert_eq!(buffer.cursor_offset(), 6);
}
#[test]
fn delete_next_word_removes_word_and_is_undoable() {
let mut buffer = EditorBuffer::new("hello world");
buffer.set_cursor_offset(0);
assert!(buffer.delete_next_word());
assert_eq!(buffer.text().to_string(), " world");
assert_eq!(buffer.cursor_offset(), 0);
buffer.undo();
assert_eq!(buffer.text().to_string(), "hello world");
assert_eq!(buffer.cursor_offset(), 0);
buffer.redo();
assert_eq!(buffer.text().to_string(), " world");
assert_eq!(buffer.cursor_offset(), 0);
}
#[test]
fn test_buffer_version_increments_on_edits() {
let mut buffer = EditorBuffer::new("initial");
assert_eq!(buffer.version(), 0);
buffer.insert(" text");
assert_eq!(buffer.version(), 1);
buffer.backspace();
assert_eq!(buffer.version(), 2);
buffer.set_cursor_offset(0);
buffer.delete();
assert_eq!(buffer.version(), 3);
buffer.undo();
assert_eq!(buffer.version(), 4);
buffer.redo();
assert_eq!(buffer.version(), 5);
}
#[test]
fn test_error_handling_validation() {
let mut buffer = EditorBuffer::new("hello\nworld");
assert!(buffer.validate_offset(0).is_ok());
assert!(buffer.validate_offset(11).is_ok());
assert!(matches!(
buffer.validate_offset(12),
Err(EditorError::OutOfBounds {
offset: 12,
len: 11
})
));
assert!(buffer.validate_range(&(0..5)).is_ok());
let (inverted_start, inverted_end) = (5, 3);
assert!(matches!(
buffer.validate_range(&(inverted_start..inverted_end)),
Err(EditorError::InvalidRange { .. })
));
assert!(matches!(
buffer.validate_range(&(0..20)),
Err(EditorError::InvalidRange { .. })
));
assert_eq!(buffer.try_line_to_string(0).unwrap(), "hello\n");
assert_eq!(buffer.try_line_to_string(1).unwrap(), "world");
assert!(matches!(
buffer.try_line_to_string(2),
Err(EditorError::InvalidRow {
row: 2,
total_lines: 2
})
));
assert!(buffer.try_replace_range(0..5, "hi").is_ok());
assert_eq!(buffer.text().to_string(), "hi\nworld");
assert!(buffer.try_delete_range(0..3).is_ok());
assert_eq!(buffer.text().to_string(), "world");
}
#[test]
fn test_file_io_roundtrip() {
let temp_dir = std::env::temp_dir();
let file_path = temp_dir.join(format!("twrite_test_{}.txt", buffer_version_rand()));
let buffer = EditorBuffer::new("Persistent story content\nLine 2");
assert!(buffer.save_to_file(&file_path).is_ok());
let loaded = EditorBuffer::from_file(&file_path);
assert!(loaded.is_ok());
let loaded = loaded.unwrap();
assert_eq!(
loaded.text().to_string(),
"Persistent story content\nLine 2"
);
let _ = std::fs::remove_file(&file_path);
}
#[test]
fn test_buffer_word_and_line_range_at() {
let buffer = EditorBuffer::new("hello world\nsecond line");
assert_eq!(buffer.word_range_at(2), 0..5);
assert_eq!(buffer.word_range_at(6), 6..11);
assert_eq!(buffer.line_range_at(3), 0..12);
assert_eq!(buffer.line_range_at(15), 12..23);
}
fn buffer_version_rand() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos() as u64
}
}