1pub mod batteries;
6pub mod buffer;
8pub mod context_menu;
10pub mod coordinates;
12pub mod effect;
14pub mod error;
16pub mod history;
18pub mod hook;
20pub mod keycode;
22#[cfg(feature = "markdown")]
27#[path = "batteries/markdown/mod.rs"]
28pub mod markdown;
29pub mod movement;
31pub mod prompt;
33pub mod search;
35pub mod search_hook;
37pub mod selection;
39pub mod syntax;
41
42pub use buffer::EditorBuffer;
43pub use context_menu::{
44 COPY_ID, CUT_ID, ContextMenuCaps, ContextMenuContext, ContextMenuItem, ContextMenuState,
45 DELETE_ID, KeyHint, PASTE_ID, REDO_ID, SELECT_ALL_ID, UNDO_ID, collect_context_items,
46 default_context_items,
47};
48pub use coordinates::Point;
49pub use effect::HookEffect;
50pub use error::{EditorError, Result as EditorResult};
51pub use hook::{
52 AutoPairsHook, CursorStyle, EditorHook, HookContext, HookOutcome, KeyEvent, Modifiers,
53 SearchSnapshot,
54};
55pub use keycode::KeyCode;
56#[cfg(feature = "markdown")]
57pub use markdown::{
58 ConcealMode, MarkdownConfig, MarkdownHighlighter, MarkdownHook, TABLE_CELL_TAG,
59 TABLE_DELIMITER_TAG, TABLE_HEADER_TAG, TableAlignment, TableBlock, TableLayout, TableRowKind,
60 fence_rows, find_unescaped_pipes, is_fenced_row, parse_delimiter_row, split_table_cells,
61 table_block_at, table_block_at_with_fences, table_layouts, table_layouts_with_fences,
62};
63pub use movement::{
64 CharKind, classify_char, find_line_end, find_line_range_at, find_line_start,
65 find_next_word_end, find_prev_word_start, find_word_range_at,
66};
67pub use prompt::{
68 PromptAction, PromptItem, PromptPlacement, PromptSpec, PromptState, fuzzy_filter, fuzzy_score,
69};
70pub use search::{SearchQuery, SearchState, find_matches, find_next, find_prev, replace_all_query};
71pub use search::{collect_replacements, replace_one_query};
72pub use search_hook::{REPLACE_PROMPT_ID, SEARCH_PROMPT_ID, SearchAction, SearchHook};
73pub use selection::Selection;
74pub use syntax::{
75 ConcealedLine, DisplayPad, HighlightTag, Rgba, StyleSpan, StyleValue, StyledSegment,
76 SyntaxHighlighter, TextStyle, UnderlineDecoration, display_width, split_line_intervals,
77};
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn delete_removes_character_at_cursor() {
85 let mut buffer = EditorBuffer::new("hello");
86 buffer.set_cursor_offset(1);
87
88 buffer.delete();
89
90 assert_eq!(buffer.text().to_string(), "hllo");
91 assert_eq!(buffer.cursor_offset(), 1);
92 }
93
94 #[test]
95 fn delete_at_end_does_nothing() {
96 let mut buffer = EditorBuffer::new("hello");
97 buffer.set_cursor_offset(5);
98
99 buffer.delete();
100
101 assert_eq!(buffer.text().to_string(), "hello");
102 assert_eq!(buffer.cursor_offset(), 5);
103 }
104
105 #[test]
106 fn delete_from_empty_buffer_does_nothing() {
107 let mut buffer = EditorBuffer::new("");
108
109 buffer.delete();
110
111 assert_eq!(buffer.text().to_string(), "");
112 assert_eq!(buffer.cursor_offset(), 0);
113 }
114
115 #[test]
116 fn delete_handles_unicode() {
117 let mut buffer = EditorBuffer::new("héllo");
118
119 buffer.set_cursor_offset(1);
120
121 assert_eq!(buffer.cursor_offset(), 1);
122 assert_eq!(buffer.text().to_string(), "héllo");
123
124 buffer.delete();
125
126 assert_eq!(buffer.text().to_string(), "hllo");
127 assert_eq!(buffer.cursor_offset(), 1);
128 }
129
130 #[test]
131 fn delete_records_undo_history() {
132 let mut buffer = EditorBuffer::new("hello");
133 buffer.set_cursor_offset(1);
134
135 buffer.delete();
136 buffer.undo();
137
138 assert_eq!(buffer.text().to_string(), "hello");
139 assert_eq!(buffer.cursor_offset(), 1);
140 }
141
142 #[test]
143 fn delete_can_be_redone() {
144 let mut buffer = EditorBuffer::new("hello");
145 buffer.set_cursor_offset(1);
146
147 buffer.delete();
148 buffer.undo();
149 buffer.redo();
150
151 assert_eq!(buffer.text().to_string(), "hllo");
152 assert_eq!(buffer.cursor_offset(), 1);
153 }
154
155 #[test]
156 fn delete_does_not_move_cursor() {
157 let mut buffer = EditorBuffer::new("hello");
158 buffer.set_cursor_offset(2);
159
160 buffer.delete();
161
162 assert_eq!(buffer.text().to_string(), "helo");
163 assert_eq!(buffer.cursor_offset(), 2);
164 }
165
166 #[test]
167 fn delete_removes_newline() {
168 let mut buffer = EditorBuffer::new("hello\nworld");
169 buffer.set_cursor_offset(5);
170
171 buffer.delete();
172
173 assert_eq!(buffer.text().to_string(), "helloworld");
174 assert_eq!(buffer.cursor_offset(), 5);
175 }
176
177 #[test]
178 fn delete_range_removes_text_and_sets_cursor() {
179 let mut buffer = EditorBuffer::new("hello world");
180 buffer.delete_range(5..11);
181
182 assert_eq!(buffer.text().to_string(), "hello");
183 assert_eq!(buffer.cursor_offset(), 5);
184
185 buffer.undo();
186 assert_eq!(buffer.text().to_string(), "hello world");
187
188 buffer.redo();
189 assert_eq!(buffer.text().to_string(), "hello");
190 }
191
192 #[test]
193 fn delete_range_all() {
194 let mut buffer = EditorBuffer::new("hello world");
195 buffer.delete_range(0..buffer.len_bytes());
196
197 assert_eq!(buffer.text().to_string(), "");
198 assert_eq!(buffer.cursor_offset(), 0);
199
200 buffer.undo();
201 assert_eq!(buffer.text().to_string(), "hello world");
202 }
203
204 #[test]
205 fn replace_range_works_and_is_undoable() {
206 let mut buffer = EditorBuffer::new("hello world");
207 buffer.replace_range(6..11, "there");
208
209 assert_eq!(buffer.text().to_string(), "hello there");
210 assert_eq!(buffer.cursor_offset(), 11);
211
212 buffer.undo();
213 assert_eq!(buffer.text().to_string(), "hello world");
214
215 buffer.redo();
216 assert_eq!(buffer.text().to_string(), "hello there");
217 }
218
219 #[test]
220 fn delete_prev_word_removes_word_and_is_undoable() {
221 let mut buffer = EditorBuffer::new("hello world");
222 buffer.set_cursor_offset(11);
223
224 assert!(buffer.delete_prev_word());
225 assert_eq!(buffer.text().to_string(), "hello ");
226 assert_eq!(buffer.cursor_offset(), 6);
227
228 buffer.undo();
229 assert_eq!(buffer.text().to_string(), "hello world");
230 assert_eq!(buffer.cursor_offset(), 11);
231
232 buffer.redo();
233 assert_eq!(buffer.text().to_string(), "hello ");
234 assert_eq!(buffer.cursor_offset(), 6);
235 }
236
237 #[test]
238 fn delete_next_word_removes_word_and_is_undoable() {
239 let mut buffer = EditorBuffer::new("hello world");
240 buffer.set_cursor_offset(0);
241
242 assert!(buffer.delete_next_word());
243 assert_eq!(buffer.text().to_string(), " world");
244 assert_eq!(buffer.cursor_offset(), 0);
245
246 buffer.undo();
247 assert_eq!(buffer.text().to_string(), "hello world");
248 assert_eq!(buffer.cursor_offset(), 0);
249
250 buffer.redo();
251 assert_eq!(buffer.text().to_string(), " world");
252 assert_eq!(buffer.cursor_offset(), 0);
253 }
254
255 #[test]
256 fn test_buffer_version_increments_on_edits() {
257 let mut buffer = EditorBuffer::new("initial");
258 assert_eq!(buffer.version(), 0);
259
260 buffer.insert(" text");
261 assert_eq!(buffer.version(), 1);
262
263 buffer.backspace();
264 assert_eq!(buffer.version(), 2);
265
266 buffer.set_cursor_offset(0);
267 buffer.delete();
268 assert_eq!(buffer.version(), 3);
269
270 buffer.undo();
271 assert_eq!(buffer.version(), 4);
272
273 buffer.redo();
274 assert_eq!(buffer.version(), 5);
275 }
276
277 #[test]
278 fn test_error_handling_validation() {
279 let mut buffer = EditorBuffer::new("hello\nworld");
280
281 assert!(buffer.validate_offset(0).is_ok());
282 assert!(buffer.validate_offset(11).is_ok());
283 assert!(matches!(
284 buffer.validate_offset(12),
285 Err(EditorError::OutOfBounds {
286 offset: 12,
287 len: 11
288 })
289 ));
290
291 assert!(buffer.validate_range(&(0..5)).is_ok());
292 let (inverted_start, inverted_end) = (5, 3);
293 assert!(matches!(
294 buffer.validate_range(&(inverted_start..inverted_end)),
295 Err(EditorError::InvalidRange { .. })
296 ));
297 assert!(matches!(
298 buffer.validate_range(&(0..20)),
299 Err(EditorError::InvalidRange { .. })
300 ));
301
302 assert_eq!(buffer.try_line_to_string(0).unwrap(), "hello\n");
303 assert_eq!(buffer.try_line_to_string(1).unwrap(), "world");
304 assert!(matches!(
305 buffer.try_line_to_string(2),
306 Err(EditorError::InvalidRow {
307 row: 2,
308 total_lines: 2
309 })
310 ));
311
312 assert!(buffer.try_replace_range(0..5, "hi").is_ok());
313 assert_eq!(buffer.text().to_string(), "hi\nworld");
314 assert!(buffer.try_delete_range(0..3).is_ok());
315 assert_eq!(buffer.text().to_string(), "world");
316 }
317
318 #[test]
319 fn test_file_io_roundtrip() {
320 let temp_dir = std::env::temp_dir();
321 let file_path = temp_dir.join(format!("twrite_test_{}.txt", buffer_version_rand()));
322
323 let buffer = EditorBuffer::new("Persistent story content\nLine 2");
324 assert!(buffer.save_to_file(&file_path).is_ok());
325
326 let loaded = EditorBuffer::from_file(&file_path);
327 assert!(loaded.is_ok());
328 let loaded = loaded.unwrap();
329 assert_eq!(
330 loaded.text().to_string(),
331 "Persistent story content\nLine 2"
332 );
333
334 let _ = std::fs::remove_file(&file_path);
335 }
336
337 #[test]
338 fn test_buffer_word_and_line_range_at() {
339 let buffer = EditorBuffer::new("hello world\nsecond line");
340 assert_eq!(buffer.word_range_at(2), 0..5);
341 assert_eq!(buffer.word_range_at(6), 6..11);
342 assert_eq!(buffer.line_range_at(3), 0..12);
343 assert_eq!(buffer.line_range_at(15), 12..23);
344 }
345
346 fn buffer_version_rand() -> u64 {
347 use std::time::{SystemTime, UNIX_EPOCH};
348 SystemTime::now()
349 .duration_since(UNIX_EPOCH)
350 .unwrap()
351 .as_nanos() as u64
352 }
353}