1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum EditMode {
8 Emacs,
9 Vim,
10}
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum VimMode {
15 Normal,
16 Insert,
17}
18
19#[derive(Debug, Clone)]
23pub struct Editor {
24 buffer: String,
25 cursor: usize,
26 mode: EditMode,
27 vim: VimMode,
28}
29
30impl Editor {
31 #[must_use]
32 pub fn new(mode: EditMode) -> Self {
33 Self {
34 buffer: String::new(),
35 cursor: 0,
36 mode,
37 vim: VimMode::Insert,
38 }
39 }
40
41 #[must_use]
42 pub fn with_buffer(mode: EditMode, buffer: impl Into<String>) -> Self {
43 let buf: String = buffer.into();
44 let cursor = buf.chars().count();
45 Self {
46 buffer: buf,
47 cursor,
48 mode,
49 vim: VimMode::Insert,
50 }
51 }
52
53 #[must_use]
54 pub fn buffer(&self) -> &str {
55 &self.buffer
56 }
57
58 #[must_use]
59 pub fn cursor(&self) -> usize {
60 self.cursor
61 }
62
63 #[must_use]
64 pub fn mode(&self) -> EditMode {
65 self.mode
66 }
67
68 #[must_use]
69 pub fn vim_mode(&self) -> VimMode {
70 self.vim
71 }
72
73 pub fn set_mode(&mut self, mode: EditMode) {
74 self.mode = mode;
75 self.vim = VimMode::Insert;
76 }
77
78 fn chars(&self) -> Vec<char> {
79 self.buffer.chars().collect()
80 }
81
82 fn rebuild(&mut self, chars: &[char]) {
83 self.buffer = chars.iter().collect();
84 if self.cursor > chars.len() {
85 self.cursor = chars.len();
86 }
87 }
88
89 pub fn insert_char(&mut self, c: char) {
92 if self.mode == EditMode::Vim && self.vim == VimMode::Normal {
93 return;
94 }
95 let mut chars = self.chars();
96 chars.insert(self.cursor, c);
97 self.cursor += 1;
98 self.rebuild(&chars);
99 }
100
101 pub fn delete_backward(&mut self) {
102 if self.cursor == 0 {
103 return;
104 }
105 let mut chars = self.chars();
106 chars.remove(self.cursor - 1);
107 self.cursor -= 1;
108 self.rebuild(&chars);
109 }
110
111 pub fn delete_forward(&mut self) {
112 let mut chars = self.chars();
113 if self.cursor < chars.len() {
114 chars.remove(self.cursor);
115 self.rebuild(&chars);
116 }
117 }
118
119 pub fn move_left(&mut self) {
120 if self.cursor > 0 {
121 self.cursor -= 1;
122 }
123 }
124
125 pub fn move_right(&mut self) {
126 let len = self.chars().len();
127 if self.cursor < len {
128 self.cursor += 1;
129 }
130 }
131
132 pub fn move_home(&mut self) {
133 self.cursor = 0;
134 }
135
136 pub fn move_end(&mut self) {
137 self.cursor = self.chars().len();
138 }
139
140 pub fn kill_to_end(&mut self) {
142 let mut chars = self.chars();
143 chars.truncate(self.cursor);
144 self.rebuild(&chars);
145 }
146
147 pub fn kill_word_backward(&mut self) {
149 let mut chars = self.chars();
150 let mut i = self.cursor;
151 while i > 0 && chars[i - 1].is_whitespace() {
152 i -= 1;
153 }
154 while i > 0 && !chars[i - 1].is_whitespace() {
155 i -= 1;
156 }
157 chars.drain(i..self.cursor);
158 self.cursor = i;
159 self.rebuild(&chars);
160 }
161
162 pub fn enter_insert_mode(&mut self) {
163 self.vim = VimMode::Insert;
164 }
165
166 pub fn enter_normal_mode(&mut self) {
167 self.vim = VimMode::Normal;
168 }
169
170 pub fn vim_action(&mut self, action: VimAction) {
175 match action {
176 VimAction::MoveLeft => self.move_left(),
177 VimAction::MoveRight => self.move_right(),
178 VimAction::MoveHome => self.move_home(),
179 VimAction::MoveEnd => self.move_end(),
180 VimAction::WordForward => self.vim_word_forward(),
181 VimAction::WordBackward => self.vim_word_backward(),
182 VimAction::DeleteChar => self.delete_forward(),
183 VimAction::DeleteWordForward => self.vim_delete_word_forward(),
184 VimAction::DeleteWordBackward => self.kill_word_backward(),
185 VimAction::InsertAtCursor => self.enter_insert_mode(),
186 VimAction::InsertAfterCursor => {
187 self.move_right();
188 self.enter_insert_mode();
189 }
190 VimAction::InsertAtLineStart => {
191 self.move_home();
192 self.enter_insert_mode();
193 }
194 VimAction::InsertAtLineEnd => {
195 self.move_end();
196 self.enter_insert_mode();
197 }
198 }
199 }
200
201 fn vim_word_forward(&mut self) {
202 let chars = self.chars();
203 let mut i = self.cursor;
204 while i < chars.len() && !chars[i].is_whitespace() {
205 i += 1;
206 }
207 while i < chars.len() && chars[i].is_whitespace() {
208 i += 1;
209 }
210 self.cursor = i;
211 }
212
213 fn vim_word_backward(&mut self) {
214 let chars = self.chars();
215 let mut i = self.cursor;
216 while i > 0 && chars[i - 1].is_whitespace() {
217 i -= 1;
218 }
219 while i > 0 && !chars[i - 1].is_whitespace() {
220 i -= 1;
221 }
222 self.cursor = i;
223 }
224
225 fn vim_delete_word_forward(&mut self) {
226 let mut chars = self.chars();
227 let start = self.cursor;
228 let mut i = start;
229 while i < chars.len() && !chars[i].is_whitespace() {
230 i += 1;
231 }
232 while i < chars.len() && chars[i].is_whitespace() {
233 i += 1;
234 }
235 chars.drain(start..i);
236 self.rebuild(&chars);
237 }
238}
239
240#[derive(Debug, Clone, Copy, PartialEq, Eq)]
242pub enum VimAction {
243 MoveLeft,
244 MoveRight,
245 MoveHome,
246 MoveEnd,
247 WordForward,
248 WordBackward,
249 DeleteChar,
250 DeleteWordForward,
251 DeleteWordBackward,
252 InsertAtCursor,
253 InsertAfterCursor,
254 InsertAtLineStart,
255 InsertAtLineEnd,
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
263 fn emacs_insert_appends_at_cursor() {
264 let mut e = Editor::new(EditMode::Emacs);
265 e.insert_char('a');
266 e.insert_char('b');
267 e.insert_char('c');
268 assert_eq!(e.buffer(), "abc");
269 assert_eq!(e.cursor(), 3);
270 }
271
272 #[test]
273 fn emacs_kill_to_end_truncates() {
274 let mut e = Editor::with_buffer(EditMode::Emacs, "hello world");
275 e.move_home();
276 for _ in 0..5 {
277 e.move_right();
278 }
279 e.kill_to_end();
280 assert_eq!(e.buffer(), "hello");
281 assert_eq!(e.cursor(), 5);
282 }
283
284 #[test]
285 fn emacs_kill_word_backward_drops_word_and_spaces() {
286 let mut e = Editor::with_buffer(EditMode::Emacs, "hello world");
287 e.move_end();
288 e.kill_word_backward();
289 assert_eq!(e.buffer(), "hello ");
290 }
291
292 #[test]
293 fn delete_backward_at_start_is_noop() {
294 let mut e = Editor::new(EditMode::Emacs);
295 e.delete_backward();
296 assert_eq!(e.buffer(), "");
297 assert_eq!(e.cursor(), 0);
298 }
299
300 #[test]
301 fn move_right_saturates_at_end() {
302 let mut e = Editor::with_buffer(EditMode::Emacs, "ab");
303 e.move_right();
304 e.move_right();
305 e.move_right();
306 assert_eq!(e.cursor(), 2);
307 }
308
309 #[test]
310 fn vim_normal_mode_ignores_insert_char() {
311 let mut e = Editor::new(EditMode::Vim);
312 e.enter_normal_mode();
313 e.insert_char('a');
314 assert_eq!(e.buffer(), "");
315 }
316
317 #[test]
318 fn vim_i_enters_insert_mode() {
319 let mut e = Editor::new(EditMode::Vim);
320 e.enter_normal_mode();
321 e.vim_action(VimAction::InsertAtCursor);
322 assert_eq!(e.vim_mode(), VimMode::Insert);
323 e.insert_char('x');
324 assert_eq!(e.buffer(), "x");
325 }
326
327 #[test]
328 fn vim_word_forward_skips_to_next_token() {
329 let mut e = Editor::with_buffer(EditMode::Vim, "hello world foo");
330 e.enter_normal_mode();
331 e.move_home();
332 e.vim_action(VimAction::WordForward);
333 assert_eq!(e.cursor(), 6);
334 e.vim_action(VimAction::WordForward);
335 assert_eq!(e.cursor(), 12);
336 }
337
338 #[test]
339 fn vim_word_backward_reverses_word_forward() {
340 let mut e = Editor::with_buffer(EditMode::Vim, "hello world foo");
341 e.enter_normal_mode();
342 e.move_end();
343 e.vim_action(VimAction::WordBackward);
344 assert_eq!(e.cursor(), 12);
345 e.vim_action(VimAction::WordBackward);
346 assert_eq!(e.cursor(), 6);
347 }
348
349 #[test]
350 fn vim_dw_deletes_word_forward() {
351 let mut e = Editor::with_buffer(EditMode::Vim, "hello world foo");
352 e.enter_normal_mode();
353 e.move_home();
354 e.vim_action(VimAction::DeleteWordForward);
355 assert_eq!(e.buffer(), "world foo");
356 }
357
358 #[test]
359 fn vim_capital_a_appends_at_eol_in_insert_mode() {
360 let mut e = Editor::with_buffer(EditMode::Vim, "ab");
361 e.enter_normal_mode();
362 e.vim_action(VimAction::InsertAtLineEnd);
363 assert_eq!(e.cursor(), 2);
364 assert_eq!(e.vim_mode(), VimMode::Insert);
365 e.insert_char('c');
366 assert_eq!(e.buffer(), "abc");
367 }
368
369 #[test]
370 fn switching_mode_resets_to_insert() {
371 let mut e = Editor::new(EditMode::Vim);
372 e.enter_normal_mode();
373 e.set_mode(EditMode::Emacs);
374 assert_eq!(e.vim_mode(), VimMode::Insert);
375 }
376}