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 CalloutKind, ConcealedLine, DisplayPad, HighlightTag, Rgba, StyleSpan, StyleValue,
76 StyledSegment, SyntaxHighlighter, TextStyle, UnderlineDecoration, display_width,
77 split_line_intervals,
78};
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
85 fn delete_removes_character_at_cursor() {
86 let mut buffer = EditorBuffer::new("hello");
87 buffer.set_cursor_offset(1);
88
89 buffer.delete();
90
91 assert_eq!(buffer.text().to_string(), "hllo");
92 assert_eq!(buffer.cursor_offset(), 1);
93 }
94
95 #[test]
96 fn delete_at_end_does_nothing() {
97 let mut buffer = EditorBuffer::new("hello");
98 buffer.set_cursor_offset(5);
99
100 buffer.delete();
101
102 assert_eq!(buffer.text().to_string(), "hello");
103 assert_eq!(buffer.cursor_offset(), 5);
104 }
105
106 #[test]
107 fn delete_from_empty_buffer_does_nothing() {
108 let mut buffer = EditorBuffer::new("");
109
110 buffer.delete();
111
112 assert_eq!(buffer.text().to_string(), "");
113 assert_eq!(buffer.cursor_offset(), 0);
114 }
115
116 #[test]
117 fn delete_handles_unicode() {
118 let mut buffer = EditorBuffer::new("héllo");
119
120 buffer.set_cursor_offset(1);
121
122 assert_eq!(buffer.cursor_offset(), 1);
123 assert_eq!(buffer.text().to_string(), "héllo");
124
125 buffer.delete();
126
127 assert_eq!(buffer.text().to_string(), "hllo");
128 assert_eq!(buffer.cursor_offset(), 1);
129 }
130
131 #[test]
132 fn delete_records_undo_history() {
133 let mut buffer = EditorBuffer::new("hello");
134 buffer.set_cursor_offset(1);
135
136 buffer.delete();
137 buffer.undo();
138
139 assert_eq!(buffer.text().to_string(), "hello");
140 assert_eq!(buffer.cursor_offset(), 1);
141 }
142
143 #[test]
144 fn delete_can_be_redone() {
145 let mut buffer = EditorBuffer::new("hello");
146 buffer.set_cursor_offset(1);
147
148 buffer.delete();
149 buffer.undo();
150 buffer.redo();
151
152 assert_eq!(buffer.text().to_string(), "hllo");
153 assert_eq!(buffer.cursor_offset(), 1);
154 }
155
156 #[test]
157 fn delete_does_not_move_cursor() {
158 let mut buffer = EditorBuffer::new("hello");
159 buffer.set_cursor_offset(2);
160
161 buffer.delete();
162
163 assert_eq!(buffer.text().to_string(), "helo");
164 assert_eq!(buffer.cursor_offset(), 2);
165 }
166
167 #[test]
168 fn delete_removes_newline() {
169 let mut buffer = EditorBuffer::new("hello\nworld");
170 buffer.set_cursor_offset(5);
171
172 buffer.delete();
173
174 assert_eq!(buffer.text().to_string(), "helloworld");
175 assert_eq!(buffer.cursor_offset(), 5);
176 }
177
178 #[test]
179 fn delete_range_removes_text_and_sets_cursor() {
180 let mut buffer = EditorBuffer::new("hello world");
181 buffer.delete_range(5..11);
182
183 assert_eq!(buffer.text().to_string(), "hello");
184 assert_eq!(buffer.cursor_offset(), 5);
185
186 buffer.undo();
187 assert_eq!(buffer.text().to_string(), "hello world");
188
189 buffer.redo();
190 assert_eq!(buffer.text().to_string(), "hello");
191 }
192
193 #[test]
194 fn delete_range_all() {
195 let mut buffer = EditorBuffer::new("hello world");
196 buffer.delete_range(0..buffer.len_bytes());
197
198 assert_eq!(buffer.text().to_string(), "");
199 assert_eq!(buffer.cursor_offset(), 0);
200
201 buffer.undo();
202 assert_eq!(buffer.text().to_string(), "hello world");
203 }
204
205 #[test]
206 fn replace_range_works_and_is_undoable() {
207 let mut buffer = EditorBuffer::new("hello world");
208 buffer.replace_range(6..11, "there");
209
210 assert_eq!(buffer.text().to_string(), "hello there");
211 assert_eq!(buffer.cursor_offset(), 11);
212
213 buffer.undo();
214 assert_eq!(buffer.text().to_string(), "hello world");
215
216 buffer.redo();
217 assert_eq!(buffer.text().to_string(), "hello there");
218 }
219
220 #[test]
221 fn delete_prev_word_removes_word_and_is_undoable() {
222 let mut buffer = EditorBuffer::new("hello world");
223 buffer.set_cursor_offset(11);
224
225 assert!(buffer.delete_prev_word());
226 assert_eq!(buffer.text().to_string(), "hello ");
227 assert_eq!(buffer.cursor_offset(), 6);
228
229 buffer.undo();
230 assert_eq!(buffer.text().to_string(), "hello world");
231 assert_eq!(buffer.cursor_offset(), 11);
232
233 buffer.redo();
234 assert_eq!(buffer.text().to_string(), "hello ");
235 assert_eq!(buffer.cursor_offset(), 6);
236 }
237
238 #[test]
239 fn delete_next_word_removes_word_and_is_undoable() {
240 let mut buffer = EditorBuffer::new("hello world");
241 buffer.set_cursor_offset(0);
242
243 assert!(buffer.delete_next_word());
244 assert_eq!(buffer.text().to_string(), " world");
245 assert_eq!(buffer.cursor_offset(), 0);
246
247 buffer.undo();
248 assert_eq!(buffer.text().to_string(), "hello world");
249 assert_eq!(buffer.cursor_offset(), 0);
250
251 buffer.redo();
252 assert_eq!(buffer.text().to_string(), " world");
253 assert_eq!(buffer.cursor_offset(), 0);
254 }
255
256 #[test]
257 fn test_buffer_version_increments_on_edits() {
258 let mut buffer = EditorBuffer::new("initial");
259 assert_eq!(buffer.version(), 0);
260
261 buffer.insert(" text");
262 assert_eq!(buffer.version(), 1);
263
264 buffer.backspace();
265 assert_eq!(buffer.version(), 2);
266
267 buffer.set_cursor_offset(0);
268 buffer.delete();
269 assert_eq!(buffer.version(), 3);
270
271 buffer.undo();
272 assert_eq!(buffer.version(), 4);
273
274 buffer.redo();
275 assert_eq!(buffer.version(), 5);
276 }
277
278 #[test]
279 fn test_error_handling_validation() {
280 let mut buffer = EditorBuffer::new("hello\nworld");
281
282 assert!(buffer.validate_offset(0).is_ok());
283 assert!(buffer.validate_offset(11).is_ok());
284 assert!(matches!(
285 buffer.validate_offset(12),
286 Err(EditorError::OutOfBounds {
287 offset: 12,
288 len: 11
289 })
290 ));
291
292 assert!(buffer.validate_range(&(0..5)).is_ok());
293 let (inverted_start, inverted_end) = (5, 3);
294 assert!(matches!(
295 buffer.validate_range(&(inverted_start..inverted_end)),
296 Err(EditorError::InvalidRange { .. })
297 ));
298 assert!(matches!(
299 buffer.validate_range(&(0..20)),
300 Err(EditorError::InvalidRange { .. })
301 ));
302
303 assert_eq!(buffer.try_line_to_string(0).unwrap(), "hello\n");
304 assert_eq!(buffer.try_line_to_string(1).unwrap(), "world");
305 assert!(matches!(
306 buffer.try_line_to_string(2),
307 Err(EditorError::InvalidRow {
308 row: 2,
309 total_lines: 2
310 })
311 ));
312
313 assert!(buffer.try_replace_range(0..5, "hi").is_ok());
314 assert_eq!(buffer.text().to_string(), "hi\nworld");
315 assert!(buffer.try_delete_range(0..3).is_ok());
316 assert_eq!(buffer.text().to_string(), "world");
317 }
318
319 #[test]
320 fn test_file_io_roundtrip() {
321 let temp_dir = std::env::temp_dir();
322 let file_path = temp_dir.join(format!("twrite_test_{}.txt", buffer_version_rand()));
323
324 let buffer = EditorBuffer::new("Persistent story content\nLine 2");
325 assert!(buffer.save_to_file(&file_path).is_ok());
326
327 let loaded = EditorBuffer::from_file(&file_path);
328 assert!(loaded.is_ok());
329 let loaded = loaded.unwrap();
330 assert_eq!(
331 loaded.text().to_string(),
332 "Persistent story content\nLine 2"
333 );
334
335 let _ = std::fs::remove_file(&file_path);
336 }
337
338 #[test]
339 fn test_buffer_word_and_line_range_at() {
340 let buffer = EditorBuffer::new("hello world\nsecond line");
341 assert_eq!(buffer.word_range_at(2), 0..5);
342 assert_eq!(buffer.word_range_at(6), 6..11);
343 assert_eq!(buffer.line_range_at(3), 0..12);
344 assert_eq!(buffer.line_range_at(15), 12..23);
345 }
346
347 fn buffer_version_rand() -> u64 {
348 use std::time::{SystemTime, UNIX_EPOCH};
349 SystemTime::now()
350 .duration_since(UNIX_EPOCH)
351 .unwrap()
352 .as_nanos() as u64
353 }
354}