Krolyxon
Tag v0.3.0

Branches

Tags

Refs →

bench51cratesbench51-guisrcvimmod.rs

1818 lines · 60.3 KB · Rust

1//! Vim keybindings for the code editor: modes, motions with counts,
2//! operators and text objects, registers, undo, `.` repeat, macros, search
3//! and `:` commands. The engine edits a `String` through a char-index
4//! cursor and knows nothing about egui, so it is tested on plain text.
5
6mod ex;
7mod keys;
8#[cfg(test)]
9mod tests;
10mod text;
11
12use std::collections::HashMap;
13
14pub use keys::{VimKey, from_egui};
15use text::{Buf, Kind, auto_indent, is_blank, is_keyword};
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub enum Mode {
19 Normal,
20 Insert,
21 Visual,
22 VisualLine,
23 /// Typing a `:` command or a `/` `?` search.
24 Command,
25}
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub enum Align {
29 Top,
30 Center,
31 Bottom,
32}
33
34/// What the editor or the app has to do after a key.
35#[derive(Clone, Debug, PartialEq, Eq)]
36pub enum Effect {
37 /// Yanked or deleted text, mirrored to the system clipboard.
38 Clipboard(String),
39 Save,
40 SaveAs(String),
41 Close {
42 force: bool,
43 },
44 Quit {
45 force: bool,
46 },
47 Edit(String),
48 Make,
49 NextDoc,
50 PrevDoc,
51 /// Scroll so the cursor line is at the top, centre or bottom.
52 Scroll(Align),
53 /// Scroll the view by this many lines (Ctrl-D/U/F/B).
54 ScrollLines(isize),
55}
56
57/// The lines currently visible, for `H M L` and paging.
58#[derive(Clone, Copy, Debug)]
59pub struct View {
60 pub top: usize,
61 pub rows: usize,
62}
63
64impl Default for View {
65 fn default() -> Self {
66 Self { top: 0, rows: 30 }
67 }
68}
69
70/// Undo and redo snapshots of one document.
71#[derive(Default)]
72pub struct History {
73 undo: Vec<(String, usize)>,
74 redo: Vec<(String, usize)>,
75}
76
77const HISTORY_LIMIT: usize = 1000;
78
79/// The document a key acts on.
80pub struct Cx<'a> {
81 pub text: &'a mut String,
82 /// Char index into `text`.
83 pub cursor: &'a mut usize,
84 pub history: &'a mut History,
85 pub readonly: bool,
86 /// Unsaved changes, for `:q`.
87 pub dirty: bool,
88 pub view: View,
89}
90
91#[derive(Clone, Debug)]
92struct Reg {
93 text: String,
94 linewise: bool,
95}
96
97/// The last search. It stays highlighted until `:noh`.
98#[derive(Clone, Debug)]
99pub struct Search {
100 pub pattern: String,
101 forward: bool,
102 whole_word: bool,
103}
104
105#[derive(Clone, Copy, Debug, PartialEq, Eq)]
106enum Op {
107 Delete,
108 Change,
109 Yank,
110 Indent,
111 Dedent,
112 Case,
113}
114
115#[derive(Clone, Copy, Debug, PartialEq, Eq)]
116enum Motion {
117 Left,
118 Right,
119 Up,
120 Down,
121 /// `+` / Enter and `-`: first non-blank of the next or previous line.
122 NextLine,
123 PrevLine,
124 WordFwd(bool),
125 WordBack(bool),
126 WordEnd(bool),
127 LineStart,
128 FirstNonBlank,
129 LineEnd,
130 FileStart,
131 FileEnd,
132 Find(char, char),
133 RepeatFind(bool),
134 MatchPair,
135 ParaFwd,
136 ParaBack,
137 ScreenTop,
138 ScreenMid,
139 ScreenBottom,
140 HalfDown,
141 HalfUp,
142 PageDown,
143 PageUp,
144 SearchNext(bool),
145 SearchWord(bool),
146}
147
148#[derive(Clone, Copy, Debug, PartialEq, Eq)]
149enum Target {
150 Motion(Motion),
151 /// Text object: its character and whether it's an "a" object.
152 Obj(char, bool),
153 /// `dd`, `yy`, `cc`, `>>`: whole lines.
154 Line,
155}
156
157#[derive(Clone, Debug, PartialEq, Eq)]
158enum Cmd {
159 Move(Motion, Option<usize>),
160 Op {
161 op: Op,
162 reg: Option<char>,
163 count: Option<usize>,
164 target: Target,
165 },
166 VisualOp(Op, Option<char>),
167 /// `viw`, `vi(`…: select a text object.
168 VisualObj(char, bool),
169 VisualJoin,
170 VisualPaste(Option<char>),
171 SwapAnchor,
172 /// `d/pattern`, `c?pattern`: an operator over a search.
173 OpSearch {
174 op: Op,
175 reg: Option<char>,
176 forward: bool,
177 },
178 Insert(char, usize),
179 Paste {
180 before: bool,
181 reg: Option<char>,
182 count: usize,
183 },
184 Undo(usize),
185 Redo(usize),
186 Repeat(usize),
187 Join(usize),
188 ToggleCase(usize),
189 Replace(char, usize),
190 Visual(bool),
191 Prompt(char),
192 MacroRecord(char),
193 MacroStop,
194 MacroPlay(Option<char>, usize),
195 Scroll(Align),
196 GotoDef,
197 NextTab,
198 PrevTab,
199}
200
201impl Cmd {
202 /// Changes the text (so it's refused when read-only and undoable).
203 fn modifies(&self) -> bool {
204 match self {
205 Cmd::Op { op, .. } | Cmd::VisualOp(op, _) => *op != Op::Yank,
206 Cmd::VisualJoin
207 | Cmd::VisualPaste(_)
208 | Cmd::Insert(..)
209 | Cmd::Paste { .. }
210 | Cmd::Undo(_)
211 | Cmd::Redo(_)
212 | Cmd::Repeat(_)
213 | Cmd::Join(_)
214 | Cmd::ToggleCase(_)
215 | Cmd::Replace(..) => true,
216 _ => false,
217 }
218 }
219
220 /// Recorded for `.`.
221 fn repeatable(&self) -> bool {
222 self.modifies() && !matches!(self, Cmd::Undo(_) | Cmd::Redo(_) | Cmd::Repeat(_))
223 }
224
225 fn enters_insert(&self) -> bool {
226 matches!(self, Cmd::Insert(..) | Cmd::Op { op: Op::Change, .. })
227 }
228}
229
230enum Parse {
231 More,
232 Bad,
233 Done(Cmd),
234}
235
236/// A charwise range is `[start, end)`; a linewise one covers whole lines,
237/// including the last line's `\n` when there is one.
238#[derive(Clone, Copy, Debug)]
239struct Range {
240 start: usize,
241 end: usize,
242 linewise: bool,
243}
244
245fn lines_range(buf: &Buf, l0: usize, l1: usize) -> Range {
246 let start = buf.start_of_line(l0);
247 let end = if l1 + 1 < buf.lines() {
248 buf.start_of_line(l1 + 1)
249 } else {
250 buf.len()
251 };
252 Range {
253 start,
254 end,
255 linewise: true,
256 }
257}
258
259fn is_register(c: char) -> bool {
260 c.is_ascii_alphabetic() || c == '"' || c == '_'
261}
262
263fn count_at(keys: &[VimKey], mut i: usize) -> (Option<usize>, usize) {
264 let mut n: Option<usize> = None;
265 while let Some(VimKey::Char(c)) = keys.get(i) {
266 let Some(d) = c.to_digit(10) else { break };
267 if d == 0 && n.is_none() {
268 break; // `0` is a motion
269 }
270 n = Some((n.unwrap_or(0) * 10 + d as usize).min(99_999));
271 i += 1;
272 }
273 (n, i)
274}
275
276/// A motion starting at `keys[i]`, and the index after it.
277fn parse_motion(keys: &[VimKey], i: usize) -> Result<Option<(Motion, usize)>, ()> {
278 use Motion::*;
279 let Some(k) = keys.get(i) else {
280 return Ok(None);
281 };
282 let single = |m| Ok(Some((m, i + 1)));
283 match k {
284 VimKey::Left | VimKey::Backspace => single(Left),
285 VimKey::Right => single(Right),
286 VimKey::Up => single(Up),
287 VimKey::Down => single(Down),
288 VimKey::Enter => single(NextLine),
289 VimKey::Home => single(LineStart),
290 VimKey::End => single(LineEnd),
291 VimKey::PageUp => single(PageUp),
292 VimKey::PageDown => single(PageDown),
293 VimKey::Ctrl('d') => single(HalfDown),
294 VimKey::Ctrl('u') => single(HalfUp),
295 VimKey::Ctrl('f') => single(PageDown),
296 VimKey::Ctrl('b') => single(PageUp),
297 VimKey::Ctrl('n') | VimKey::Ctrl('j') => single(Down),
298 VimKey::Ctrl('p') => single(Up),
299 VimKey::Char(c) => match c {
300 'h' => single(Left),
301 'l' | ' ' => single(Right),
302 'j' => single(Down),
303 'k' => single(Up),
304 '+' => single(NextLine),
305 '-' => single(PrevLine),
306 'w' => single(WordFwd(false)),
307 'W' => single(WordFwd(true)),
308 'b' => single(WordBack(false)),
309 'B' => single(WordBack(true)),
310 'e' => single(WordEnd(false)),
311 'E' => single(WordEnd(true)),
312 '0' => single(LineStart),
313 '^' => single(FirstNonBlank),
314 '$' => single(LineEnd),
315 'G' => single(FileEnd),
316 '%' => single(MatchPair),
317 '}' => single(ParaFwd),
318 '{' => single(ParaBack),
319 'H' => single(ScreenTop),
320 'M' => single(ScreenMid),
321 'L' => single(ScreenBottom),
322 'n' => single(SearchNext(false)),
323 'N' => single(SearchNext(true)),
324 '*' => single(SearchWord(true)),
325 '#' => single(SearchWord(false)),
326 ';' => single(RepeatFind(false)),
327 ',' => single(RepeatFind(true)),
328 'g' => match keys.get(i + 1) {
329 None => Ok(None),
330 Some(VimKey::Char('g')) => Ok(Some((FileStart, i + 2))),
331 _ => Err(()),
332 },
333 'f' | 'F' | 't' | 'T' => match keys.get(i + 1) {
334 None => Ok(None),
335 Some(VimKey::Char(ch)) => Ok(Some((Find(*c, *ch), i + 2))),
336 _ => Err(()),
337 },
338 _ => Err(()),
339 },
340 _ => Err(()),
341 }
342}
343
344fn parse(keys: &[VimKey], visual: bool, recording: bool) -> Parse {
345 use VimKey::Char;
346 let mut i = 0;
347 let mut reg = None;
348 if keys.first() == Some(&Char('"')) {
349 match keys.get(1) {
350 None => return Parse::More,
351 Some(Char(c)) if is_register(*c) => {
352 reg = Some(*c);
353 i = 2;
354 }
355 _ => return Parse::Bad,
356 }
357 }
358 let (count, j) = count_at(keys, i);
359 i = j;
360 let Some(k) = keys.get(i) else {
361 return Parse::More;
362 };
363 let n = count.unwrap_or(1);
364 let op = |op, target| {
365 Parse::Done(Cmd::Op {
366 op,
367 reg,
368 count,
369 target,
370 })
371 };
372 let next_char = |f: &dyn Fn(char) -> Cmd| match keys.get(i + 1) {
373 None => Parse::More,
374 Some(Char(c)) => Parse::Done(f(*c)),
375 _ => Parse::Bad,
376 };
377
378 if visual {
379 let done = |cmd| Parse::Done(cmd);
380 match k {
381 Char('d' | 'x' | 'X' | 'D') | VimKey::Delete => {
382 return done(Cmd::VisualOp(Op::Delete, reg));
383 }
384 Char('c' | 's' | 'C' | 'S' | 'R') => return done(Cmd::VisualOp(Op::Change, reg)),
385 Char('y' | 'Y') => return done(Cmd::VisualOp(Op::Yank, reg)),
386 Char('>') => return done(Cmd::VisualOp(Op::Indent, reg)),
387 Char('<') => return done(Cmd::VisualOp(Op::Dedent, reg)),
388 Char('~' | 'u' | 'U') => return done(Cmd::VisualOp(Op::Case, reg)),
389 Char('J') => return done(Cmd::VisualJoin),
390 Char('p' | 'P') => return done(Cmd::VisualPaste(reg)),
391 Char('o' | 'O') => return done(Cmd::SwapAnchor),
392 Char('v') => return done(Cmd::Visual(false)),
393 Char('V') => return done(Cmd::Visual(true)),
394 Char(':') => return done(Cmd::Prompt(':')),
395 Char(ia @ ('i' | 'a')) => {
396 return match keys.get(i + 1) {
397 None => Parse::More,
398 Some(Char(obj)) => done(Cmd::VisualObj(*obj, *ia == 'a')),
399 _ => Parse::Bad,
400 };
401 }
402 _ => {}
403 }
404 }
405
406 match k {
407 Char(o @ ('d' | 'c' | 'y' | '>' | '<')) => {
408 let o = *o;
409 let kind = match o {
410 'd' => Op::Delete,
411 'c' => Op::Change,
412 'y' => Op::Yank,
413 '>' => Op::Indent,
414 _ => Op::Dedent,
415 };
416 let (count2, j) = count_at(keys, i + 1);
417 let count = match (count, count2) {
418 (None, None) => None,
419 (a, b) => Some(a.unwrap_or(1) * b.unwrap_or(1)),
420 };
421 let op = |target| {
422 Parse::Done(Cmd::Op {
423 op: kind,
424 reg,
425 count,
426 target,
427 })
428 };
429 match keys.get(j) {
430 None => Parse::More,
431 Some(Char(c)) if *c == o => op(Target::Line),
432 Some(Char(p @ ('/' | '?'))) => Parse::Done(Cmd::OpSearch {
433 op: kind,
434 reg,
435 forward: *p == '/',
436 }),
437 Some(Char(ia @ ('i' | 'a'))) => match keys.get(j + 1) {
438 None => Parse::More,
439 Some(Char(obj)) if "wWsp\"'`()b[]{}B<>".contains(*obj) => {
440 op(Target::Obj(*obj, *ia == 'a'))
441 }
442 _ => Parse::Bad,
443 },
444 _ => match parse_motion(keys, j) {
445 Ok(None) => Parse::More,
446 Err(()) => Parse::Bad,
447 Ok(Some((m, _))) => op(Target::Motion(m)),
448 },
449 }
450 }
451 Char('x') | VimKey::Delete => op(Op::Delete, Target::Motion(Motion::Right)),
452 Char('X') => op(Op::Delete, Target::Motion(Motion::Left)),
453 Char('D') => op(Op::Delete, Target::Motion(Motion::LineEnd)),
454 Char('C') => op(Op::Change, Target::Motion(Motion::LineEnd)),
455 Char('s') => op(Op::Change, Target::Motion(Motion::Right)),
456 Char('S') => op(Op::Change, Target::Line),
457 Char('Y') => op(Op::Yank, Target::Line),
458 Char(c @ ('i' | 'a' | 'I' | 'A' | 'o' | 'O')) => Parse::Done(Cmd::Insert(*c, n)),
459 Char(c @ ('p' | 'P')) => Parse::Done(Cmd::Paste {
460 before: *c == 'P',
461 reg,
462 count: n,
463 }),
464 Char('u') => Parse::Done(Cmd::Undo(n)),
465 VimKey::Ctrl('r') => Parse::Done(Cmd::Redo(n)),
466 Char('.') => Parse::Done(Cmd::Repeat(n)),
467 Char('J') => Parse::Done(Cmd::Join(n)),
468 Char('~') => Parse::Done(Cmd::ToggleCase(n)),
469 Char('r') => next_char(&|c| Cmd::Replace(c, n)),
470 Char('v') => Parse::Done(Cmd::Visual(false)),
471 Char('V') => Parse::Done(Cmd::Visual(true)),
472 Char(c @ (':' | '/' | '?')) => Parse::Done(Cmd::Prompt(*c)),
473 Char('q') if recording => Parse::Done(Cmd::MacroStop),
474 Char('q') => match keys.get(i + 1) {
475 None => Parse::More,
476 Some(Char(c)) if c.is_ascii_lowercase() => Parse::Done(Cmd::MacroRecord(*c)),
477 _ => Parse::Bad,
478 },
479 Char('@') => match keys.get(i + 1) {
480 None => Parse::More,
481 Some(Char('@')) => Parse::Done(Cmd::MacroPlay(None, n)),
482 Some(Char(c)) if c.is_ascii_lowercase() => Parse::Done(Cmd::MacroPlay(Some(*c), n)),
483 _ => Parse::Bad,
484 },
485 Char('z') => match keys.get(i + 1) {
486 None => Parse::More,
487 Some(Char('z' | '.')) => Parse::Done(Cmd::Scroll(Align::Center)),
488 Some(Char('t') | VimKey::Enter) => Parse::Done(Cmd::Scroll(Align::Top)),
489 Some(Char('b' | '-')) => Parse::Done(Cmd::Scroll(Align::Bottom)),
490 _ => Parse::Bad,
491 },
492 Char('g') => match keys.get(i + 1) {
493 None => Parse::More,
494 Some(Char('d')) => Parse::Done(Cmd::GotoDef),
495 Some(Char('t')) => Parse::Done(Cmd::NextTab),
496 Some(Char('T')) => Parse::Done(Cmd::PrevTab),
497 Some(Char('g')) => Parse::Done(Cmd::Move(Motion::FileStart, count)),
498 _ => Parse::Bad,
499 },
500 _ => match parse_motion(keys, i) {
501 Ok(None) => Parse::More,
502 Err(()) => Parse::Bad,
503 Ok(Some((m, _))) => Parse::Done(Cmd::Move(m, count)),
504 },
505 }
506}
507
508pub struct Vim {
509 pub mode: Mode,
510 pending: Vec<VimKey>,
511 anchor: usize,
512 /// Column `j`/`k` aim for; `usize::MAX` after `$`.
513 want_col: Option<usize>,
514 registers: HashMap<char, Reg>,
515 last_find: Option<(char, char)>,
516 pub search: Option<Search>,
517 pub highlight: bool,
518 /// The `:`, `/` or `?` prompt and what's been typed after it.
519 pub cmdline: Option<(char, String)>,
520 /// Status message, and whether it's an error.
521 pub message: Option<(String, bool)>,
522 /// Text and cursor before the change in progress, for undo.
523 change_start: Option<(String, usize)>,
524 /// Operator waiting for the `/` or `?` search that ends its range.
525 search_op: Option<(Op, Option<char>)>,
526 dot_rec: Option<Vec<VimKey>>,
527 last_change: Vec<VimKey>,
528 dot_replay: bool,
529 macro_rec: Option<(char, Vec<VimKey>)>,
530 macros: HashMap<char, Vec<VimKey>>,
531 last_macro: Option<char>,
532 macro_depth: u8,
533 /// Count and command of the insert in progress (`3ix`, `2o`).
534 insert_repeat: (usize, char),
535 insert_keys: Vec<VimKey>,
536}
537
538impl Default for Vim {
539 fn default() -> Self {
540 Self {
541 mode: Mode::Normal,
542 pending: Vec::new(),
543 anchor: 0,
544 want_col: None,
545 registers: HashMap::new(),
546 last_find: None,
547 search: None,
548 highlight: false,
549 cmdline: None,
550 message: None,
551 change_start: None,
552 search_op: None,
553 dot_rec: None,
554 last_change: Vec::new(),
555 dot_replay: false,
556 macro_rec: None,
557 macros: HashMap::new(),
558 last_macro: None,
559 macro_depth: 0,
560 insert_repeat: (1, 'i'),
561 insert_keys: Vec::new(),
562 }
563 }
564}
565
566impl Vim {
567 pub fn mode_label(&self) -> &'static str {
568 match self.mode {
569 Mode::Normal => "normal",
570 Mode::Insert => "insert",
571 Mode::Visual => "visual",
572 Mode::VisualLine => "v-line",
573 Mode::Command => "command",
574 }
575 }
576
577 /// Keys of a command still being typed, e.g. `2d`.
578 pub fn pending_label(&self) -> String {
579 self.pending.iter().map(VimKey::label).collect()
580 }
581
582 pub fn recording(&self) -> Option<char> {
583 self.macro_rec.as_ref().map(|r| r.0)
584 }
585
586 /// Normal, Visual and command-line modes show a block cursor.
587 pub fn block_cursor(&self) -> bool {
588 self.mode != Mode::Insert
589 }
590
591 /// Back to Normal mode without finishing anything, e.g. when another
592 /// document becomes active.
593 pub fn abandon(&mut self) {
594 self.mode = Mode::Normal;
595 self.pending.clear();
596 self.cmdline = None;
597 self.change_start = None;
598 self.search_op = None;
599 self.dot_rec = None;
600 self.want_col = None;
601 }
602
603 /// The mouse put the cursor at `head`, selecting from `tail` if they
604 /// differ (egui's `[min, max)`; Vim's selection includes the cursor).
605 pub fn mouse(&mut self, tail: usize, head: usize, text: &str, cursor: &mut usize) {
606 self.pending.clear();
607 if self.mode == Mode::Command {
608 self.cmdline = None;
609 self.search_op = None;
610 self.mode = Mode::Normal;
611 }
612 let buf = Buf::new(text);
613 if tail != head && self.mode != Mode::Insert {
614 let (a, b) = (tail.min(head), tail.max(head) - 1);
615 if head > tail {
616 self.anchor = a;
617 *cursor = b;
618 } else {
619 self.anchor = b;
620 *cursor = a;
621 }
622 if self.mode != Mode::VisualLine {
623 self.mode = Mode::Visual;
624 }
625 } else {
626 if matches!(self.mode, Mode::Visual | Mode::VisualLine) {
627 self.mode = Mode::Normal;
628 }
629 *cursor = if self.mode == Mode::Insert {
630 head.min(buf.len())
631 } else {
632 buf.clamp_normal(head)
633 };
634 }
635 }
636
637 /// The Visual selection as a char range `[start, end)`.
638 pub fn selection(&self, text: &str, cursor: usize) -> Option<(usize, usize)> {
639 let (a, b) = (self.anchor.min(cursor), self.anchor.max(cursor));
640 let len = text.chars().count();
641 match self.mode {
642 Mode::Visual => Some((a.min(len), (b + 1).min(len))),
643 Mode::VisualLine => {
644 let buf = Buf::new(text);
645 Some((buf.line_start(a), buf.line_end(b)))
646 }
647 _ => None,
648 }
649 }
650
651 /// Byte ranges of the highlighted search's matches in `text`.
652 pub fn search_ranges(&self, text: &str) -> Vec<(usize, usize)> {
653 let Some(s) = self.search.as_ref().filter(|_| self.highlight) else {
654 return Vec::new();
655 };
656 let buf = Buf::new(text);
657 let plen = s.pattern.chars().count();
658 if plen == 0 {
659 return Vec::new();
660 }
661 let bytes: Vec<usize> = text
662 .char_indices()
663 .map(|(b, _)| b)
664 .chain([text.len()])
665 .collect();
666 let mut out = Vec::new();
667 let mut i = 0;
668 while i + plen <= buf.len() && out.len() < 2000 {
669 if matches_at(&buf, i, s) {
670 out.push((bytes[i], bytes[i + plen]));
671 i += plen;
672 } else {
673 i += 1;
674 }
675 }
676 out
677 }
678
679 /// Handles one key typed by the user.
680 pub fn key(&mut self, key: VimKey, cx: &mut Cx) -> Vec<Effect> {
681 let mut fx = Vec::new();
682 if self.mode != Mode::Command {
683 self.message = None;
684 }
685 if let Some((_, keys)) = &mut self.macro_rec {
686 keys.push(key.clone());
687 }
688 self.handle(key, cx, &mut fx);
689 fx
690 }
691
692 fn handle(&mut self, key: VimKey, cx: &mut Cx, fx: &mut Vec<Effect>) {
693 match self.mode {
694 Mode::Insert => self.insert_key(key, cx),
695 Mode::Command => self.cmdline_key(key, cx, fx),
696 _ => self.normal_key(key, cx, fx),
697 }
698 if matches!(self.mode, Mode::Normal | Mode::Visual | Mode::VisualLine) {
699 let buf = Buf::new(cx.text);
700 *cx.cursor = buf.clamp_normal(*cx.cursor);
701 }
702 }
703
704 fn error(&mut self, msg: impl Into<String>) {
705 self.message = Some((msg.into(), true));
706 }
707
708 // ---- Normal and Visual mode ----
709
710 fn normal_key(&mut self, key: VimKey, cx: &mut Cx, fx: &mut Vec<Effect>) {
711 let visual = matches!(self.mode, Mode::Visual | Mode::VisualLine);
712 match &key {
713 VimKey::Esc | VimKey::Ctrl('c') => {
714 self.pending.clear();
715 if visual {
716 self.mode = Mode::Normal;
717 }
718 return;
719 }
720 VimKey::Copy => {
721 self.pending.clear();
722 if visual {
723 self.exec(Cmd::VisualOp(Op::Yank, None), cx, fx);
724 }
725 return;
726 }
727 VimKey::Cut => {
728 self.pending.clear();
729 if visual {
730 self.exec(Cmd::VisualOp(Op::Delete, None), cx, fx);
731 }
732 return;
733 }
734 VimKey::Paste(t) => {
735 self.pending.clear();
736 let t = t.clone();
737 self.paste_text(&t, visual, cx);
738 return;
739 }
740 _ => {}
741 }
742 self.pending.push(key);
743 match parse(&self.pending, visual, self.macro_rec.is_some()) {
744 Parse::More => {}
745 Parse::Bad => self.pending.clear(),
746 Parse::Done(cmd) => {
747 let keys = std::mem::take(&mut self.pending);
748 if !visual && !self.dot_replay && cmd.repeatable() && !cx.readonly {
749 if cmd.enters_insert() {
750 self.dot_rec = Some(keys);
751 } else {
752 self.last_change = keys;
753 }
754 }
755 self.exec(cmd, cx, fx);
756 }
757 }
758 }
759
760 fn begin_change(&mut self, cx: &Cx) {
761 if self.change_start.is_none() {
762 self.change_start = Some((cx.text.clone(), *cx.cursor));
763 }
764 }
765
766 fn end_change(&mut self, cx: &mut Cx) {
767 if let Some((text, cursor)) = self.change_start.take()
768 && text != *cx.text
769 {
770 cx.history.undo.push((text, cursor));
771 if cx.history.undo.len() > HISTORY_LIMIT {
772 cx.history.undo.remove(0);
773 }
774 cx.history.redo.clear();
775 }
776 }
777
778 fn exec(&mut self, cmd: Cmd, cx: &mut Cx, fx: &mut Vec<Effect>) {
779 if cmd.modifies() && cx.readonly {
780 self.error("read-only while debugging");
781 if matches!(self.mode, Mode::Visual | Mode::VisualLine) {
782 self.mode = Mode::Normal;
783 }
784 return;
785 }
786 let tracked =
787 cmd.modifies() && !matches!(cmd, Cmd::Undo(_) | Cmd::Redo(_) | Cmd::Repeat(_));
788 if tracked {
789 self.begin_change(cx);
790 }
791 let mut buf = Buf::new(cx.text);
792 let pos = (*cx.cursor).min(buf.len());
793 match cmd {
794 Cmd::Move(m, count) => {
795 if let Some((p, _)) = self.motion(m, count, &buf, pos, false, cx.view, fx) {
796 *cx.cursor = p;
797 }
798 }
799 Cmd::Op {
800 op,
801 reg,
802 count,
803 target,
804 } => {
805 if let Some(r) = self.op_range(op, count, target, &buf, pos, cx.view, fx) {
806 self.apply(op, reg, r, &mut buf, cx, fx);
807 }
808 }
809 Cmd::VisualOp(op, reg) => {
810 let r = self.visual_range(&buf, pos, op);
811 self.mode = Mode::Normal;
812 self.apply(op, reg, r, &mut buf, cx, fx);
813 }
814 Cmd::VisualJoin => {
815 let (a, b) = (self.anchor.min(pos), self.anchor.max(pos));
816 let lines = buf.line_of(b) - buf.line_of(a) + 1;
817 self.mode = Mode::Normal;
818 *cx.cursor = a;
819 self.join(lines.max(2), &mut buf, cx);
820 }
821 Cmd::VisualPaste(reg) => {
822 if let Some(r) = self.get_reg(reg) {
823 let range = self.visual_range(&buf, pos, Op::Delete);
824 self.mode = Mode::Normal;
825 let mut t = r.text.clone();
826 if range.linewise {
827 // Whole lines in, whole lines out.
828 if !t.ends_with('\n') {
829 t.push('\n');
830 }
831 if !buf.c[range.start..range.end].ends_with(&['\n']) {
832 t.pop();
833 }
834 } else if r.linewise {
835 // Lines into part of a line go on lines of their own.
836 t.insert(0, '\n');
837 }
838 buf.remove(range.start, range.end);
839 buf.insert(range.start, &t);
840 *cx.cursor = range.start + usize::from(!range.linewise && r.linewise);
841 *cx.text = buf.text();
842 }
843 }
844 Cmd::VisualObj(c, around) => {
845 if let Some((a, b, linewise)) = buf.object(pos, c, around) {
846 self.anchor = a;
847 if linewise {
848 self.mode = Mode::VisualLine;
849 *cx.cursor = b;
850 } else {
851 *cx.cursor = b.saturating_sub(1).max(a);
852 }
853 }
854 }
855 Cmd::OpSearch { op, reg, forward } => {
856 if op != Op::Yank && cx.readonly {
857 self.error("read-only while debugging");
858 } else {
859 self.search_op = Some((op, reg));
860 self.mode = Mode::Command;
861 self.cmdline = Some((if forward { '/' } else { '?' }, String::new()));
862 }
863 }
864 Cmd::SwapAnchor => {
865 std::mem::swap(&mut self.anchor, cx.cursor);
866 }
867 Cmd::Insert(kind, count) => self.start_insert(kind, count, &mut buf, pos, cx),
868 Cmd::Paste { before, reg, count } => {
869 if let Some(r) = self.get_reg(reg) {
870 self.put(&r, before, count, &mut buf, pos, cx);
871 }
872 }
873 Cmd::Undo(n) => self.undo(n, cx, true),
874 Cmd::Redo(n) => self.undo(n, cx, false),
875 Cmd::Repeat(n) => {
876 let keys = self.last_change.clone();
877 if !keys.is_empty() {
878 self.dot_replay = true;
879 for _ in 0..n {
880 for k in &keys {
881 self.handle(k.clone(), cx, fx);
882 }
883 }
884 self.dot_replay = false;
885 }
886 }
887 Cmd::Join(n) => self.join(n.max(2), &mut buf, cx),
888 Cmd::ToggleCase(n) => {
889 let e = buf.line_end(pos);
890 let end = (pos + n).min(e);
891 for i in pos..end {
892 buf.c[i] = toggle_case(buf.c[i]);
893 }
894 *cx.text = buf.text();
895 *cx.cursor = end;
896 }
897 Cmd::Replace(c, n) => {
898 let e = buf.line_end(pos);
899 if pos + n <= e {
900 for i in pos..pos + n {
901 buf.c[i] = c;
902 }
903 *cx.text = buf.text();
904 *cx.cursor = pos + n - 1;
905 }
906 }
907 Cmd::Visual(line) => {
908 let want = if line { Mode::VisualLine } else { Mode::Visual };
909 self.mode = match self.mode {
910 m if m == want => Mode::Normal,
911 Mode::Normal => {
912 self.anchor = pos;
913 want
914 }
915 _ => want,
916 };
917 }
918 Cmd::Prompt(p) => {
919 self.mode = Mode::Command;
920 self.cmdline = Some((p, String::new()));
921 }
922 Cmd::MacroRecord(c) => self.macro_rec = Some((c, Vec::new())),
923 Cmd::MacroStop => {
924 if let Some((c, mut keys)) = self.macro_rec.take() {
925 keys.pop(); // the `q` that stopped it
926 self.macros.insert(c, keys);
927 }
928 }
929 Cmd::MacroPlay(reg, n) => {
930 let Some(r) = reg.or(self.last_macro) else {
931 return;
932 };
933 self.last_macro = Some(r);
934 let Some(keys) = self.macros.get(&r).cloned() else {
935 self.error(format!("register {r} is empty"));
936 return;
937 };
938 if self.macro_depth >= 8 {
939 return;
940 }
941 self.macro_depth += 1;
942 for _ in 0..n {
943 for k in &keys {
944 self.handle(k.clone(), cx, fx);
945 }
946 }
947 self.macro_depth -= 1;
948 }
949 Cmd::Scroll(a) => fx.push(Effect::Scroll(a)),
950 Cmd::GotoDef => match self.find_definition(&buf, pos) {
951 Some(p) => *cx.cursor = p,
952 None => self.error("definition not found"),
953 },
954 Cmd::NextTab => fx.push(Effect::NextDoc),
955 Cmd::PrevTab => fx.push(Effect::PrevDoc),
956 }
957 if tracked && self.mode != Mode::Insert {
958 self.end_change(cx);
959 }
960 }
961
962 // ---- Motions ----
963
964 #[allow(clippy::too_many_arguments)]
965 fn motion(
966 &mut self,
967 m: Motion,
968 count: Option<usize>,
969 buf: &Buf,
970 pos: usize,
971 for_op: bool,
972 view: View,
973 fx: &mut Vec<Effect>,
974 ) -> Option<(usize, Kind)> {
975 use Kind::*;
976 use Motion::*;
977 let n = count.unwrap_or(1).max(1);
978 let line = buf.line_of(pos);
979 let last = buf.lines() - 1;
980 let fnb = |l: usize| buf.first_non_blank(buf.start_of_line(l.min(last)));
981 let r = match m {
982 Left => (pos.saturating_sub(n).max(buf.line_start(pos)), Exclusive),
983 Right => {
984 let s = buf.line_start(pos);
985 let e = buf.line_end(pos);
986 let max = if for_op {
987 e
988 } else {
989 e.saturating_sub(1).max(s)
990 };
991 ((pos + n).min(max), Exclusive)
992 }
993 Up | Down => {
994 if (m == Up && line == 0) || (m == Down && line == last) {
995 return None;
996 }
997 let tl = if m == Up {
998 line.saturating_sub(n)
999 } else {
1000 (line + n).min(last)
1001 };
1002 let col = self.want_col.unwrap_or(buf.col(pos));
1003 self.want_col = Some(col);
1004 let target = if col == usize::MAX {
1005 buf.clamp_normal(buf.line_end(buf.start_of_line(tl)))
1006 } else {
1007 buf.at_col(tl, col)
1008 };
1009 return Some((target, Linewise));
1010 }
1011 NextLine => {
1012 if line == last {
1013 return None;
1014 }
1015 (fnb(line + n), Linewise)
1016 }
1017 PrevLine => {
1018 if line == 0 {
1019 return None;
1020 }
1021 (fnb(line.saturating_sub(n)), Linewise)
1022 }
1023 WordFwd(big) => ((0..n).fold(pos, |p, _| buf.word_fwd(p, big)), Exclusive),
1024 WordBack(big) => ((0..n).fold(pos, |p, _| buf.word_back(p, big)), Exclusive),
1025 WordEnd(big) => ((0..n).fold(pos, |p, _| buf.word_end(p, big)), Inclusive),
1026 LineStart => (buf.line_start(pos), Exclusive),
1027 FirstNonBlank => (buf.first_non_blank(pos), Exclusive),
1028 LineEnd => {
1029 let s = buf.start_of_line((line + n - 1).min(last));
1030 let e = buf.line_end(s);
1031 self.want_col = Some(usize::MAX);
1032 if e == s {
1033 return Some((s, Exclusive));
1034 }
1035 return Some((e - 1, Inclusive));
1036 }
1037 FileStart => (fnb(count.map_or(0, |c| c - 1)), Linewise),
1038 FileEnd => (fnb(count.map_or(last, |c| c - 1)), Linewise),
1039 Find(kind, ch) => {
1040 self.last_find = Some((kind, ch));
1041 (buf.find_char(pos, kind, ch, n, false)?, find_kind(kind))
1042 }
1043 RepeatFind(rev) => {
1044 let (kind, ch) = self.last_find?;
1045 let kind = if rev {
1046 match kind {
1047 'f' => 'F',
1048 'F' => 'f',
1049 't' => 'T',
1050 _ => 't',
1051 }
1052 } else {
1053 kind
1054 };
1055 (buf.find_char(pos, kind, ch, n, true)?, find_kind(kind))
1056 }
1057 MatchPair => (buf.match_pair(pos)?, Inclusive),
1058 ParaFwd => ((0..n).fold(pos, |p, _| buf.paragraph(p, true)), Exclusive),
1059 ParaBack => ((0..n).fold(pos, |p, _| buf.paragraph(p, false)), Exclusive),
1060 ScreenTop | ScreenMid | ScreenBottom => {
1061 let top = view.top.min(last);
1062 let bottom = (view.top + view.rows.max(1) - 1).min(last);
1063 let l = match m {
1064 ScreenTop => (top + n - 1).min(bottom),
1065 ScreenMid => (top + bottom) / 2,
1066 _ => bottom.saturating_sub(n - 1).max(top),
1067 };
1068 (fnb(l), Linewise)
1069 }
1070 HalfDown | HalfUp | PageDown | PageUp => {
1071 let step = n * match m {
1072 HalfDown | HalfUp => (view.rows / 2).max(1),
1073 _ => view.rows.saturating_sub(2).max(1),
1074 };
1075 let down = matches!(m, HalfDown | PageDown);
1076 let tl = if down {
1077 (line + step).min(last)
1078 } else {
1079 line.saturating_sub(step)
1080 };
1081 if !for_op {
1082 let delta = step as isize;
1083 fx.push(Effect::ScrollLines(if down { delta } else { -delta }));
1084 }
1085 (fnb(tl), Linewise)
1086 }
1087 SearchNext(rev) => {
1088 let s = self.search.clone()?;
1089 self.highlight = true;
1090 let fwd = s.forward != rev;
1091 let mut p = pos;
1092 for _ in 0..n {
1093 p = self.find_next(buf, p, fwd, &s)?;
1094 }
1095 (p, Exclusive)
1096 }
1097 SearchWord(fwd) => {
1098 let word = word_at(buf, pos)?;
1099 let s = Search {
1100 pattern: word,
1101 forward: fwd,
1102 whole_word: true,
1103 };
1104 self.search = Some(s.clone());
1105 self.highlight = true;
1106 (self.find_next(buf, pos, fwd, &s)?, Exclusive)
1107 }
1108 };
1109 if !for_op || !matches!(m, Up | Down) {
1110 self.want_col = None;
1111 }
1112 Some(r)
1113 }
1114
1115 fn find_next(&mut self, buf: &Buf, from: usize, fwd: bool, s: &Search) -> Option<usize> {
1116 let n = buf.len();
1117 if n == 0 {
1118 return None;
1119 }
1120 let order: Box<dyn Iterator<Item = usize>> = if fwd {
1121 Box::new((from + 1..n).chain(0..=from.min(n - 1)))
1122 } else {
1123 Box::new((0..from).rev().chain((from..n).rev()))
1124 };
1125 for i in order {
1126 if matches_at(buf, i, s) {
1127 if (fwd && i <= from) || (!fwd && i >= from) {
1128 self.message = Some((
1129 if fwd {
1130 "search hit BOTTOM, continuing at TOP"
1131 } else {
1132 "search hit TOP, continuing at BOTTOM"
1133 }
1134 .into(),
1135 false,
1136 ));
1137 }
1138 return Some(i);
1139 }
1140 }
1141 self.error(format!("E486: Pattern not found: {}", s.pattern));
1142 None
1143 }
1144
1145 fn find_definition(&self, buf: &Buf, pos: usize) -> Option<usize> {
1146 let word = word_at(buf, pos)?.to_ascii_uppercase();
1147 (0..buf.lines()).find_map(|l| {
1148 let s = buf.start_of_line(l);
1149 let line = buf.line_text(s);
1150 let t = line.trim_start();
1151 let rest = t.get(word.len()..)?;
1152 if !t[..word.len()].eq_ignore_ascii_case(&word) {
1153 return None;
1154 }
1155 let rest_up = rest.to_ascii_uppercase();
1156 let defines = rest.starts_with(':')
1157 || ["EQU", "SET", "BIT", "DATA", "IDATA", "XDATA", "CODE"]
1158 .iter()
1159 .any(|d| {
1160 rest_up.trim_start().starts_with(d) && rest.starts_with(char::is_whitespace)
1161 });
1162 defines.then(|| buf.first_non_blank(s))
1163 })
1164 }
1165
1166 // ---- Operators ----
1167
1168 #[allow(clippy::too_many_arguments)]
1169 fn op_range(
1170 &mut self,
1171 op: Op,
1172 count: Option<usize>,
1173 target: Target,
1174 buf: &Buf,
1175 pos: usize,
1176 view: View,
1177 fx: &mut Vec<Effect>,
1178 ) -> Option<Range> {
1179 let line = buf.line_of(pos);
1180 match target {
1181 Target::Line => {
1182 let l1 = (line + count.unwrap_or(1) - 1).min(buf.lines() - 1);
1183 Some(lines_range(buf, line, l1))
1184 }
1185 Target::Obj(c, around) => {
1186 let (a, b, linewise) = buf.object(pos, c, around)?;
1187 Some(if linewise {
1188 lines_range(buf, buf.line_of(a), buf.line_of(b))
1189 } else {
1190 Range {
1191 start: a,
1192 end: b,
1193 linewise: false,
1194 }
1195 })
1196 }
1197 Target::Motion(m) => {
1198 // `cw` changes to the end of the word, like `ce`.
1199 if op == Op::Change
1200 && let Motion::WordFwd(big) = m
1201 && pos < buf.len()
1202 && !buf.c[pos].is_whitespace()
1203 {
1204 let mut end = buf.run_end(pos, big);
1205 for _ in 1..count.unwrap_or(1) {
1206 end = buf.word_end(end, big);
1207 }
1208 return Some(Range {
1209 start: pos,
1210 end: end + 1,
1211 linewise: false,
1212 });
1213 }
1214 let (to, kind) = self.motion(m, count, buf, pos, true, view, fx)?;
1215 let (a, b) = (pos.min(to), pos.max(to));
1216 Some(match kind {
1217 Kind::Linewise => lines_range(buf, buf.line_of(a), buf.line_of(b)),
1218 Kind::Inclusive => Range {
1219 start: a,
1220 end: (b + 1).min(buf.len()),
1221 linewise: false,
1222 },
1223 Kind::Exclusive => {
1224 // An exclusive motion that ends at the start of a
1225 // later line stops at the end of the line before
1226 // (`dw` on a line's last word), or covers whole
1227 // lines if it started at the indentation (`d}`).
1228 if b > a && buf.col(b) == 0 && buf.line_of(b) > buf.line_of(a) {
1229 if buf.col(a) <= buf.col(buf.first_non_blank(a)) {
1230 return Some(lines_range(buf, buf.line_of(a), buf.line_of(b) - 1));
1231 }
1232 return Some(Range {
1233 start: a,
1234 end: b - 1,
1235 linewise: false,
1236 });
1237 }
1238 Range {
1239 start: a,
1240 end: b,
1241 linewise: false,
1242 }
1243 }
1244 })
1245 }
1246 }
1247 }
1248
1249 fn visual_range(&self, buf: &Buf, pos: usize, op: Op) -> Range {
1250 let (a, b) = (self.anchor.min(pos), self.anchor.max(pos));
1251 if self.mode == Mode::VisualLine || matches!(op, Op::Indent | Op::Dedent) {
1252 lines_range(buf, buf.line_of(a), buf.line_of(b))
1253 } else {
1254 Range {
1255 start: a,
1256 end: (b + 1).min(buf.len()),
1257 linewise: false,
1258 }
1259 }
1260 }
1261
1262 fn apply(
1263 &mut self,
1264 op: Op,
1265 reg: Option<char>,
1266 r: Range,
1267 buf: &mut Buf,
1268 cx: &mut Cx,
1269 fx: &mut Vec<Effect>,
1270 ) {
1271 let pos = *cx.cursor;
1272 let mut text: String = buf.c[r.start..r.end].iter().collect();
1273 if r.linewise && !text.ends_with('\n') {
1274 text.push('\n');
1275 }
1276 match op {
1277 Op::Yank => {
1278 self.set_reg(reg, text, r.linewise, fx);
1279 *cx.cursor = if r.linewise {
1280 if r.start < buf.line_start(pos) {
1281 buf.at_col(buf.line_of(r.start), buf.col(pos))
1282 } else {
1283 pos
1284 }
1285 } else {
1286 r.start.min(pos)
1287 };
1288 }
1289 Op::Delete => {
1290 self.set_reg(reg, text, r.linewise, fx);
1291 let mut start = r.start;
1292 // Deleting the last lines also takes the `\n` before them.
1293 if r.linewise
1294 && r.end == buf.len()
1295 && start > 0
1296 && !buf.c[r.start..r.end].ends_with(&['\n'])
1297 {
1298 start -= 1;
1299 }
1300 buf.remove(start, r.end);
1301 *cx.text = buf.text();
1302 *cx.cursor = if r.linewise {
1303 buf.first_non_blank(start.min(buf.len()))
1304 } else {
1305 start
1306 };
1307 }
1308 Op::Change => {
1309 self.set_reg(reg, text, r.linewise, fx);
1310 if r.linewise {
1311 let indent: String = buf.c[r.start..r.end]
1312 .iter()
1313 .take_while(|&&c| is_blank(c))
1314 .collect();
1315 let end = if buf.c[r.start..r.end].ends_with(&['\n']) {
1316 r.end - 1
1317 } else {
1318 r.end
1319 };
1320 buf.remove(r.start, end);
1321 let n = buf.insert(r.start, &indent);
1322 *cx.cursor = r.start + n;
1323 } else {
1324 buf.remove(r.start, r.end);
1325 *cx.cursor = r.start;
1326 }
1327 *cx.text = buf.text();
1328 self.mode = Mode::Insert;
1329 self.insert_repeat = (1, 'c');
1330 self.insert_keys.clear();
1331 }
1332 Op::Indent | Op::Dedent => {
1333 let (l0, l1) = (
1334 buf.line_of(r.start),
1335 buf.line_of(r.end.saturating_sub(1).max(r.start)),
1336 );
1337 for l in l0..=l1 {
1338 let s = buf.start_of_line(l);
1339 let e = buf.line_end(s);
1340 if op == Op::Indent {
1341 if e > s {
1342 buf.insert(s, " ");
1343 }
1344 } else {
1345 let n = if buf.c.get(s) == Some(&'\t') {
1346 1
1347 } else {
1348 buf.c[s..e]
1349 .iter()
1350 .take(8)
1351 .take_while(|&&c| c == ' ')
1352 .count()
1353 };
1354 buf.remove(s, s + n);
1355 }
1356 }
1357 *cx.text = buf.text();
1358 *cx.cursor = buf.first_non_blank(buf.start_of_line(l0));
1359 }
1360 Op::Case => {
1361 for i in r.start..r.end {
1362 buf.c[i] = toggle_case(buf.c[i]);
1363 }
1364 *cx.text = buf.text();
1365 *cx.cursor = r.start;
1366 }
1367 }
1368 }
1369
1370 fn set_reg(&mut self, reg: Option<char>, text: String, linewise: bool, fx: &mut Vec<Effect>) {
1371 match reg {
1372 Some('_') => return,
1373 Some(c) if c.is_ascii_lowercase() => {
1374 self.registers.insert(
1375 c,
1376 Reg {
1377 text: text.clone(),
1378 linewise,
1379 },
1380 );
1381 }
1382 Some(c) if c.is_ascii_uppercase() => {
1383 let r = self.registers.entry(c.to_ascii_lowercase()).or_insert(Reg {
1384 text: String::new(),
1385 linewise,
1386 });
1387 r.text.push_str(&text);
1388 }
1389 _ => {}
1390 }
1391 fx.push(Effect::Clipboard(text.clone()));
1392 self.registers.insert('"', Reg { text, linewise });
1393 }
1394
1395 fn get_reg(&mut self, reg: Option<char>) -> Option<Reg> {
1396 let c = reg.unwrap_or('"').to_ascii_lowercase();
1397 let r = self.registers.get(&c).cloned();
1398 if r.is_none() {
1399 self.error(format!("E353: Nothing in register {c}"));
1400 }
1401 r
1402 }
1403
1404 fn put(&mut self, r: &Reg, before: bool, count: usize, buf: &mut Buf, pos: usize, cx: &mut Cx) {
1405 if r.linewise {
1406 let mut t = r.text.repeat(count);
1407 if !t.ends_with('\n') {
1408 t.push('\n');
1409 }
1410 let at = if before {
1411 buf.line_start(pos)
1412 } else {
1413 let e = buf.line_end(pos);
1414 if e == buf.len() {
1415 // After the last line: the newline goes in front.
1416 t.pop();
1417 t.insert(0, '\n');
1418 e
1419 } else {
1420 e + 1
1421 }
1422 };
1423 buf.insert(at, &t);
1424 let first = if !before && at == buf.line_end(pos) && buf.c.get(at) == Some(&'\n') {
1425 at + 1
1426 } else {
1427 at
1428 };
1429 *cx.text = buf.text();
1430 *cx.cursor = buf.first_non_blank(first);
1431 } else {
1432 let t = r.text.repeat(count);
1433 let at = if before || buf.line_end(pos) == pos {
1434 pos
1435 } else {
1436 pos + 1
1437 };
1438 let n = buf.insert(at, &t);
1439 *cx.text = buf.text();
1440 *cx.cursor = at + n.saturating_sub(1);
1441 }
1442 }
1443
1444 /// Ctrl+V: the system clipboard, before the cursor or over the selection.
1445 fn paste_text(&mut self, t: &str, visual: bool, cx: &mut Cx) {
1446 if cx.readonly {
1447 self.error("read-only while debugging");
1448 return;
1449 }
1450 let r = Reg {
1451 text: t.to_string(),
1452 linewise: t.ends_with('\n'),
1453 };
1454 self.begin_change(cx);
1455 let mut buf = Buf::new(cx.text);
1456 let pos = (*cx.cursor).min(buf.len());
1457 if visual {
1458 let range = self.visual_range(&buf, pos, Op::Delete);
1459 self.mode = Mode::Normal;
1460 buf.remove(range.start, range.end);
1461 let n = buf.insert(range.start, t);
1462 *cx.text = buf.text();
1463 *cx.cursor = range.start + n.saturating_sub(1);
1464 } else {
1465 self.put(&r, true, 1, &mut buf, pos, cx);
1466 }
1467 self.end_change(cx);
1468 }
1469
1470 fn join(&mut self, lines: usize, buf: &mut Buf, cx: &mut Cx) {
1471 let mut pos = *cx.cursor;
1472 for _ in 1..lines {
1473 let e = buf.line_end(pos);
1474 if e >= buf.len() {
1475 break;
1476 }
1477 let mut f = e + 1;
1478 while f < buf.len() && is_blank(buf.c[f]) {
1479 f += 1;
1480 }
1481 buf.remove(e, f);
1482 let line_empty = e == buf.line_start(e);
1483 let ends_blank = e > 0 && is_blank(buf.c[e - 1]);
1484 let next_empty = e >= buf.len() || buf.c[e] == '\n' || buf.c[e] == ')';
1485 if !line_empty && !ends_blank && !next_empty {
1486 buf.insert(e, " ");
1487 }
1488 pos = e;
1489 }
1490 *cx.text = buf.text();
1491 *cx.cursor = pos;
1492 }
1493
1494 fn undo(&mut self, n: usize, cx: &mut Cx, undo: bool) {
1495 for _ in 0..n {
1496 let (from, to) = if undo {
1497 (&mut cx.history.undo, &mut cx.history.redo)
1498 } else {
1499 (&mut cx.history.redo, &mut cx.history.undo)
1500 };
1501 let Some((text, cursor)) = from.pop() else {
1502 self.error(if undo {
1503 "Already at oldest change"
1504 } else {
1505 "Already at newest change"
1506 });
1507 return;
1508 };
1509 to.push((std::mem::replace(cx.text, text), *cx.cursor));
1510 *cx.cursor = cursor;
1511 }
1512 }
1513
1514 // ---- Insert mode ----
1515
1516 fn start_insert(&mut self, kind: char, count: usize, buf: &mut Buf, pos: usize, cx: &mut Cx) {
1517 let s = buf.line_start(pos);
1518 let e = buf.line_end(pos);
1519 *cx.cursor = match kind {
1520 'a' if e > pos => pos + 1,
1521 'I' => (s..e).find(|&i| !is_blank(buf.c[i])).unwrap_or(e),
1522 'A' => e,
1523 'o' | 'O' => {
1524 let indent = auto_indent(&buf.line_text(pos));
1525 let at = if kind == 'o' { e } else { s };
1526 let line = if kind == 'o' {
1527 format!("\n{indent}")
1528 } else {
1529 format!("{indent}\n")
1530 };
1531 buf.insert(at, &line);
1532 *cx.text = buf.text();
1533 at + indent.chars().count() + usize::from(kind == 'o')
1534 }
1535 _ => pos,
1536 };
1537 self.mode = Mode::Insert;
1538 self.insert_repeat = (count, kind);
1539 self.insert_keys.clear();
1540 }
1541
1542 fn insert_key(&mut self, key: VimKey, cx: &mut Cx) {
1543 if let Some(rec) = &mut self.dot_rec
1544 && !self.dot_replay
1545 {
1546 rec.push(key.clone());
1547 }
1548 if cx.readonly || matches!(key, VimKey::Esc | VimKey::Copy | VimKey::Ctrl('c')) {
1549 self.leave_insert(cx);
1550 return;
1551 }
1552 if matches!(
1553 key,
1554 VimKey::Char(_) | VimKey::Enter | VimKey::Tab | VimKey::Backspace | VimKey::Paste(_)
1555 ) {
1556 self.insert_keys.push(key.clone());
1557 }
1558 self.type_key(&key, cx);
1559 }
1560
1561 /// One key's effect on the text in Insert mode.
1562 fn type_key(&mut self, key: &VimKey, cx: &mut Cx) {
1563 let mut buf = Buf::new(cx.text);
1564 let pos = (*cx.cursor).min(buf.len());
1565 let s = buf.line_start(pos);
1566 let mut changed = true;
1567 match key {
1568 VimKey::Char(c) => *cx.cursor = pos + buf.insert(pos, &c.to_string()),
1569 VimKey::Paste(t) => *cx.cursor = pos + buf.insert(pos, t),
1570 VimKey::Enter => {
1571 let indent = auto_indent(&buf.c[s..pos].iter().collect::<String>());
1572 *cx.cursor = pos + buf.insert(pos, &format!("\n{indent}"));
1573 }
1574 VimKey::Tab => {
1575 let n = 8 - (pos - s) % 8;
1576 *cx.cursor = pos + buf.insert(pos, &" ".repeat(n));
1577 }
1578 VimKey::Backspace if pos > 0 => {
1579 buf.remove(pos - 1, pos);
1580 *cx.cursor = pos - 1;
1581 }
1582 VimKey::Delete if pos < buf.len() => {
1583 buf.remove(pos, pos + 1);
1584 }
1585 VimKey::Ctrl('w') if pos > s => {
1586 let mut a = pos;
1587 while a > s && is_blank(buf.c[a - 1]) {
1588 a -= 1;
1589 }
1590 let kw = a > s && is_keyword(buf.c[a - 1]);
1591 while a > s && !is_blank(buf.c[a - 1]) && is_keyword(buf.c[a - 1]) == kw {
1592 a -= 1;
1593 }
1594 buf.remove(a, pos);
1595 *cx.cursor = a;
1596 }
1597 VimKey::Ctrl('u') if pos > s => {
1598 buf.remove(s, pos);
1599 *cx.cursor = s;
1600 }
1601 other => {
1602 changed = false;
1603 let e = buf.line_end(pos);
1604 let line = buf.line_of(pos);
1605 *cx.cursor = match other {
1606 VimKey::Left => pos.saturating_sub(1).max(s),
1607 VimKey::Right => (pos + 1).min(e),
1608 VimKey::Home => s,
1609 VimKey::End => e,
1610 VimKey::Up if line > 0 => {
1611 let s2 = buf.start_of_line(line - 1);
1612 (s2 + (pos - s)).min(buf.line_end(s2))
1613 }
1614 VimKey::Down if line + 1 < buf.lines() => {
1615 let s2 = buf.start_of_line(line + 1);
1616 (s2 + (pos - s)).min(buf.line_end(s2))
1617 }
1618 _ => pos,
1619 };
1620 }
1621 }
1622 if changed {
1623 *cx.text = buf.text();
1624 }
1625 }
1626
1627 fn leave_insert(&mut self, cx: &mut Cx) {
1628 let (count, kind) = self.insert_repeat;
1629 if count > 1 && !self.insert_keys.is_empty() && !cx.readonly {
1630 let keys = std::mem::take(&mut self.insert_keys);
1631 for _ in 1..count {
1632 if matches!(kind, 'o' | 'O') {
1633 let buf = Buf::new(cx.text);
1634 let pos = (*cx.cursor).min(buf.len());
1635 let indent = auto_indent(&buf.line_text(pos));
1636 let mut buf = buf;
1637 let e = buf.line_end(pos);
1638 *cx.cursor = e + buf.insert(e, &format!("\n{indent}"));
1639 *cx.text = buf.text();
1640 }
1641 for k in &keys {
1642 self.type_key(k, cx);
1643 }
1644 }
1645 }
1646 self.insert_keys.clear();
1647 self.mode = Mode::Normal;
1648 let buf = Buf::new(cx.text);
1649 if *cx.cursor > buf.line_start(*cx.cursor) {
1650 *cx.cursor -= 1;
1651 }
1652 self.end_change(cx);
1653 if let Some(rec) = self.dot_rec.take() {
1654 self.last_change = rec;
1655 }
1656 }
1657
1658 // ---- Command line ----
1659
1660 fn cmdline_key(&mut self, key: VimKey, cx: &mut Cx, fx: &mut Vec<Effect>) {
1661 let Some((prompt, input)) = &mut self.cmdline else {
1662 self.mode = Mode::Normal;
1663 return;
1664 };
1665 match key {
1666 VimKey::Esc | VimKey::Copy | VimKey::Ctrl('c') => {
1667 self.cmdline = None;
1668 self.search_op = None;
1669 self.mode = Mode::Normal;
1670 }
1671 VimKey::Backspace => {
1672 if input.pop().is_none() {
1673 self.cmdline = None;
1674 self.search_op = None;
1675 self.mode = Mode::Normal;
1676 }
1677 }
1678 VimKey::Char(c) => input.push(c),
1679 VimKey::Paste(t) => input.push_str(t.lines().next().unwrap_or("")),
1680 VimKey::Enter => {
1681 let (prompt, input) = (*prompt, std::mem::take(input));
1682 self.cmdline = None;
1683 self.mode = Mode::Normal;
1684 if prompt == ':' {
1685 self.run_ex(&input, cx, fx);
1686 } else {
1687 self.run_search(prompt == '/', input, cx, fx);
1688 }
1689 }
1690 _ => {}
1691 }
1692 }
1693
1694 fn run_search(&mut self, forward: bool, input: String, cx: &mut Cx, fx: &mut Vec<Effect>) {
1695 let op = self.search_op.take();
1696 let pattern = if input.is_empty() {
1697 match &self.search {
1698 Some(s) => s.pattern.clone(),
1699 None => return,
1700 }
1701 } else {
1702 input
1703 };
1704 let s = Search {
1705 pattern,
1706 forward,
1707 whole_word: false,
1708 };
1709 self.search = Some(s.clone());
1710 self.highlight = true;
1711 let mut buf = Buf::new(cx.text);
1712 let pos = *cx.cursor;
1713 let Some(p) = self.find_next(&buf, pos, forward, &s) else {
1714 return;
1715 };
1716 let Some((op, reg)) = op else {
1717 *cx.cursor = p;
1718 return;
1719 };
1720 let r = Range {
1721 start: pos.min(p),
1722 end: pos.max(p),
1723 linewise: false,
1724 };
1725 if op != Op::Yank {
1726 self.begin_change(cx);
1727 }
1728 self.apply(op, reg, r, &mut buf, cx, fx);
1729 if op != Op::Yank && self.mode != Mode::Insert {
1730 self.end_change(cx);
1731 }
1732 }
1733
1734 fn run_ex(&mut self, input: &str, cx: &mut Cx, fx: &mut Vec<Effect>) {
1735 use ex::Ex;
1736 let buf = Buf::new(cx.text);
1737 match ex::parse(input) {
1738 Ex::Write(None) => fx.push(Effect::Save),
1739 Ex::Write(Some(p)) => fx.push(Effect::SaveAs(p)),
1740 Ex::Quit { force } => {
1741 if !force && cx.dirty {
1742 self.error("E37: No write since last change (add ! to override)");
1743 } else {
1744 fx.push(Effect::Close { force });
1745 }
1746 }
1747 Ex::WriteQuit => fx.extend([Effect::Save, Effect::Close { force: false }]),
1748 Ex::Exit => {
1749 if cx.dirty {
1750 fx.push(Effect::Save);
1751 }
1752 fx.push(Effect::Close { force: false });
1753 }
1754 Ex::QuitAll { force } => fx.push(Effect::Quit { force }),
1755 Ex::Edit(p) => fx.push(Effect::Edit(p)),
1756 Ex::Line(n) => *cx.cursor = buf.first_non_blank(buf.start_of_line(n.saturating_sub(1))),
1757 Ex::LastLine => *cx.cursor = buf.first_non_blank(buf.start_of_line(buf.lines() - 1)),
1758 Ex::NoHighlight => self.highlight = false,
1759 Ex::Make => fx.push(Effect::Make),
1760 Ex::Next => fx.push(Effect::NextDoc),
1761 Ex::Prev => fx.push(Effect::PrevDoc),
1762 Ex::Unknown(s) => self.error(format!("E492: Not an editor command: {s}")),
1763 }
1764 }
1765}
1766
1767fn find_kind(kind: char) -> Kind {
1768 if matches!(kind, 'f' | 't') {
1769 Kind::Inclusive
1770 } else {
1771 Kind::Exclusive
1772 }
1773}
1774
1775fn toggle_case(c: char) -> char {
1776 if c.is_lowercase() {
1777 c.to_uppercase().next().unwrap_or(c)
1778 } else {
1779 c.to_lowercase().next().unwrap_or(c)
1780 }
1781}
1782
1783/// The keyword under (or after, on the same line) the cursor.
1784fn word_at(buf: &Buf, pos: usize) -> Option<String> {
1785 let e = buf.line_end(pos);
1786 let start = (pos..e).find(|&i| is_keyword(buf.c[i]))?;
1787 let mut a = start;
1788 while a > buf.line_start(pos) && is_keyword(buf.c[a - 1]) {
1789 a -= 1;
1790 }
1791 let mut b = start;
1792 while b < e && is_keyword(buf.c[b]) {
1793 b += 1;
1794 }
1795 Some(buf.c[a..b].iter().collect())
1796}
1797
1798/// Smartcase: case-insensitive unless the pattern has capitals.
1799fn matches_at(buf: &Buf, i: usize, s: &Search) -> bool {
1800 let pat: Vec<char> = s.pattern.chars().collect();
1801 if pat.is_empty() || i + pat.len() > buf.len() {
1802 return false;
1803 }
1804 let exact = pat.iter().any(|c| c.is_uppercase());
1805 let eq = |a: char, b: char| {
1806 if exact {
1807 a == b
1808 } else {
1809 a.to_lowercase().eq(b.to_lowercase())
1810 }
1811 };
1812 if !pat.iter().enumerate().all(|(k, &p)| eq(buf.c[i + k], p)) {
1813 return false;
1814 }
1815 !s.whole_word
1816 || ((i == 0 || !is_keyword(buf.c[i - 1]))
1817 && (i + pat.len() == buf.len() || !is_keyword(buf.c[i + pat.len()])))
1818}