1use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
20
21use super::rope_buffer::{CursorMove, RopeBuffer};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum Operation {
26 Move {
29 to: CursorMove,
30 extend: bool,
31 },
32 SelectAll,
33 DeleteWordBack,
35 DeleteWordForward,
37 Insert(char),
38 InsertNewline,
39 DeleteBack,
41 DeleteForward,
43}
44
45pub fn operation(key: KeyEvent) -> Option<Operation> {
51 let extend = key.modifiers.contains(KeyModifiers::SHIFT);
52 let chord = key.modifiers & !KeyModifiers::SHIFT;
53
54 let motion = |to| Some(Operation::Move { to, extend });
55 match (chord, key.code) {
56 (KeyModifiers::NONE, KeyCode::Left) => motion(CursorMove::Back),
57 (KeyModifiers::NONE, KeyCode::Right) => motion(CursorMove::Forward),
58 (KeyModifiers::NONE, KeyCode::Up) => motion(CursorMove::Up),
59 (KeyModifiers::NONE, KeyCode::Down) => motion(CursorMove::Down),
60 (KeyModifiers::NONE, KeyCode::Home) => motion(CursorMove::Head),
61 (KeyModifiers::NONE, KeyCode::End) => motion(CursorMove::End),
62 (KeyModifiers::NONE, KeyCode::PageUp) => motion(CursorMove::ParagraphBack),
63 (KeyModifiers::NONE, KeyCode::PageDown) => motion(CursorMove::ParagraphForward),
64
65 (KeyModifiers::CONTROL, KeyCode::Left) => motion(CursorMove::WordBack),
66 (KeyModifiers::CONTROL, KeyCode::Right) => motion(CursorMove::WordForward),
67 (KeyModifiers::CONTROL, KeyCode::Home) => motion(CursorMove::Top),
68 (KeyModifiers::CONTROL, KeyCode::End) => motion(CursorMove::Bottom),
69
70 (KeyModifiers::ALT, KeyCode::Left) => motion(CursorMove::WordBack),
75 (KeyModifiers::ALT, KeyCode::Right) => motion(CursorMove::WordForward),
76 (KeyModifiers::ALT, KeyCode::Char('b' | 'B')) => motion(CursorMove::WordBack),
77 (KeyModifiers::ALT, KeyCode::Char('f' | 'F')) => motion(CursorMove::WordForward),
78 (KeyModifiers::SUPER, KeyCode::Left) => motion(CursorMove::Head),
79 (KeyModifiers::SUPER, KeyCode::Right) => motion(CursorMove::End),
80 (KeyModifiers::SUPER, KeyCode::Up) => motion(CursorMove::Top),
81 (KeyModifiers::SUPER, KeyCode::Down) => motion(CursorMove::Bottom),
82
83 (KeyModifiers::CONTROL, KeyCode::Char('a')) => Some(Operation::SelectAll),
84 (KeyModifiers::CONTROL, KeyCode::Backspace) | (KeyModifiers::ALT, KeyCode::Backspace) => {
85 Some(Operation::DeleteWordBack)
86 }
87 (KeyModifiers::CONTROL, KeyCode::Delete) | (KeyModifiers::ALT, KeyCode::Delete) => {
88 Some(Operation::DeleteWordForward)
89 }
90
91 (KeyModifiers::NONE, KeyCode::Char(c)) => Some(Operation::Insert(c)),
95 (KeyModifiers::NONE, KeyCode::Enter) => Some(Operation::InsertNewline),
96 (KeyModifiers::NONE, KeyCode::Backspace) => Some(Operation::DeleteBack),
102 (KeyModifiers::NONE, KeyCode::Delete) => Some(Operation::DeleteForward),
103
104 _ => None,
105 }
106}
107
108pub fn apply(op: Operation, buf: &mut RopeBuffer) -> bool {
111 match op {
112 Operation::Move { to, extend } => {
113 if extend {
117 if buf.selection_range().is_none() {
118 buf.start_selection();
119 }
120 } else {
121 buf.cancel_selection();
122 }
123 buf.move_cursor(to);
124 false
125 }
126 Operation::SelectAll => {
127 buf.select_all();
128 false
129 }
130 Operation::DeleteWordBack => buf.delete_word(),
131 Operation::DeleteWordForward => buf.delete_next_word(),
132 Operation::Insert(c) => {
133 buf.insert_char(c);
134 true
135 }
136 Operation::InsertNewline => {
137 buf.insert_newline();
138 true
139 }
140 Operation::DeleteBack => buf.delete_char(),
141 Operation::DeleteForward => buf.delete_next_char(),
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use crate::ropetext::Text;
149
150 fn key(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent {
151 KeyEvent::new(code, modifiers)
152 }
153
154 fn plain(code: KeyCode) -> Option<Operation> {
155 operation(key(code, KeyModifiers::NONE))
156 }
157
158 fn with(code: KeyCode, modifiers: KeyModifiers) -> Option<Operation> {
159 operation(key(code, modifiers))
160 }
161
162 #[test]
165 fn arrows_move_without_extending() {
166 assert_eq!(
167 plain(KeyCode::Left),
168 Some(Operation::Move {
169 to: CursorMove::Back,
170 extend: false
171 })
172 );
173 assert_eq!(
174 plain(KeyCode::Down),
175 Some(Operation::Move {
176 to: CursorMove::Down,
177 extend: false
178 })
179 );
180 }
181
182 #[test]
183 fn shift_extends_whatever_it_is_held_with() {
184 for (code, to) in [
185 (KeyCode::Left, CursorMove::Back),
186 (KeyCode::Home, CursorMove::Head),
187 (KeyCode::PageDown, CursorMove::ParagraphForward),
188 ] {
189 assert_eq!(
190 with(code, KeyModifiers::SHIFT),
191 Some(Operation::Move { to, extend: true }),
192 "{code:?}"
193 );
194 }
195 assert_eq!(
196 with(KeyCode::Left, KeyModifiers::CONTROL | KeyModifiers::SHIFT),
197 Some(Operation::Move {
198 to: CursorMove::WordBack,
199 extend: true
200 }),
201 "shift composes with a chord rather than replacing it"
202 );
203 }
204
205 #[test]
206 fn control_moves_by_word_and_to_the_ends() {
207 assert_eq!(
208 with(KeyCode::Right, KeyModifiers::CONTROL),
209 Some(Operation::Move {
210 to: CursorMove::WordForward,
211 extend: false
212 })
213 );
214 assert_eq!(
215 with(KeyCode::End, KeyModifiers::CONTROL),
216 Some(Operation::Move {
217 to: CursorMove::Bottom,
218 extend: false
219 })
220 );
221 }
222
223 #[test]
224 fn either_modifier_deletes_a_word() {
225 for modifier in [KeyModifiers::CONTROL, KeyModifiers::ALT] {
226 assert_eq!(
227 with(KeyCode::Backspace, modifier),
228 Some(Operation::DeleteWordBack)
229 );
230 assert_eq!(
231 with(KeyCode::Delete, modifier),
232 Some(Operation::DeleteWordForward)
233 );
234 }
235 }
236
237 #[test]
238 fn unmodified_keys_type() {
239 assert_eq!(plain(KeyCode::Char('x')), Some(Operation::Insert('x')));
240 assert_eq!(plain(KeyCode::Enter), Some(Operation::InsertNewline));
241 assert_eq!(plain(KeyCode::Tab), None);
244 assert_eq!(plain(KeyCode::Backspace), Some(Operation::DeleteBack));
245 assert_eq!(plain(KeyCode::Delete), Some(Operation::DeleteForward));
246 assert_eq!(
247 with(KeyCode::Char('X'), KeyModifiers::SHIFT),
248 Some(Operation::Insert('X')),
249 "a shifted character is still a character"
250 );
251 }
252
253 #[test]
254 fn the_macos_conventions_are_here_too() {
255 assert_eq!(
259 with(KeyCode::Left, KeyModifiers::ALT),
260 Some(Operation::Move {
261 to: CursorMove::WordBack,
262 extend: false
263 })
264 );
265 assert_eq!(
266 with(KeyCode::Char('f'), KeyModifiers::ALT),
267 Some(Operation::Move {
268 to: CursorMove::WordForward,
269 extend: false
270 })
271 );
272 assert_eq!(
273 with(KeyCode::Char('B'), KeyModifiers::ALT | KeyModifiers::SHIFT),
274 Some(Operation::Move {
275 to: CursorMove::WordBack,
276 extend: true
277 }),
278 "the uppercase letter arrives with SHIFT set; the modifier decides"
279 );
280 assert_eq!(
281 with(KeyCode::Up, KeyModifiers::SUPER),
282 Some(Operation::Move {
283 to: CursorMove::Top,
284 extend: false
285 })
286 );
287 }
288
289 #[test]
290 fn undo_and_redo_are_not_bound_here() {
291 assert_eq!(with(KeyCode::Char('u'), KeyModifiers::CONTROL), None);
296 assert_eq!(with(KeyCode::Char('r'), KeyModifiers::CONTROL), None);
297 }
298
299 #[test]
300 fn keys_that_mean_nothing_here_mean_nothing() {
301 assert_eq!(plain(KeyCode::F(1)), None);
302 assert_eq!(plain(KeyCode::Null), None);
303 assert_eq!(plain(KeyCode::Esc), None);
304 assert_eq!(with(KeyCode::Char('v'), KeyModifiers::CONTROL), None);
305 assert_eq!(
306 with(KeyCode::Char('q'), KeyModifiers::ALT),
307 None,
308 "an unclaimed chord must not type its character"
309 );
310 }
311
312 fn buffer(text: &str) -> RopeBuffer {
315 RopeBuffer::new(Text::from(text))
316 }
317
318 #[test]
319 fn a_move_reports_no_text_change() {
320 let mut buf = buffer("hello");
321 let changed = apply(
322 Operation::Move {
323 to: CursorMove::Forward,
324 extend: false,
325 },
326 &mut buf,
327 );
328 assert!(!changed);
329 assert_eq!(buf.cursor(), (0, 1));
330 }
331
332 #[test]
333 fn extending_grows_a_selection_and_moving_drops_it() {
334 let mut buf = buffer("hello");
335 for _ in 0..2 {
336 apply(
337 Operation::Move {
338 to: CursorMove::Forward,
339 extend: true,
340 },
341 &mut buf,
342 );
343 }
344 assert_eq!(buf.selection_range(), Some(((0, 0), (0, 2))));
345 apply(
346 Operation::Move {
347 to: CursorMove::Forward,
348 extend: false,
349 },
350 &mut buf,
351 );
352 assert!(buf.selection_range().is_none());
353 }
354
355 #[test]
356 fn a_delete_with_nothing_to_delete_reports_no_change() {
357 let mut buf = buffer("");
358 assert!(!apply(Operation::DeleteBack, &mut buf));
359 assert!(!apply(Operation::DeleteForward, &mut buf));
360 assert!(!apply(Operation::DeleteWordBack, &mut buf));
361 }
362
363 #[test]
364 fn typing_reports_a_change() {
365 let mut buf = buffer("");
366 assert!(apply(Operation::Insert('a'), &mut buf));
367 assert!(apply(Operation::InsertNewline, &mut buf));
368 assert_eq!(buf.text().to_string(), "a\n");
369 }
370}