Krolyxon
Commit 0103561

Branches

Tags

Refs →

bench510103561

add a Vim engine for the GUI editor

A pure state machine over a String and a char cursor: Normal, Insert,
Visual and Visual Line modes, motions with counts, d/c/y/>/< with motions
and text objects, registers, undo/redo, . repeat, macros, search with
smartcase, and : commands. gd finds a label's definition and new lines
line up after labels. Tested on plain text, with no UI.
Author
krolyxon
Date
2026-11-01 09:45 +0530
Parent
88f3865
Commit
0103561bdf49c0acaf84278ca838433917e34cf5

+2874 −0

5 files changed

  1. A crates/bench51-gui/src/vim/ex.rs +84 −0
  2. A crates/bench51-gui/src/vim/keys.rs +128 −0
  3. A crates/bench51-gui/src/vim/mod.rs +1818 −0
  4. A crates/bench51-gui/src/vim/tests.rs +358 −0
  5. A crates/bench51-gui/src/vim/text.rs +486 −0
A crates/bench51-gui/src/vim/ex.rs View
@@ -0,0 +1,84 @@
1+//! `:` commands.
2+
3+#[derive(Debug, Clone, PartialEq, Eq)]
4+pub enum Ex {
5+ Write(Option<String>),
6+ Quit {
7+ force: bool,
8+ },
9+ WriteQuit,
10+ /// `:x`: write if changed, then quit.
11+ Exit,
12+ QuitAll {
13+ force: bool,
14+ },
15+ Edit(String),
16+ /// 1-based line number.
17+ Line(usize),
18+ LastLine,
19+ NoHighlight,
20+ Make,
21+ Next,
22+ Prev,
23+ Unknown(String),
24+}
25+
26+pub fn parse(input: &str) -> Ex {
27+ let s = input.trim();
28+ if let Ok(n) = s.parse::<usize>() {
29+ return Ex::Line(n);
30+ }
31+ if s == "$" {
32+ return Ex::LastLine;
33+ }
34+ let (cmd, arg) = match s.find(char::is_whitespace) {
35+ Some(i) => (&s[..i], s[i..].trim()),
36+ None => (s, ""),
37+ };
38+ let (cmd, bang) = match cmd.strip_suffix('!') {
39+ Some(c) => (c, true),
40+ None => (cmd, false),
41+ };
42+ let arg = (!arg.is_empty()).then(|| arg.to_string());
43+ match cmd {
44+ "w" | "write" => Ex::Write(arg),
45+ "q" | "quit" | "clo" | "close" => Ex::Quit { force: bang },
46+ "wq" => Ex::WriteQuit,
47+ "x" | "xit" | "exi" | "exit" => Ex::Exit,
48+ "qa" | "qall" | "quita" | "quitall" => Ex::QuitAll { force: bang },
49+ "e" | "edit" => match arg {
50+ Some(a) => Ex::Edit(a),
51+ None => Ex::Unknown(s.to_string()),
52+ },
53+ "noh" | "nohl" | "nohlsearch" => Ex::NoHighlight,
54+ "mak" | "make" => Ex::Make,
55+ "bn" | "bnext" | "tabn" | "tabnext" => Ex::Next,
56+ "bp" | "bprevious" | "bN" | "bNext" | "tabp" | "tabprevious" | "tabN" => Ex::Prev,
57+ _ => Ex::Unknown(s.to_string()),
58+ }
59+}
60+
61+#[cfg(test)]
62+mod tests {
63+ use super::{Ex, parse};
64+
65+ #[test]
66+ fn parses_common_commands() {
67+ assert_eq!(parse("w"), Ex::Write(None));
68+ assert_eq!(parse("w out.asm "), Ex::Write(Some("out.asm".into())));
69+ assert_eq!(parse("q!"), Ex::Quit { force: true });
70+ assert_eq!(parse("wq"), Ex::WriteQuit);
71+ assert_eq!(parse("x"), Ex::Exit);
72+ assert_eq!(parse("qa!"), Ex::QuitAll { force: true });
73+ assert_eq!(parse("42"), Ex::Line(42));
74+ assert_eq!(parse("$"), Ex::LastLine);
75+ assert_eq!(
76+ parse("e examples/blink.asm"),
77+ Ex::Edit("examples/blink.asm".into())
78+ );
79+ assert_eq!(parse("noh"), Ex::NoHighlight);
80+ assert_eq!(parse("make"), Ex::Make);
81+ assert_eq!(parse("bn"), Ex::Next);
82+ assert_eq!(parse("frobnicate"), Ex::Unknown("frobnicate".into()));
83+ }
84+}
A crates/bench51-gui/src/vim/keys.rs View
@@ -0,0 +1,128 @@
1+//! Keys as the Vim engine sees them, and their conversion from egui events.
2+
3+use eframe::egui::{Event, Key};
4+
5+#[derive(Clone, Debug, PartialEq, Eq)]
6+pub enum VimKey {
7+ Char(char),
8+ Esc,
9+ Enter,
10+ Backspace,
11+ Delete,
12+ Tab,
13+ Up,
14+ Down,
15+ Left,
16+ Right,
17+ Home,
18+ End,
19+ PageUp,
20+ PageDown,
21+ /// Ctrl plus a lowercase letter.
22+ Ctrl(char),
23+ /// Ctrl+V: the system clipboard's text.
24+ Paste(String),
25+ /// Ctrl+C.
26+ Copy,
27+ /// Ctrl+X.
28+ Cut,
29+}
30+
31+impl VimKey {
32+ /// How the key is shown in the mode line while a command is pending.
33+ pub fn label(&self) -> String {
34+ match self {
35+ VimKey::Char(c) => c.to_string(),
36+ VimKey::Ctrl(c) => format!("^{}", c.to_ascii_uppercase()),
37+ VimKey::Esc => "<Esc>".into(),
38+ VimKey::Enter => "<CR>".into(),
39+ other => format!("<{other:?}>"),
40+ }
41+ }
42+}
43+
44+/// The keys in a keyboard event; text events can carry several chars.
45+pub fn from_egui(event: &Event) -> Vec<VimKey> {
46+ match event {
47+ Event::Text(t) => t.chars().map(VimKey::Char).collect(),
48+ Event::Paste(t) => vec![VimKey::Paste(t.clone())],
49+ Event::Copy => vec![VimKey::Copy],
50+ Event::Cut => vec![VimKey::Cut],
51+ Event::Key {
52+ key,
53+ pressed: true,
54+ modifiers,
55+ ..
56+ } => {
57+ if modifiers.ctrl && !modifiers.alt {
58+ let name = key.name();
59+ if *key == Key::OpenBracket {
60+ return vec![VimKey::Esc];
61+ }
62+ if name.len() == 1 && name.as_bytes()[0].is_ascii_alphabetic() {
63+ return vec![VimKey::Ctrl(
64+ name.to_ascii_lowercase().chars().next().unwrap(),
65+ )];
66+ }
67+ return Vec::new();
68+ }
69+ let k = match key {
70+ Key::Escape => VimKey::Esc,
71+ Key::Enter => VimKey::Enter,
72+ Key::Backspace => VimKey::Backspace,
73+ Key::Delete => VimKey::Delete,
74+ Key::Tab if !modifiers.shift => VimKey::Tab,
75+ Key::ArrowUp => VimKey::Up,
76+ Key::ArrowDown => VimKey::Down,
77+ Key::ArrowLeft => VimKey::Left,
78+ Key::ArrowRight => VimKey::Right,
79+ Key::Home => VimKey::Home,
80+ Key::End => VimKey::End,
81+ Key::PageUp => VimKey::PageUp,
82+ Key::PageDown => VimKey::PageDown,
83+ // Printable keys arrive as `Event::Text`.
84+ _ => return Vec::new(),
85+ };
86+ vec![k]
87+ }
88+ _ => Vec::new(),
89+ }
90+}
91+
92+/// Keys from a string, for tests and replays: `<Esc>`, `<CR>`, `<BS>`,
93+/// `<C-r>` style names, everything else literally.
94+#[cfg(test)]
95+pub fn parse_keys(s: &str) -> Vec<VimKey> {
96+ let mut out = Vec::new();
97+ let mut rest = s;
98+ while let Some(c) = rest.chars().next() {
99+ if c == '<'
100+ && let Some(end) = rest.find('>')
101+ {
102+ let name = &rest[1..end];
103+ let k = match name {
104+ "Esc" => Some(VimKey::Esc),
105+ "CR" => Some(VimKey::Enter),
106+ "BS" => Some(VimKey::Backspace),
107+ "Del" => Some(VimKey::Delete),
108+ "Tab" => Some(VimKey::Tab),
109+ "Up" => Some(VimKey::Up),
110+ "Down" => Some(VimKey::Down),
111+ "Left" => Some(VimKey::Left),
112+ "Right" => Some(VimKey::Right),
113+ _ if name.starts_with("C-") && name.len() == 3 => {
114+ Some(VimKey::Ctrl(name.chars().nth(2).unwrap()))
115+ }
116+ _ => None,
117+ };
118+ if let Some(k) = k {
119+ out.push(k);
120+ rest = &rest[end + 1..];
121+ continue;
122+ }
123+ }
124+ out.push(VimKey::Char(c));
125+ rest = &rest[c.len_utf8()..];
126+ }
127+ out
128+}
A crates/bench51-gui/src/vim/mod.rs View
@@ -0,0 +1,1818 @@
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+
6+mod ex;
7+mod keys;
8+#[cfg(test)]
9+mod tests;
10+mod text;
11+
12+use std::collections::HashMap;
13+
14+pub use keys::{VimKey, from_egui};
15+use text::{Buf, Kind, auto_indent, is_blank, is_keyword};
16+
17+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18+pub 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)]
28+pub 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)]
36+pub 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)]
59+pub struct View {
60+ pub top: usize,
61+ pub rows: usize,
62+}
63+
64+impl 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)]
72+pub struct History {
73+ undo: Vec<(String, usize)>,
74+ redo: Vec<(String, usize)>,
75+}
76+
77+const HISTORY_LIMIT: usize = 1000;
78+
79+/// The document a key acts on.
80+pub 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)]
92+struct Reg {
93+ text: String,
94+ linewise: bool,
95+}
96+
97+/// The last search. It stays highlighted until `:noh`.
98+#[derive(Clone, Debug)]
99+pub struct Search {
100+ pub pattern: String,
101+ forward: bool,
102+ whole_word: bool,
103+}
104+
105+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
106+enum Op {
107+ Delete,
108+ Change,
109+ Yank,
110+ Indent,
111+ Dedent,
112+ Case,
113+}
114+
115+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
116+enum 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)]
149+enum 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)]
158+enum 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+
201+impl 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+
230+enum 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)]
239+struct Range {
240+ start: usize,
241+ end: usize,
242+ linewise: bool,
243+}
244+
245+fn 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+
259+fn is_register(c: char) -> bool {
260+ c.is_ascii_alphabetic() || c == '"' || c == '_'
261+}
262+
263+fn 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.
277+fn 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+
344+fn 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+
508+pub 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+
538+impl 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+
566+impl 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+
1767+fn find_kind(kind: char) -> Kind {
1768+ if matches!(kind, 'f' | 't') {
1769+ Kind::Inclusive
1770+ } else {
1771+ Kind::Exclusive
1772+ }
1773+}
1774+
1775+fn 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.
1784+fn 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.
1799+fn 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+}
A crates/bench51-gui/src/vim/tests.rs View
@@ -0,0 +1,358 @@
1+//! Engine tests. Buffers are written with `|` marking the cursor.
2+
3+use super::keys::parse_keys;
4+use super::{Cx, Effect, History, Mode, View, Vim};
5+
6+struct T {
7+ vim: Vim,
8+ text: String,
9+ cursor: usize,
10+ history: History,
11+ readonly: bool,
12+ dirty: bool,
13+ fx: Vec<Effect>,
14+}
15+
16+impl T {
17+ fn new(marked: &str) -> Self {
18+ let cursor = marked.chars().position(|c| c == '|').unwrap_or(0);
19+ Self {
20+ vim: Vim::default(),
21+ text: marked.replace('|', ""),
22+ cursor,
23+ history: History::default(),
24+ readonly: false,
25+ dirty: false,
26+ fx: Vec::new(),
27+ }
28+ }
29+
30+ fn keys(&mut self, keys: &str) -> &mut Self {
31+ for k in parse_keys(keys) {
32+ let mut cx = Cx {
33+ text: &mut self.text,
34+ cursor: &mut self.cursor,
35+ history: &mut self.history,
36+ readonly: self.readonly,
37+ dirty: self.dirty,
38+ view: View { top: 0, rows: 10 },
39+ };
40+ let fx = self.vim.key(k, &mut cx);
41+ self.fx.extend(fx);
42+ }
43+ self
44+ }
45+
46+ fn show(&self) -> String {
47+ let mut s: Vec<char> = self.text.chars().collect();
48+ s.insert(self.cursor.min(s.len()), '|');
49+ s.into_iter().collect()
50+ }
51+}
52+
53+fn after(start: &str, keys: &str) -> String {
54+ T::new(start).keys(keys).show()
55+}
56+
57+#[test]
58+fn word_and_line_motions() {
59+ assert_eq!(after("|MOV A,#1 ; c", "w"), "MOV |A,#1 ; c");
60+ assert_eq!(after("|MOV A,#1 ; c", "ww"), "MOV A|,#1 ; c");
61+ assert_eq!(after("|MOV A,#1 ; c", "W"), "MOV |A,#1 ; c");
62+ assert_eq!(after("|MOV A,#1 ; c", "2W"), "MOV A,#1 |; c");
63+ assert_eq!(after("MOV A,#1 ; |c", "b"), "MOV A,#1 |; c");
64+ assert_eq!(after("|MOV A,#1", "e"), "MO|V A,#1");
65+ assert_eq!(after(" MOV |A", "0"), "| MOV A");
66+ assert_eq!(after(" MOV |A", "^"), " |MOV A");
67+ assert_eq!(after(" |MOV A", "$"), " MOV |A");
68+ assert_eq!(after("foo\n\n|bar", "b"), "foo\n|\nbar");
69+ assert_eq!(after("|foo\n\nbar", "w"), "foo\n|\nbar");
70+}
71+
72+#[test]
73+fn vertical_motions_keep_the_column() {
74+ assert_eq!(after("abcd|ef\nab\nabcdef", "j"), "abcdef\na|b\nabcdef");
75+ assert_eq!(after("abcd|ef\nab\nabcdef", "jj"), "abcdef\nab\nabcd|ef");
76+ assert_eq!(after("a|bc\nabcdef", "$j"), "abc\nabcde|f");
77+ assert_eq!(after("a\nb\nc\n|d", "gg"), "|a\nb\nc\nd");
78+ assert_eq!(after("|a\nb\nc\nd", "G"), "a\nb\nc\n|d");
79+ assert_eq!(after("|a\nb\nc\nd", "3G"), "a\nb\n|c\nd");
80+ assert_eq!(after("|a\nb\nc\nd", "2j"), "a\nb\n|c\nd");
81+ assert_eq!(after("|a\n b", "<CR>"), "a\n |b");
82+}
83+
84+#[test]
85+fn find_and_match_motions() {
86+ assert_eq!(after("|a,b,c,d", "f,"), "a|,b,c,d");
87+ assert_eq!(after("|a,b,c,d", "f,;"), "a,b|,c,d");
88+ assert_eq!(after("|a,b,c,d", "f,;,"), "a|,b,c,d");
89+ assert_eq!(after("|a,b,c,d", "2f,"), "a,b|,c,d");
90+ assert_eq!(after("|a,b,c,d", "t,"), "|a,b,c,d");
91+ assert_eq!(after("|ab,c", "t,"), "a|b,c");
92+ assert_eq!(after("a,b,|c", "F,"), "a,b|,c");
93+ assert_eq!(after("MOV A,|#(1+(2))", "%"), "MOV A,#(1+(2)|)");
94+ assert_eq!(after("MOV A,#(1+(2)|)", "%"), "MOV A,#|(1+(2))");
95+ assert_eq!(after("|a\nb\n\nc\nd", "}"), "a\nb\n|\nc\nd");
96+}
97+
98+#[test]
99+fn delete_yank_put_lines() {
100+ assert_eq!(after("a\n|b\nc", "dd"), "a\n|c");
101+ assert_eq!(after("a\nb\n|c", "dd"), "a\n|b");
102+ assert_eq!(after("|a\nb\nc", "2dd"), "|c");
103+ assert_eq!(after("|a\nb\nc", "dj"), "|c");
104+ assert_eq!(after("a\n|b\nc", "dk"), "|c");
105+ assert_eq!(after("|a\nb", "yyp"), "a\n|a\nb");
106+ assert_eq!(after("a\n|b", "yyp"), "a\nb\n|b");
107+ assert_eq!(after("a\n|b", "yyP"), "a\n|b\nb");
108+ assert_eq!(after("| a\nb", "ddp"), "b\n |a");
109+ assert_eq!(after("|a\nb\nc", "dG"), "|");
110+ assert_eq!(after("|para\none\n\nnext", "d}"), "|\nnext");
111+}
112+
113+#[test]
114+fn charwise_operators() {
115+ assert_eq!(after("foo |bar\nbaz", "dw"), "foo| \nbaz");
116+ assert_eq!(after("|foo bar", "dw"), "|bar");
117+ assert_eq!(after("|foo bar", "d2w"), "|");
118+ assert_eq!(after("|MOV A", "cwMOVX<Esc>"), "MOV|X A");
119+ assert_eq!(after("MO|V A", "cw<Esc>"), "M|O A");
120+ assert_eq!(after("MOV A,B|B", "ciwR7<Esc>"), "MOV A,R|7");
121+ assert_eq!(after("DB \"hel|lo\", 0", "ci\"bye<Esc>"), "DB \"by|e\", 0");
122+ assert_eq!(after("DB \"hel|lo\", 0", "da\""), "DB |, 0");
123+ assert_eq!(after("MOV A,#(1+|2)", "di("), "MOV A,#(|)");
124+ assert_eq!(after("MOV A,#(1+|2)", "da("), "MOV A,|#");
125+ assert_eq!(after("a|bc", "x"), "a|c");
126+ assert_eq!(after("ab|c", "x"), "a|b");
127+ assert_eq!(after("ab|c", "X"), "a|c");
128+ assert_eq!(after("a|bcd", "D"), "|a");
129+ assert_eq!(after("a|bcd", "Cx<Esc>"), "a|x");
130+ assert_eq!(after("a|bcd", "sX<Esc>"), "a|Xcd");
131+ assert_eq!(after("|MOV A,B", "dt,"), "|,B");
132+ assert_eq!(after("|MOV A,B", "df,"), "|B");
133+ assert_eq!(after("|MOV A,B", "y$P"), "MOV A,|BMOV A,B");
134+}
135+
136+#[test]
137+fn indent_join_case_replace() {
138+ assert_eq!(after("|MOV A,B", ">>"), " |MOV A,B");
139+ assert_eq!(after(" MOV |A,B", "<<"), "|MOV A,B");
140+ assert_eq!(after("|a\nb", "2>>"), " |a\n b");
141+ assert_eq!(after("|a\n b", "J"), "a| b");
142+ assert_eq!(after("|a\nb\nc", "3J"), "a b| c");
143+ assert_eq!(after("|abc", "~~"), "AB|c");
144+ assert_eq!(after("|abc", "rx"), "|xbc");
145+ assert_eq!(after("|abc", "2rx"), "x|xc");
146+}
147+
148+#[test]
149+fn undo_redo_and_dot() {
150+ assert_eq!(after("|abc", "xxu"), "|bc");
151+ assert_eq!(after("|abc", "xxuu"), "|abc");
152+ assert_eq!(after("|abc", "xxuu<C-r>"), "|bc");
153+ assert_eq!(after("|abc", "ixy<Esc>u"), "|abc");
154+ assert_eq!(after("|a b c d", "dw."), "|c d");
155+ assert_eq!(after("|a b c d", "dw2."), "|d");
156+ assert_eq!(after("|ab\ncd", "Ax<Esc>j."), "abx\ncd|x");
157+ assert_eq!(after("|a b", "cwX<Esc>w."), "X |X");
158+ // Undoing a `.` undoes the whole repeated change.
159+ assert_eq!(after("|ab\ncd", "Axy<Esc>j.u"), "abxy\nc|d");
160+}
161+
162+#[test]
163+fn insert_mode_and_counts() {
164+ assert_eq!(after("|", "3ix<Esc>"), "xx|x");
165+ assert_eq!(after("a|b", "ix<Esc>"), "a|xb");
166+ assert_eq!(after("a|b", "ax<Esc>"), "ab|x");
167+ assert_eq!(after(" a|b", "Ix<Esc>"), " |xab");
168+ assert_eq!(after("a|b", "Ax<Esc>"), "ab|x");
169+ assert_eq!(after("a|b", "ix<BS>y<Esc>"), "a|yb");
170+ assert_eq!(after("|", "i<Tab>x<Esc>"), " |x");
171+ assert_eq!(after("ab|", "a<CR>c<Esc>"), "ab\n|c");
172+ assert_eq!(after("|a", "2ox<Esc>"), "a\nx\n|x");
173+}
174+
175+#[test]
176+fn new_lines_line_up_after_labels() {
177+ assert_eq!(
178+ after("|main: MOV A,#1", "oNOP<Esc>"),
179+ "main: MOV A,#1\n NO|P"
180+ );
181+ assert_eq!(after("|loop:", "oNOP<Esc>"), "loop:\n NO|P");
182+ assert_eq!(
183+ after("| MOV A,B", "oX<Esc>"),
184+ " MOV A,B\n |X"
185+ );
186+ assert_eq!(after("|; comment", "oX<Esc>"), "; comment\n|X");
187+ assert_eq!(after(" |NOP", "OX<Esc>"), " |X\n NOP");
188+}
189+
190+#[test]
191+fn visual_mode() {
192+ let mut t = T::new("|abcdef");
193+ t.keys("lvlly");
194+ assert_eq!(t.show(), "a|bcdef");
195+ assert!(t.fx.contains(&Effect::Clipboard("bcd".into())));
196+ t.keys("$p");
197+ assert_eq!(t.show(), "abcdefbc|d");
198+ assert_eq!(after("a\n|b\nc\nd", "Vjd"), "a\n|d");
199+ assert_eq!(after("|abcdef", "vld"), "|cdef");
200+ assert_eq!(after("|abcdef", "vlcX<Esc>"), "|Xcdef");
201+ assert_eq!(after("|a\nb", "Vj>"), " |a\n b");
202+ assert_eq!(after("|abc", "vl~"), "|ABc");
203+ assert_eq!(after("|ab cd", "yiwwviwp"), "ab |ab");
204+ let mut t = T::new("|abc");
205+ t.keys("v");
206+ assert_eq!(t.vim.selection(&t.text, t.cursor), Some((0, 1)));
207+ t.keys("l");
208+ assert_eq!(t.vim.selection(&t.text, t.cursor), Some((0, 2)));
209+ t.keys("<Esc>");
210+ assert_eq!(t.vim.mode, Mode::Normal);
211+}
212+
213+#[test]
214+fn registers_and_clipboard() {
215+ let mut t = T::new("|one\ntwo");
216+ t.keys("\"ayyj\"byy\"ap");
217+ assert_eq!(t.show(), "one\ntwo\n|one");
218+ assert!(t.fx.contains(&Effect::Clipboard("one\n".into())));
219+ // The black hole register doesn't touch the clipboard or `"`.
220+ let mut t = T::new("|ab");
221+ t.keys("yl\"_xp");
222+ assert_eq!(t.show(), "b|a");
223+ assert_eq!(t.fx, vec![Effect::Clipboard("a".into())]);
224+ // Ctrl+V pastes the system clipboard before the cursor.
225+ let mut t = T::new("a|b");
226+ t.vim.key(
227+ super::VimKey::Paste("XY".into()),
228+ &mut Cx {
229+ text: &mut t.text,
230+ cursor: &mut t.cursor,
231+ history: &mut t.history,
232+ readonly: false,
233+ dirty: false,
234+ view: View::default(),
235+ },
236+ );
237+ assert_eq!(t.show(), "aX|Yb");
238+}
239+
240+#[test]
241+fn search() {
242+ assert_eq!(after("|foo bar\nbaz bar", "/bar<CR>"), "foo |bar\nbaz bar");
243+ assert_eq!(after("|foo bar\nbaz bar", "/bar<CR>n"), "foo bar\nbaz |bar");
244+ assert_eq!(
245+ after("|foo bar\nbaz bar", "/bar<CR>nn"),
246+ "foo |bar\nbaz bar"
247+ );
248+ assert_eq!(after("|foo bar\nbaz bar", "/bar<CR>N"), "foo bar\nbaz |bar");
249+ assert_eq!(after("|foo bar\nbaz bar", "?baz<CR>"), "foo bar\n|baz bar");
250+ assert_eq!(after("|foo Bar bar", "/bar<CR>"), "foo |Bar bar");
251+ assert_eq!(after("|foo Bar bar", "/Bar<CR>n"), "foo |Bar bar");
252+ assert_eq!(after("|loop loops loop", "*"), "loop loops |loop");
253+ assert_eq!(after("|foo bar", "d/bar<CR>"), "|bar");
254+ assert_eq!(after("|foo bar", "d/zzz<CR>"), "|foo bar");
255+ assert_eq!(after("|foo bar", "d/<Esc>x"), "|oo bar");
256+ let mut t = T::new("|foo bar bar");
257+ t.keys("/bar<CR>");
258+ assert_eq!(t.vim.search_ranges(&t.text), vec![(4, 7), (8, 11)]);
259+ t.keys(":noh<CR>");
260+ assert!(t.vim.search_ranges(&t.text).is_empty());
261+ let mut t = T::new("|foo");
262+ t.keys("/zzz<CR>");
263+ assert_eq!(t.show(), "|foo");
264+ assert!(t.vim.message.as_ref().is_some_and(|m| m.1));
265+}
266+
267+#[test]
268+fn macros() {
269+ assert_eq!(after("|1\n2\n3", "qaA;<Esc>jq@a"), "1;\n2;\n|3");
270+ assert_eq!(after("|1\n2\n3\n4", "qaA;<Esc>jq2@a"), "1;\n2;\n3;\n|4");
271+ assert_eq!(after("|1\n2\n3", "qaA;<Esc>jq@a@@"), "1;\n2;\n3|;");
272+ let mut t = T::new("|x");
273+ t.keys("qa");
274+ assert_eq!(t.vim.recording(), Some('a'));
275+ t.keys("q");
276+ assert_eq!(t.vim.recording(), None);
277+}
278+
279+#[test]
280+fn ex_commands() {
281+ let mut t = T::new("|a\nb\nc");
282+ t.keys(":w<CR>:wq<CR>:qa!<CR>:make<CR>:bn<CR>");
283+ assert_eq!(
284+ t.fx,
285+ vec![
286+ Effect::Save,
287+ Effect::Save,
288+ Effect::Close { force: false },
289+ Effect::Quit { force: true },
290+ Effect::Make,
291+ Effect::NextDoc,
292+ ]
293+ );
294+ assert_eq!(after("|a\nb\n c", ":3<CR>"), "a\nb\n |c");
295+ assert_eq!(after("|a\nb\n c", ":$<CR>"), "a\nb\n |c");
296+ let mut t = T::new("|a");
297+ t.dirty = true;
298+ t.keys(":q<CR>");
299+ assert!(t.fx.is_empty());
300+ assert!(
301+ t.vim
302+ .message
303+ .as_ref()
304+ .is_some_and(|m| m.0.starts_with("E37"))
305+ );
306+ t.keys(":q!<CR>");
307+ assert_eq!(t.fx, vec![Effect::Close { force: true }]);
308+ let mut t = T::new("|a");
309+ t.keys(":wat<CR>");
310+ assert!(
311+ t.vim
312+ .message
313+ .as_ref()
314+ .is_some_and(|m| m.0.starts_with("E492"))
315+ );
316+ assert_eq!(t.vim.mode, Mode::Normal);
317+}
318+
319+#[test]
320+fn read_only_allows_moving_and_yanking() {
321+ let mut t = T::new("|abc\ndef");
322+ t.readonly = true;
323+ t.keys("x");
324+ assert_eq!(t.show(), "|abc\ndef");
325+ assert!(t.vim.message.as_ref().is_some_and(|m| m.1));
326+ t.keys("ix");
327+ assert_eq!(t.vim.mode, Mode::Normal);
328+ t.keys("jlyl");
329+ assert_eq!(t.show(), "abc\nd|ef");
330+ assert!(t.fx.contains(&Effect::Clipboard("e".into())));
331+}
332+
333+#[test]
334+fn goto_definition_and_tabs() {
335+ assert_eq!(
336+ after(" SJMP |loop\nloop: NOP", "gd"),
337+ " SJMP loop\n|loop: NOP"
338+ );
339+ assert_eq!(
340+ after("MOV A,#|RELOAD\nRELOAD EQU 5", "gd"),
341+ "MOV A,#RELOAD\n|RELOAD EQU 5"
342+ );
343+ let mut t = T::new("|a");
344+ t.keys("gtgT");
345+ assert_eq!(t.fx, vec![Effect::NextDoc, Effect::PrevDoc]);
346+}
347+
348+#[test]
349+fn escape_and_pending() {
350+ let mut t = T::new("|abc");
351+ t.keys("2d");
352+ assert_eq!(t.vim.pending_label(), "2d");
353+ t.keys("<Esc>");
354+ assert_eq!(t.vim.pending_label(), "");
355+ assert_eq!(t.show(), "|abc");
356+ assert_eq!(after("ab|c", "i<Esc>"), "a|bc");
357+ assert_eq!(after("|abc", "i<Esc>"), "|abc");
358+}
A crates/bench51-gui/src/vim/text.rs View
@@ -0,0 +1,486 @@
1+//! Char-indexed text helpers for the Vim engine: lines, columns, word
2+//! classes, motions and text objects. Positions are char indices.
3+
4+/// How a motion's range is taken by an operator (`:help exclusive`).
5+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6+pub enum Kind {
7+ Exclusive,
8+ Inclusive,
9+ Linewise,
10+}
11+
12+pub struct Buf {
13+ pub c: Vec<char>,
14+}
15+
16+pub fn is_blank(ch: char) -> bool {
17+ ch == ' ' || ch == '\t'
18+}
19+
20+fn is_space(ch: char) -> bool {
21+ ch.is_whitespace()
22+}
23+
24+pub fn is_keyword(ch: char) -> bool {
25+ ch.is_alphanumeric() || ch == '_'
26+}
27+
28+/// 0 whitespace, 1 keyword, 2 punctuation; `big` words only tell
29+/// whitespace from the rest.
30+fn class(ch: char, big: bool) -> u8 {
31+ if is_space(ch) {
32+ 0
33+ } else if big || is_keyword(ch) {
34+ 1
35+ } else {
36+ 2
37+ }
38+}
39+
40+impl Buf {
41+ pub fn new(text: &str) -> Self {
42+ Self {
43+ c: text.chars().collect(),
44+ }
45+ }
46+
47+ pub fn text(&self) -> String {
48+ self.c.iter().collect()
49+ }
50+
51+ pub fn len(&self) -> usize {
52+ self.c.len()
53+ }
54+
55+ pub fn line_start(&self, pos: usize) -> usize {
56+ let mut i = pos.min(self.len());
57+ while i > 0 && self.c[i - 1] != '\n' {
58+ i -= 1;
59+ }
60+ i
61+ }
62+
63+ /// Index of the line's `\n`, or the text length on the last line.
64+ pub fn line_end(&self, pos: usize) -> usize {
65+ let mut i = pos.min(self.len());
66+ while i < self.len() && self.c[i] != '\n' {
67+ i += 1;
68+ }
69+ i
70+ }
71+
72+ pub fn line_of(&self, pos: usize) -> usize {
73+ self.c[..pos.min(self.len())]
74+ .iter()
75+ .filter(|&&ch| ch == '\n')
76+ .count()
77+ }
78+
79+ pub fn lines(&self) -> usize {
80+ self.c.iter().filter(|&&ch| ch == '\n').count() + 1
81+ }
82+
83+ /// Start of `line`, clamped to the last line.
84+ pub fn start_of_line(&self, line: usize) -> usize {
85+ if line == 0 {
86+ return 0;
87+ }
88+ let mut seen = 0;
89+ let mut last_start = 0;
90+ for (i, &ch) in self.c.iter().enumerate() {
91+ if ch == '\n' {
92+ seen += 1;
93+ last_start = i + 1;
94+ if seen == line {
95+ return i + 1;
96+ }
97+ }
98+ }
99+ last_start
100+ }
101+
102+ pub fn col(&self, pos: usize) -> usize {
103+ pos - self.line_start(pos)
104+ }
105+
106+ pub fn line_is_blank(&self, line: usize) -> bool {
107+ let s = self.start_of_line(line);
108+ let e = self.line_end(s);
109+ self.c[s..e].iter().all(|&ch| is_blank(ch))
110+ }
111+
112+ pub fn line_text(&self, pos: usize) -> String {
113+ let s = self.line_start(pos);
114+ self.c[s..self.line_end(s)].iter().collect()
115+ }
116+
117+ /// Position on `line` at `col`, clamped to the line's last char.
118+ pub fn at_col(&self, line: usize, col: usize) -> usize {
119+ let s = self.start_of_line(line);
120+ let e = self.line_end(s);
121+ if e == s { s } else { (s + col).min(e - 1) }
122+ }
123+
124+ pub fn first_non_blank(&self, pos: usize) -> usize {
125+ let s = self.line_start(pos);
126+ let e = self.line_end(s);
127+ let mut i = s;
128+ while i < e && is_blank(self.c[i]) {
129+ i += 1;
130+ }
131+ if i == e && e > s { e - 1 } else { i }
132+ }
133+
134+ /// In Normal mode the cursor sits on a character, never on the `\n`
135+ /// (except on an empty line).
136+ pub fn clamp_normal(&self, pos: usize) -> usize {
137+ let pos = pos.min(self.len());
138+ let s = self.line_start(pos);
139+ let e = self.line_end(pos);
140+ if e > s && pos >= e { e - 1 } else { pos }
141+ }
142+
143+ pub fn insert(&mut self, at: usize, s: &str) -> usize {
144+ let new: Vec<char> = s.chars().collect();
145+ let n = new.len();
146+ self.c.splice(at..at, new);
147+ n
148+ }
149+
150+ pub fn remove(&mut self, start: usize, end: usize) -> String {
151+ self.c.drain(start..end).collect()
152+ }
153+
154+ // ---- Motions ----
155+
156+ pub fn word_fwd(&self, pos: usize, big: bool) -> usize {
157+ let n = self.len();
158+ if pos >= n {
159+ return n;
160+ }
161+ let mut i = pos;
162+ let cls = class(self.c[i], big);
163+ if cls != 0 {
164+ while i < n && class(self.c[i], big) == cls {
165+ i += 1;
166+ }
167+ }
168+ while i < n && is_space(self.c[i]) {
169+ // An empty line counts as a word.
170+ if self.c[i] == '\n' && i + 1 < n && self.c[i + 1] == '\n' {
171+ return i + 1;
172+ }
173+ i += 1;
174+ }
175+ i
176+ }
177+
178+ pub fn word_back(&self, pos: usize, big: bool) -> usize {
179+ if pos == 0 {
180+ return 0;
181+ }
182+ let mut i = pos - 1;
183+ while is_space(self.c[i]) {
184+ let empty_line = self.c[i] == '\n' && (i == 0 || self.c[i - 1] == '\n');
185+ if empty_line && i < pos {
186+ return i;
187+ }
188+ if i == 0 {
189+ return 0;
190+ }
191+ i -= 1;
192+ }
193+ let cls = class(self.c[i], big);
194+ while i > 0 && class(self.c[i - 1], big) == cls {
195+ i -= 1;
196+ }
197+ i
198+ }
199+
200+ pub fn word_end(&self, pos: usize, big: bool) -> usize {
201+ let n = self.len();
202+ if n == 0 {
203+ return 0;
204+ }
205+ let mut i = pos + 1;
206+ while i < n && is_space(self.c[i]) {
207+ i += 1;
208+ }
209+ if i >= n {
210+ return n - 1;
211+ }
212+ let cls = class(self.c[i], big);
213+ while i + 1 < n && class(self.c[i + 1], big) == cls {
214+ i += 1;
215+ }
216+ i
217+ }
218+
219+ /// End (inclusive) of the run of same-class chars starting at `pos`.
220+ pub fn run_end(&self, pos: usize, big: bool) -> usize {
221+ let cls = class(self.c[pos], big);
222+ let mut i = pos;
223+ while i + 1 < self.len() && self.c[i + 1] != '\n' && class(self.c[i + 1], big) == cls {
224+ i += 1;
225+ }
226+ i
227+ }
228+
229+ /// `f`/`t` (forward) or `F`/`T` (backward) to `ch` on the current line.
230+ /// `repeat` is for `;` and `,`, where `t` mustn't stay stuck right
231+ /// before the char it stopped at last time.
232+ pub fn find_char(
233+ &self,
234+ pos: usize,
235+ kind: char,
236+ ch: char,
237+ count: usize,
238+ repeat: bool,
239+ ) -> Option<usize> {
240+ let s = self.line_start(pos);
241+ let e = self.line_end(pos);
242+ let mut i = pos;
243+ for k in 0..count {
244+ let skip = repeat && matches!(kind, 't' | 'T') && k == 0;
245+ i = match kind {
246+ 'f' | 't' => {
247+ let from = i + 1 + usize::from(skip && i + 1 < e && self.c[i + 1] == ch);
248+ (from..e).find(|&j| self.c[j] == ch)?
249+ }
250+ _ => {
251+ let to = if skip && i > s && self.c[i - 1] == ch {
252+ i - 1
253+ } else {
254+ i
255+ };
256+ (s..to).rev().find(|&j| self.c[j] == ch)?
257+ }
258+ };
259+ }
260+ Some(match kind {
261+ 't' => i - 1,
262+ 'T' => i + 1,
263+ _ => i,
264+ })
265+ }
266+
267+ /// `%`: the bracket matching the first one at or after `pos` on its line.
268+ pub fn match_pair(&self, pos: usize) -> Option<usize> {
269+ let e = self.line_end(pos);
270+ let start = (pos..e).find(|&i| "()[]{}".contains(self.c[i]))?;
271+ let open = self.c[start];
272+ let (close, fwd) = match open {
273+ '(' => (')', true),
274+ '[' => (']', true),
275+ '{' => ('}', true),
276+ ')' => ('(', false),
277+ ']' => ('[', false),
278+ _ => ('{', false),
279+ };
280+ let mut depth = 0usize;
281+ if fwd {
282+ for i in start..self.len() {
283+ if self.c[i] == open {
284+ depth += 1;
285+ } else if self.c[i] == close {
286+ depth -= 1;
287+ if depth == 0 {
288+ return Some(i);
289+ }
290+ }
291+ }
292+ } else {
293+ for i in (0..=start).rev() {
294+ if self.c[i] == open {
295+ depth += 1;
296+ } else if self.c[i] == close {
297+ depth -= 1;
298+ if depth == 0 {
299+ return Some(i);
300+ }
301+ }
302+ }
303+ }
304+ None
305+ }
306+
307+ /// `}` (forward) or `{`: the next blank line past the current paragraph.
308+ pub fn paragraph(&self, pos: usize, fwd: bool) -> usize {
309+ let last = self.lines() - 1;
310+ let mut l = self.line_of(pos);
311+ if fwd {
312+ while l < last && self.line_is_blank(l) {
313+ l += 1;
314+ }
315+ while l < last && !self.line_is_blank(l) {
316+ l += 1;
317+ }
318+ if l == last && !self.line_is_blank(l) {
319+ return self.len();
320+ }
321+ } else {
322+ while l > 0 && self.line_is_blank(l) {
323+ l -= 1;
324+ }
325+ while l > 0 && !self.line_is_blank(l) {
326+ l -= 1;
327+ }
328+ }
329+ self.start_of_line(l)
330+ }
331+
332+ /// Text object `c` (`w W " ' ( b [ { B < p`) around `pos`.
333+ pub fn object(&self, pos: usize, c: char, around: bool) -> Option<(usize, usize, bool)> {
334+ match c {
335+ 'w' => self.obj_word(pos, false, around),
336+ 'W' => self.obj_word(pos, true, around),
337+ '"' | '\'' | '`' => self.obj_quote(pos, c, around),
338+ '(' | ')' | 'b' => self.obj_paren(pos, '(', ')', around),
339+ '[' | ']' => self.obj_paren(pos, '[', ']', around),
340+ '{' | '}' | 'B' => self.obj_paren(pos, '{', '}', around),
341+ '<' | '>' => self.obj_paren(pos, '<', '>', around),
342+ 'p' => self.obj_paragraph(pos, around),
343+ _ => None,
344+ }
345+ }
346+
347+ // ---- Text objects ----
348+ //
349+ // Charwise objects return (start, end exclusive, false); linewise ones
350+ // return (a position on the first line, one on the last line, true).
351+
352+ pub fn obj_word(&self, pos: usize, big: bool, around: bool) -> Option<(usize, usize, bool)> {
353+ if pos >= self.len() || self.c[pos] == '\n' {
354+ return None;
355+ }
356+ let s = self.line_start(pos);
357+ let e = self.line_end(pos);
358+ let cls = class(self.c[pos], big);
359+ let mut a = pos;
360+ while a > s && class(self.c[a - 1], big) == cls {
361+ a -= 1;
362+ }
363+ let mut b = pos + 1;
364+ while b < e && class(self.c[b], big) == cls {
365+ b += 1;
366+ }
367+ if around {
368+ let b2 = (b..e).find(|&i| !is_blank(self.c[i])).unwrap_or(e);
369+ if b2 > b {
370+ b = b2;
371+ } else {
372+ while a > s && is_blank(self.c[a - 1]) {
373+ a -= 1;
374+ }
375+ }
376+ }
377+ Some((a, b, false))
378+ }
379+
380+ pub fn obj_quote(&self, pos: usize, q: char, around: bool) -> Option<(usize, usize, bool)> {
381+ let s = self.line_start(pos);
382+ let e = self.line_end(pos);
383+ let quotes: Vec<usize> = (s..e).filter(|&i| self.c[i] == q).collect();
384+ let pair = quotes
385+ .chunks_exact(2)
386+ .find(|p| pos <= p[1])
387+ .map(|p| (p[0], p[1]))?;
388+ let (open, close) = pair;
389+ if around {
390+ let mut b = close + 1;
391+ while b < e && is_blank(self.c[b]) {
392+ b += 1;
393+ }
394+ Some((open, b, false))
395+ } else {
396+ Some((open + 1, close, false))
397+ }
398+ }
399+
400+ pub fn obj_paren(
401+ &self,
402+ pos: usize,
403+ open: char,
404+ close: char,
405+ around: bool,
406+ ) -> Option<(usize, usize, bool)> {
407+ let mut depth = 0usize;
408+ let mut o = None;
409+ let start = if pos < self.len() && self.c[pos] == close {
410+ pos.checked_sub(1)?
411+ } else {
412+ pos
413+ };
414+ for i in (0..=start.min(self.len().saturating_sub(1))).rev() {
415+ if self.c[i] == close && i != pos {
416+ depth += 1;
417+ } else if self.c[i] == open {
418+ if depth == 0 {
419+ o = Some(i);
420+ break;
421+ }
422+ depth -= 1;
423+ }
424+ }
425+ let o = o?;
426+ let mut depth = 0usize;
427+ let c = (o + 1..self.len()).find(|&i| {
428+ if self.c[i] == open {
429+ depth += 1;
430+ } else if self.c[i] == close {
431+ if depth == 0 {
432+ return true;
433+ }
434+ depth -= 1;
435+ }
436+ false
437+ })?;
438+ Some(if around {
439+ (o, c + 1, false)
440+ } else {
441+ (o + 1, c, false)
442+ })
443+ }
444+
445+ pub fn obj_paragraph(&self, pos: usize, around: bool) -> Option<(usize, usize, bool)> {
446+ let last = self.lines() - 1;
447+ let l = self.line_of(pos);
448+ let blank = self.line_is_blank(l);
449+ let mut a = l;
450+ while a > 0 && self.line_is_blank(a - 1) == blank {
451+ a -= 1;
452+ }
453+ let mut b = l;
454+ while b < last && self.line_is_blank(b + 1) == blank {
455+ b += 1;
456+ }
457+ if around {
458+ while b < last && self.line_is_blank(b + 1) != blank {
459+ b += 1;
460+ }
461+ }
462+ Some((self.start_of_line(a), self.start_of_line(b), true))
463+ }
464+}
465+
466+/// Indentation for a new line after `line`: its leading whitespace, or for
467+/// a `label:` line, the column where its instruction starts (8 if bare).
468+pub fn auto_indent(line: &str) -> String {
469+ let lead: String = line.chars().take_while(|&c| is_blank(c)).collect();
470+ if !lead.is_empty() || line.trim().is_empty() {
471+ return lead;
472+ }
473+ let label_end = line
474+ .char_indices()
475+ .find(|&(_, c)| !(is_keyword(c) || c == '?'))
476+ .map_or(line.len(), |(i, _)| i);
477+ if label_end > 0 && line[label_end..].starts_with(':') {
478+ let rest = &line[label_end + 1..];
479+ if rest.trim().is_empty() || rest.trim_start().starts_with(';') {
480+ return " ".repeat(8);
481+ }
482+ let pad = rest.chars().take_while(|&c| is_blank(c)).count();
483+ return " ".repeat(line[..label_end].chars().count() + 1 + pad);
484+ }
485+ String::new()
486+}