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