1use ratatui::style::{Color, Modifier, Style};
2use ratatui::widgets::Block;
3use std::fmt;
4use tui_textarea::{CursorMove, Input, Key, TextArea};
5
6use crate::app::word::move_cursor_to_word_end;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum VimMode {
10 Normal,
11 Insert,
12 Visual,
13 Operator(char),
14}
15
16impl VimMode {
17 pub fn block<'a>(&self) -> Block<'a> {
18 let help = match self {
19 Self::Normal => "Esc=exit, i=insert, v=visual, y/d/c=operator",
20 Self::Insert => "Esc=normal mode",
21 Self::Visual => "Esc=normal, y=yank, d=delete, c=change",
22 Self::Operator(_) => "Move cursor to apply operator",
23 };
24 let title = format!(" {} MODE ({}) ", self, help);
25 Block::default().title(title)
26 }
27
28 pub fn cursor_style(&self) -> Style {
29 let color = match self {
30 Self::Normal => Color::Reset,
31 Self::Insert => Color::LightBlue,
32 Self::Visual => Color::LightYellow,
33 Self::Operator(_) => Color::LightGreen,
34 };
35 Style::default().fg(color).add_modifier(Modifier::REVERSED)
36 }
37}
38
39impl fmt::Display for VimMode {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
41 match self {
42 Self::Normal => write!(f, "NORMAL"),
43 Self::Insert => write!(f, "INSERT"),
44 Self::Visual => write!(f, "VISUAL"),
45 Self::Operator(c) => write!(f, "OPERATOR({})", c),
46 }
47 }
48}
49
50pub enum Transition {
52 Nop,
53 Mode(VimMode),
54 Pending(Input),
55 Exit,
56}
57
58#[derive(Clone)]
60pub struct VimState {
61 pub mode: VimMode,
62 pub pending: Input, }
64
65impl VimState {
66 pub fn new(mode: VimMode) -> Self {
67 Self {
68 mode,
69 pending: Input::default(),
70 }
71 }
72
73 pub fn with_pending(self, pending: Input) -> Self {
74 Self {
75 mode: self.mode,
76 pending,
77 }
78 }
79
80 pub fn transition(&self, input: Input, textarea: &mut TextArea<'_>) -> Transition {
81 if input.key == Key::Null {
82 return Transition::Nop;
83 }
84
85 match self.mode {
86 VimMode::Normal | VimMode::Visual | VimMode::Operator(_) => {
87 match input {
88 Input {
90 key: Key::Char('h'),
91 ..
92 } => textarea.move_cursor(CursorMove::Back),
93 Input {
94 key: Key::Char('j'),
95 ..
96 } => textarea.move_cursor(CursorMove::Down),
97 Input {
98 key: Key::Char('k'),
99 ..
100 } => textarea.move_cursor(CursorMove::Up),
101 Input {
102 key: Key::Char('l'),
103 ..
104 } => textarea.move_cursor(CursorMove::Forward),
105 Input {
106 key: Key::Char('w'),
107 ..
108 } => textarea.move_cursor(CursorMove::WordForward),
109 Input {
110 key: Key::Char('e'),
111 ctrl: false,
112 ..
113 } => {
114 let lines = textarea.lines();
116 let (row, col) = textarea.cursor();
117 let (new_row, new_col) = move_cursor_to_word_end(lines, row, col);
118
119 if row != new_row {
121 while textarea.cursor().0 < new_row {
123 textarea.move_cursor(CursorMove::Down);
124 }
125 textarea.move_cursor(CursorMove::Head);
126 while textarea.cursor().1 < new_col {
127 textarea.move_cursor(CursorMove::Forward);
128 }
129 } else {
130 if col < new_col {
132 while textarea.cursor().1 < new_col {
134 textarea.move_cursor(CursorMove::Forward);
135 }
136 } else {
137 while textarea.cursor().1 > new_col {
139 textarea.move_cursor(CursorMove::Back);
140 }
141 }
142 }
143
144 if matches!(self.mode, VimMode::Operator(_)) {
146 textarea.move_cursor(CursorMove::Forward);
147 }
148 }
149 Input {
150 key: Key::Char('b'),
151 ctrl: false,
152 ..
153 } => textarea.move_cursor(CursorMove::WordBack),
154 Input {
155 key: Key::Char('^'),
156 ..
157 } => textarea.move_cursor(CursorMove::Head),
158 Input {
159 key: Key::Char('$'),
160 ..
161 } => textarea.move_cursor(CursorMove::End),
162
163 Input {
165 key: Key::Char('D'),
166 ..
167 } => {
168 textarea.delete_line_by_end();
169 return Transition::Mode(VimMode::Normal);
170 }
171 Input {
172 key: Key::Char('C'),
173 ..
174 } => {
175 textarea.delete_line_by_end();
176 textarea.cancel_selection();
177 return Transition::Mode(VimMode::Insert);
178 }
179 Input {
180 key: Key::Char('p'),
181 ..
182 } => {
183 textarea.paste();
184 return Transition::Mode(VimMode::Normal);
185 }
186 Input {
187 key: Key::Char('u'),
188 ctrl: false,
189 ..
190 } => {
191 textarea.undo();
192 return Transition::Mode(VimMode::Normal);
193 }
194 Input {
195 key: Key::Char('r'),
196 ctrl: true,
197 ..
198 } => {
199 textarea.redo();
200 return Transition::Mode(VimMode::Normal);
201 }
202 Input {
203 key: Key::Char('x'),
204 ..
205 } => {
206 textarea.delete_next_char();
207 return Transition::Mode(VimMode::Normal);
208 }
209
210 Input {
212 key: Key::Char('i'),
213 ..
214 } => {
215 textarea.cancel_selection();
216 return Transition::Mode(VimMode::Insert);
217 }
218 Input {
219 key: Key::Char('a'),
220 ..
221 } => {
222 textarea.cancel_selection();
223 textarea.move_cursor(CursorMove::Forward);
224 return Transition::Mode(VimMode::Insert);
225 }
226 Input {
227 key: Key::Char('A'),
228 ..
229 } => {
230 textarea.cancel_selection();
231 textarea.move_cursor(CursorMove::End);
232 return Transition::Mode(VimMode::Insert);
233 }
234 Input {
235 key: Key::Char('I'),
236 ..
237 } => {
238 textarea.cancel_selection();
239 textarea.move_cursor(CursorMove::Head);
240 return Transition::Mode(VimMode::Insert);
241 }
242 Input {
243 key: Key::Char('o'),
244 ..
245 } => {
246 textarea.move_cursor(CursorMove::End);
247 textarea.insert_newline();
248 return Transition::Mode(VimMode::Insert);
249 }
250 Input {
251 key: Key::Char('O'),
252 ..
253 } => {
254 textarea.move_cursor(CursorMove::Head);
255 textarea.insert_newline();
256 textarea.move_cursor(CursorMove::Up);
257 return Transition::Mode(VimMode::Insert);
258 }
259
260 Input { key: Key::Esc, .. } => {
262 if self.mode == VimMode::Visual {
263 textarea.cancel_selection();
264 return Transition::Mode(VimMode::Normal);
265 }
266 return Transition::Exit;
267 }
268
269 Input {
271 key: Key::Char('e'),
272 ctrl: true,
273 ..
274 } => textarea.scroll((1, 0)),
275 Input {
276 key: Key::Char('y'),
277 ctrl: true,
278 ..
279 } => textarea.scroll((-1, 0)),
280
281 Input {
283 key: Key::Char('v'),
284 ctrl: false,
285 ..
286 } if self.mode == VimMode::Normal => {
287 textarea.start_selection();
288 return Transition::Mode(VimMode::Visual);
289 }
290 Input {
291 key: Key::Char('V'),
292 ctrl: false,
293 ..
294 } if self.mode == VimMode::Normal => {
295 textarea.move_cursor(CursorMove::Head);
296 textarea.start_selection();
297 textarea.move_cursor(CursorMove::End);
298 return Transition::Mode(VimMode::Visual);
299 }
300
301 Input {
303 key: Key::Char('g'),
304 ctrl: false,
305 ..
306 } if matches!(
307 self.pending,
308 Input {
309 key: Key::Char('g'),
310 ctrl: false,
311 ..
312 }
313 ) =>
314 {
315 textarea.move_cursor(CursorMove::Top)
316 }
317 Input {
318 key: Key::Char('G'),
319 ctrl: false,
320 ..
321 } => textarea.move_cursor(CursorMove::Bottom),
322 Input {
323 key: Key::Char('y'),
324 ctrl: false,
325 ..
326 } if self.mode == VimMode::Visual => {
327 textarea.move_cursor(CursorMove::Forward); textarea.copy();
329 return Transition::Mode(VimMode::Normal);
330 }
331 Input {
332 key: Key::Char('d'),
333 ctrl: false,
334 ..
335 } if self.mode == VimMode::Visual => {
336 textarea.move_cursor(CursorMove::Forward); textarea.cut();
338 return Transition::Mode(VimMode::Normal);
339 }
340 Input {
341 key: Key::Char('c'),
342 ctrl: false,
343 ..
344 } if self.mode == VimMode::Visual => {
345 textarea.move_cursor(CursorMove::Forward); textarea.cut();
347 return Transition::Mode(VimMode::Insert);
348 }
349 Input {
350 key: Key::Char(op @ ('y' | 'd' | 'c')),
351 ctrl: false,
352 ..
353 } if self.mode == VimMode::Normal => {
354 textarea.start_selection();
355 return Transition::Mode(VimMode::Operator(op));
356 }
357
358 input => return Transition::Pending(input),
359 }
360
361 match self.mode {
363 VimMode::Operator('y') => {
364 textarea.copy();
365 Transition::Mode(VimMode::Normal)
366 }
367 VimMode::Operator('d') => {
368 textarea.cut();
369 Transition::Mode(VimMode::Normal)
370 }
371 VimMode::Operator('c') => {
372 textarea.cut();
373 Transition::Mode(VimMode::Insert)
374 }
375 _ => Transition::Nop,
376 }
377 }
378 VimMode::Insert => match input {
379 Input { key: Key::Esc, .. }
380 | Input {
381 key: Key::Char('c'),
382 ctrl: true,
383 ..
384 } => Transition::Mode(VimMode::Normal),
385 input => {
386 textarea.input(input); Transition::Mode(VimMode::Insert)
388 }
389 },
390 }
391 }
392}