Krolyxon
Commit 8756c54

Branches

Tags

Refs →

bench518756c54

Merge pull request #4 from krolyxon/vim

Vim keybindings in the GUI editor (default), Settings menu to switch
Author
K
Date
2026-11-01 10:11 +0530
Committer
GitHub · 2026-11-01 10:11 +0530
Parents
88f3865 4a1855a
Commit
8756c54bff5f3093cd821e7d3120f4b7af7bc768

+3378 −44

13 files changed

  1. M CHANGELOG.md +21 −0
  2. M README.md +33 −5
  3. M crates/bench51-gui/src/app.rs +56 −8
  4. M crates/bench51-gui/src/chrome.rs +26 −12
  5. M crates/bench51-gui/src/editor.rs +301 −16
  6. M crates/bench51-gui/src/main.rs +10 −1
  7. M crates/bench51-gui/src/modals.rs +28 −1
  8. M crates/bench51-gui/src/state.rs +27 −1
  9. A crates/bench51-gui/src/vim/ex.rs +84 −0
  10. A crates/bench51-gui/src/vim/keys.rs +128 −0
  11. A crates/bench51-gui/src/vim/mod.rs +1818 −0
  12. A crates/bench51-gui/src/vim/tests.rs +358 −0
  13. A crates/bench51-gui/src/vim/text.rs +488 −0
M CHANGELOG.md View
@@ -4,6 +4,27 @@ All notable changes to bench51. The format follows
44 [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and versions follow
55 [Semantic Versioning](https://semver.org/).
66
7+## [Unreleased]
8+
9+### Added
10+
11+- Vim keybindings in the GUI editor, on by default: Normal, Insert, Visual
12+ and Visual Line modes, motions with counts, operators with text objects,
13+ registers, undo/redo, `.` repeat, macros, search with highlighting, and
14+ `:w :q :wq :e :make`. A block cursor and a mode line show the state, and
15+ `gd` jumps to a label's definition.
16+- A Settings menu with Keybindings (Vim / Standard) and Theme, and a
17+ `--keymap vim|standard` flag.
18+
19+### Changed
20+
21+- Theme moved from the View menu to Settings.
22+
23+### Fixed
24+
25+- `--theme`, `--keymap` and `--clock` no longer overwrite the saved
26+ settings; they only apply to that run.
27+
728 ## [0.2.0] - 2026-11-01
829
930 ### Added
M README.md View
@@ -65,8 +65,8 @@ Linux, in a window or right in the terminal.
6565 UART #1, Ports, Timers and Interrupts windows. Every window docks and tabs
6666 wherever you drag it; the edit and debug layouts are remembered separately,
6767 as in µVision.
68−- **Editor** with syntax highlighting, undo/redo, find, and breakpoint, error
69− and PC markers in the gutter.
68+- **Editor** with Vim keybindings (or standard ones, in Settings), syntax
69+ highlighting, find, and breakpoint, error and PC markers in the gutter.
7070 - **Two-pass assembler** for the full 8051 instruction set (all 255 opcodes):
7171 - labels, expressions (`+ - * / MOD AND OR XOR NOT SHL SHR HIGH LOW`, `$`)
7272 - number formats `42`, `2Ah`, `0x2A`, `101010b`, `52o`, `'*'`
@@ -132,9 +132,10 @@ bench51-gui firmware.hex # disassemble & debug a HEX file
132132 bench51-gui --theme light
133133 ```
134134
135−`--theme system|dark|light` overrides the theme chosen in View → Theme, and
136−`--clock <MHz>` the Xtal set in Project → Options for Target (default
137−11.0592), which is used for the elapsed-time display.
135+`--theme system|dark|light` and `--keymap vim|standard` override the choices
136+in the Settings menu for one run. `--clock <MHz>` overrides the Xtal set in
137+Project → Options for Target (default 11.0592), which is used for the
138+elapsed-time display.
138139
139140 On Linux it runs on Wayland and X11 and uses the XDG desktop portal for file
140141 dialogs. Building it from source on Debian or Ubuntu needs the packages egui
@@ -164,6 +165,33 @@ change it, click a pin in Ports to hold it low from outside, and click the
164165 bit chips in Registers, Timers and Interrupts to flip SFR bits. Files can be
165166 dropped onto the window.
166167
168+### Editing with Vim keys
169+
170+The editor starts in Vim's Normal mode, with a block cursor and a mode line
171+along its bottom edge:
172+
173+- **Modes:** `i a I A o O` to insert, `v V` for Visual and Visual Line, Esc
174+ to go back.
175+- **Motions:** `h j k l w b e W B E 0 ^ $ gg G f t F T ; , % { } H M L`,
176+ Ctrl-D/U/F/B. All take counts (`5j`, `3w`).
177+- **Operators:** `d c y > <` with any motion or text object (`iw aw i" a"
178+ i( a( ip`), doubled for whole lines (`dd`, `yy`, `>>`). Also
179+ `x X D C s S r J ~ p P`.
180+- **Repeat and undo:** `u` undoes, Ctrl-R redoes, `.` repeats the last
181+ change, and `qa`…`q` / `@a` record and replay a macro.
182+- **Search:** `/ ? n N * #`; `:noh` clears the highlight.
183+- **Assembly helpers:** `gd` jumps to a label's definition, `gt`/`gT`
184+ switch files, and new lines line up with the instruction column after a
185+ label.
186+- **Commands:** `:w`, `:q`, `:wq`, `:e file`, `:42`, and `:make` to build.
187+- **Clipboard:** yanks and deletes also go to the system clipboard, and
188+ Ctrl+V pastes from it.
189+- **Debug sessions:** the editor is read-only, but moving, searching and
190+ yanking still work.
191+
192+Settings → Keybindings → Standard switches to ordinary editing: type to
193+insert, with Ctrl+Z to undo. F1 lists the keys for whichever is active.
194+
167195 ## Terminal version
168196
169197 `bench51` is the same IDE in a terminal, the way `emacs -nw` is Emacs
M crates/bench51-gui/src/app.rs View
@@ -10,9 +10,11 @@ use eframe::egui::{
1010 use egui_tiles::Tree;
1111
1212 use crate::layout::{self, Pane, PaneBehavior};
13−use crate::state::{Action, BuildTab, LogKind, LogLine, Modal, Mode, Prefs, State, ThemePref};
13+use crate::state::{
14+ Action, BuildTab, Keymap, LogKind, LogLine, Modal, Mode, Prefs, State, ThemePref,
15+};
1416 use crate::theme::{self, palette};
15−use crate::{chrome, modals};
17+use crate::{chrome, editor, modals};
1618
1719 /// One simulation chunk, as in the TUI's frame loop.
1820 const TICK: f64 = 1.0 / 30.0;
@@ -43,6 +45,10 @@ pub struct GuiApp {
4345 /// The user confirmed quitting with unsaved changes.
4446 allow_close: bool,
4547 title: String,
48+ /// Prefs as saved before the command line overrode them; those
49+ /// overrides last only for this run.
50+ saved_prefs: Prefs,
51+ cli: Prefs,
4652 }
4753
4854 fn theme_preference(t: ThemePref) -> ThemePreference {
@@ -76,6 +82,7 @@ impl GuiApp {
7682 files: Vec<PathBuf>,
7783 clock: Option<f64>,
7884 theme: Option<ThemePref>,
85+ keymap: Option<Keymap>,
7986 ) -> Self {
8087 let ctx = &cc.egui_ctx;
8188 theme::install(ctx);
@@ -83,16 +90,23 @@ impl GuiApp {
8390 .storage
8491 .and_then(|s| eframe::get_value(s, "prefs"))
8592 .unwrap_or_default();
93+ let saved_prefs = prefs.clone();
8694 if let Some(c) = clock {
8795 prefs.clock_mhz = c;
8896 }
8997 if let Some(t) = theme {
9098 prefs.theme = t;
9199 }
100+ if let Some(k) = keymap {
101+ prefs.keymap = k;
102+ }
92103 prefs.speed = prefs.speed.min(bench51_core::session::SPEEDS.len() - 1);
93104 ctx.set_theme(theme_preference(prefs.theme));
94105 ctx.set_zoom_factor(prefs.zoom.clamp(0.5, 3.0));
106+ let cli = prefs.clone();
95107 let mut app = Self {
108+ saved_prefs,
109+ cli,
96110 st: State::new(prefs),
97111 edit_tree: load_tree(cc, "edit_layout", layout::edit_tree),
98112 debug_tree: load_tree(cc, "debug_layout", layout::debug_tree),
@@ -125,6 +139,12 @@ impl GuiApp {
125139 let none = Modifiers::NONE;
126140 let running = self.st.running();
127141 let active = self.st.active;
142+ // In Vim mode a focused editor keeps Ctrl+F (page down) for itself.
143+ let vim_editor = self.st.prefs.keymap == Keymap::Vim
144+ && self
145+ .st
146+ .doc()
147+ .is_some_and(|d| ctx.memory(|m| m.has_focus(editor::edit_id(d.id))));
128148 // Most specific first: `consume_key` ignores extra Shift.
129149 let table: [(Modifiers, Key, Action); 25] = [
130150 (cmd_shift, Key::S, Action::SaveAs),
@@ -155,6 +175,9 @@ impl GuiApp {
155175 ];
156176 ctx.input_mut(|i| {
157177 for (mods, key, action) in table {
178+ if vim_editor && action == Action::Find {
179+ continue;
180+ }
158181 if i.consume_key(mods, key) {
159182 self.st.actions.push(action);
160183 }
@@ -299,13 +322,16 @@ impl GuiApp {
299322 }
300323 }
301324
302− fn save(&mut self, ask: bool) {
325+ /// Saves the active document: to `to`, to its own path, or wherever the
326+ /// user picks (always when `ask`).
327+ fn save(&mut self, ask: bool, to: Option<PathBuf>) {
303328 let dir = self.start_dir();
304329 let Some(doc) = self.st.doc_mut() else {
305330 return;
306331 };
307− let path = match (&doc.path, ask) {
308− (Some(p), false) => Some(p.clone()),
332+ let path = match (to, &doc.path, ask) {
333+ (Some(p), _, _) => Some(p),
334+ (None, Some(p), false) => Some(p.clone()),
309335 _ => {
310336 let name = doc.name();
311337 let stem = name.rsplit_once('.').map_or(name.as_str(), |(s, _)| s);
@@ -350,6 +376,8 @@ impl GuiApp {
350376 if self.st.active >= i && self.st.active > 0 {
351377 self.st.active -= 1;
352378 }
379+ self.st.vim.abandon();
380+ self.st.focus_editor = true;
353381 if self.st.docs.is_empty() {
354382 self.st.mode = Mode::Edit;
355383 self.st.active = 0;
@@ -368,7 +396,9 @@ impl GuiApp {
368396 if i < self.st.docs.len() && i != self.st.active {
369397 self.pause_active();
370398 self.st.active = i;
399+ self.st.vim.abandon();
371400 }
401+ self.st.focus_editor = true;
372402 }
373403
374404 // ---- Build & debug ----
@@ -461,8 +491,9 @@ impl GuiApp {
461491 }
462492 Action::Open => self.open_dialog(),
463493 Action::OpenPath(p) => self.open_path(&p),
464− Action::Save => self.save(false),
465− Action::SaveAs => self.save(true),
494+ Action::Save => self.save(false, None),
495+ Action::SaveAs => self.save(true, None),
496+ Action::SaveTo(p) => self.save(false, Some(p)),
466497 Action::Close(i) => {
467498 if self.st.docs.get(i).is_some_and(|d| d.dirty()) {
468499 self.st.modal = Some(Modal::ConfirmClose(i));
@@ -554,6 +585,11 @@ impl GuiApp {
554585 self.st.prefs.theme = t;
555586 ctx.set_theme(theme_preference(t));
556587 }
588+ Action::SetKeymap(k) => {
589+ self.st.prefs.keymap = k;
590+ self.st.vim.abandon();
591+ self.st.focus_editor = true;
592+ }
557593 Action::Zoom(d) => ctx.set_zoom_factor((ctx.zoom_factor() + d).clamp(0.5, 3.0)),
558594 Action::ZoomReset => ctx.set_zoom_factor(1.0),
559595 Action::Find => {
@@ -643,7 +679,19 @@ impl eframe::App for GuiApp {
643679 }
644680
645681 fn save(&mut self, storage: &mut dyn eframe::Storage) {
646− eframe::set_value(storage, "prefs", &self.st.prefs);
682+ // A setting still at its command-line value is saved as it was
683+ // before; one changed in the app since is saved as changed.
684+ let mut prefs = self.st.prefs.clone();
685+ if prefs.theme == self.cli.theme {
686+ prefs.theme = self.saved_prefs.theme;
687+ }
688+ if prefs.keymap == self.cli.keymap {
689+ prefs.keymap = self.saved_prefs.keymap;
690+ }
691+ if prefs.clock_mhz == self.cli.clock_mhz {
692+ prefs.clock_mhz = self.saved_prefs.clock_mhz;
693+ }
694+ eframe::set_value(storage, "prefs", &prefs);
647695 eframe::set_value(storage, "edit_layout", &self.edit_tree);
648696 eframe::set_value(storage, "debug_layout", &self.debug_tree);
649697 }
M crates/bench51-gui/src/chrome.rs View
@@ -6,7 +6,7 @@ use eframe::egui::{self, Align, Button, FontId, Layout, RichText, Stroke, Ui};
66
77 use crate::icons::Icon;
88 use crate::layout::Pane;
9−use crate::state::{Action, Modal, Mode, State, ThemePref};
9+use crate::state::{Action, Keymap, Modal, Mode, State, ThemePref};
1010 use crate::theme::{ACCENT, LABEL, LABEL_TRACKING, palette};
1111 use crate::widgets;
1212
@@ -99,17 +99,6 @@ pub fn menu_bar(ui: &mut Ui, st: &mut State) {
9999 }
100100 }
101101 ui.separator();
102− ui.menu_button(title(ui, "Theme"), |ui| {
103− for (t, name) in [
104− (ThemePref::System, "System"),
105− (ThemePref::Dark, "Dark"),
106− (ThemePref::Light, "Light"),
107− ] {
108− if check(ui, name, st.prefs.theme == t) {
109− acts.push(Action::SetTheme(t));
110− }
111− }
112− });
113102 if item(ui, "Zoom in", "Ctrl++") {
114103 acts.push(Action::Zoom(0.1));
115104 }
@@ -185,6 +174,26 @@ pub fn menu_bar(ui: &mut Ui, st: &mut State) {
185174 }
186175 }
187176 });
177+ ui.menu_button(title(ui, "Settings"), |ui| {
178+ ui.menu_button(title(ui, "Keybindings"), |ui| {
179+ for (k, name) in [(Keymap::Vim, "Vim"), (Keymap::Standard, "Standard")] {
180+ if check(ui, name, st.prefs.keymap == k) {
181+ acts.push(Action::SetKeymap(k));
182+ }
183+ }
184+ });
185+ ui.menu_button(title(ui, "Theme"), |ui| {
186+ for (t, name) in [
187+ (ThemePref::System, "System"),
188+ (ThemePref::Dark, "Dark"),
189+ (ThemePref::Light, "Light"),
190+ ] {
191+ if check(ui, name, st.prefs.theme == t) {
192+ acts.push(Action::SetTheme(t));
193+ }
194+ }
195+ });
196+ });
188197 ui.menu_button(title(ui, "Help"), |ui| {
189198 if item(ui, "Keyboard shortcuts", "F1") {
190199 acts.push(Action::Modal(Modal::Shortcuts));
@@ -374,6 +383,11 @@ pub fn status_bar(ui: &mut Ui, st: &State) {
374383 "edit"
375384 };
376385 ui.label(text(mode.into()).color(p.text_disabled));
386+ let keys = match st.prefs.keymap {
387+ Keymap::Vim => "vim",
388+ Keymap::Standard => "std",
389+ };
390+ ui.label(text(keys.into()).color(p.text_disabled));
377391 // Running: a dot that blinks in hard 2 Hz steps.
378392 let (r, _) = ui.allocate_exact_size(egui::vec2(8.0, 8.0), egui::Sense::hover());
379393 let on = !live || (ui.input(|i| i.time) * 4.0) as i64 % 2 == 0;
M crates/bench51-gui/src/editor.rs View
@@ -4,9 +4,12 @@
44 use std::collections::{HashMap, HashSet};
55
66 use bench51_core::syntax::tokens;
7+use std::path::PathBuf;
8+
79 use eframe::egui::{
8− self, Align, Align2, Color32, CursorIcon, FontId, Id, Key, Layout, Modifiers, Rect, RichText,
9− Sense, Shape, Stroke, TextBuffer, TextFormat, Ui, Vec2,
10+ self, Align, Align2, Color32, Context, CursorIcon, Event, EventFilter, FontId, Id, Key, Layout,
11+ Modifiers, Rect, RichText, Sense, Shape, Stroke, StrokeKind, TextBuffer, TextFormat, Ui,
12+ UiBuilder, Vec2, WidgetText,
1013 cache::{ComputerMut, FrameCache},
1114 pos2,
1215 text::{CCursor, CCursorRange, LayoutJob},
@@ -14,8 +17,9 @@ use eframe::egui::{
1417 vec2,
1518 };
1619
17−use crate::state::{Action, Document, Mode, State};
20+use crate::state::{Action, Document, Keymap, Mode, State};
1821 use crate::theme::{self, ACCENT, BODY, DARK, LIGHT, WARNING, palette};
22+use crate::vim::{self, VimKey};
1923 use crate::widgets::{self, PillKind};
2024
2125 const MARGIN: Vec2 = vec2(10.0, 8.0);
@@ -29,7 +33,173 @@ pub fn show(ui: &mut Ui, st: &mut State) {
2933 if st.find.open {
3034 find_bar(ui, st);
3135 }
32− code(ui, st);
36+ if st.prefs.keymap == Keymap::Vim {
37+ let full = ui.available_rect_before_wrap();
38+ let h = 26.0;
39+ let code_rect = Rect::from_min_max(full.min, pos2(full.max.x, full.max.y - h));
40+ let line_rect = Rect::from_min_max(pos2(full.min.x, full.max.y - h), full.max);
41+ ui.scope_builder(UiBuilder::new().max_rect(code_rect), |ui| code(ui, st));
42+ mode_line(ui, line_rect, st);
43+ ui.advance_cursor_after_rect(full);
44+ } else {
45+ code(ui, st);
46+ }
47+}
48+
49+/// Id of a document's text editor widget.
50+pub fn edit_id(doc_id: u64) -> Id {
51+ Id::unique(("editor", doc_id))
52+}
53+
54+/// Vim's mode, recording state, command line or message, and pending keys.
55+fn mode_line(ui: &Ui, rect: Rect, st: &State) {
56+ let p = palette(ui.ctx());
57+ let v = &st.vim;
58+ let painter = ui.painter();
59+ painter.hline(rect.x_range(), rect.top() + 0.5, Stroke::new(1.0, p.border));
60+ let label = |text: &str, color: Color32| {
61+ WidgetText::from(p.label(text).color(color)).into_galley(
62+ ui,
63+ Some(egui::TextWrapMode::Extend),
64+ f32::INFINITY,
65+ egui::TextStyle::Small,
66+ )
67+ };
68+ let cy = rect.center().y;
69+ let mut x = rect.left() + 12.0;
70+ let put = |g: std::sync::Arc<egui::Galley>, x: &mut f32, color: Color32| {
71+ let w = g.size().x;
72+ painter.galley(pos2(*x, cy - g.size().y / 2.0), g, color);
73+ *x += w;
74+ };
75+ let color = if v.mode == vim::Mode::Normal {
76+ p.text_secondary
77+ } else {
78+ p.text_display
79+ };
80+ put(label(v.mode_label(), color), &mut x, color);
81+ x += 18.0;
82+ if let Some(r) = v.recording() {
83+ painter.circle_filled(pos2(x + 3.5, cy), 3.5, ACCENT);
84+ x += 12.0;
85+ put(label(&format!("rec @{r}"), ACCENT), &mut x, ACCENT);
86+ x += 18.0;
87+ }
88+ let font = FontId::monospace(13.0);
89+ if let Some((prompt, input)) = &v.cmdline {
90+ let r = painter.text(
91+ pos2(x, cy),
92+ Align2::LEFT_CENTER,
93+ format!("{prompt}{input}"),
94+ font.clone(),
95+ p.text_display,
96+ );
97+ let cw = ui.fonts_mut(|f| f.glyph_width(&font, '0'));
98+ painter.rect_filled(
99+ Rect::from_center_size(pos2(r.right() + cw / 2.0, cy), vec2(cw, r.height())),
100+ 0.0,
101+ p.text_display,
102+ );
103+ } else if let Some((msg, error)) = &v.message {
104+ let color = if *error { ACCENT } else { p.text_secondary };
105+ painter.text(pos2(x, cy), Align2::LEFT_CENTER, msg, font.clone(), color);
106+ }
107+ painter.text(
108+ pos2(rect.right() - 12.0, cy),
109+ Align2::RIGHT_CENTER,
110+ v.pending_label(),
111+ font,
112+ p.text_display,
113+ );
114+}
115+
116+/// Where the view goes after Vim handled this frame's keys.
117+enum VimScroll {
118+ /// Just keep the cursor visible.
119+ Cursor,
120+ Align(vim::Align),
121+ Lines(isize),
122+}
123+
124+/// Keyboard input for the focused editor (Ctrl+Tab stays with the panes).
125+fn is_keyboard(e: &Event) -> bool {
126+ match e {
127+ Event::Key { key, modifiers, .. } => !(modifiers.ctrl && *key == Key::Tab),
128+ Event::Text(_) | Event::Paste(_) | Event::Copy | Event::Cut | Event::Ime(_) => true,
129+ _ => false,
130+ }
131+}
132+
133+/// A path typed in `:w` or `:e`, relative to the document's folder.
134+fn resolve(doc: &Document, path: &str) -> PathBuf {
135+ let p = PathBuf::from(path);
136+ match doc.path.as_ref().and_then(|d| d.parent()) {
137+ Some(dir) if p.is_relative() => dir.join(p),
138+ _ => p,
139+ }
140+}
141+
142+/// Takes this frame's keys away from the TextEdit, feeds them to Vim and
143+/// turns its effects into app actions.
144+fn drive_vim(ctx: &Context, st: &mut State, debug: bool) -> Option<VimScroll> {
145+ let events: Vec<Event> = ctx.input_mut(|i| {
146+ let (keys, rest) = std::mem::take(&mut i.events)
147+ .into_iter()
148+ .partition(is_keyboard);
149+ i.events = rest;
150+ keys
151+ });
152+ let mut keys: Vec<VimKey> = events.iter().flat_map(vim::from_egui).collect();
153+ if debug && st.vim.mode == vim::Mode::Insert {
154+ keys.insert(0, VimKey::Esc);
155+ }
156+ if keys.is_empty() {
157+ return None;
158+ }
159+ let n_docs = st.docs.len();
160+ let State {
161+ docs,
162+ active,
163+ vim,
164+ actions,
165+ vim_view,
166+ ..
167+ } = st;
168+ let active = *active;
169+ let doc = docs.get_mut(active)?;
170+ let mut scroll = VimScroll::Cursor;
171+ for k in keys {
172+ let dirty = doc.dirty();
173+ let mut cx = vim::Cx {
174+ text: &mut doc.text,
175+ cursor: &mut doc.cursor_char,
176+ history: &mut doc.history,
177+ readonly: debug,
178+ dirty,
179+ view: *vim_view,
180+ };
181+ for fx in vim.key(k, &mut cx) {
182+ use vim::Effect;
183+ match fx {
184+ Effect::Clipboard(t) => ctx.copy_text(t),
185+ Effect::Save => actions.push(Action::Save),
186+ Effect::SaveAs(p) => actions.push(Action::SaveTo(resolve(doc, &p))),
187+ Effect::Close { force: true } => actions.push(Action::ForceClose(active)),
188+ Effect::Close { force: false } => actions.push(Action::Close(active)),
189+ Effect::Quit { force: true } => actions.push(Action::ForceQuit),
190+ Effect::Quit { force: false } => actions.push(Action::Quit),
191+ Effect::Edit(p) => actions.push(Action::OpenPath(resolve(doc, &p))),
192+ Effect::Make => actions.push(Action::Build),
193+ Effect::NextDoc => actions.push(Action::Activate((active + 1) % n_docs)),
194+ Effect::PrevDoc => actions.push(Action::Activate((active + n_docs - 1) % n_docs)),
195+ Effect::Scroll(a) => scroll = VimScroll::Align(a),
196+ Effect::ScrollLines(n) => scroll = VimScroll::Lines(n),
197+ }
198+ }
199+ }
200+ doc.cursor_char = doc.cursor_char.min(doc.text.chars().count());
201+ doc.cursor = line_col(&doc.text, doc.cursor_char);
202+ Some(scroll)
33203 }
34204
35205 // ------------------------------------------------------------- tabs, find
@@ -169,6 +339,7 @@ fn find_bar(ui: &mut Ui, st: &mut State) {
169339 }
170340 if close {
171341 st.find.open = false;
342+ st.focus_editor = true;
172343 }
173344 }
174345
@@ -373,12 +544,34 @@ struct Marks {
373544
374545 fn code(ui: &mut Ui, st: &mut State) {
375546 let debug = st.mode == Mode::Debug;
547+ let vim_on = st.prefs.keymap == Keymap::Vim;
548+ let ctx = ui.ctx().clone();
549+ let Some(doc_id) = st.doc().map(|d| d.id) else {
550+ return;
551+ };
552+ let edit_id = edit_id(doc_id);
553+ if std::mem::take(&mut st.focus_editor) && !st.find.open {
554+ ctx.memory_mut(|m| m.request_focus(edit_id));
555+ }
556+ let focused = ctx.memory(|m| m.has_focus(edit_id));
557+ let vim_scroll = if vim_on && focused {
558+ drive_vim(&ctx, st, debug)
559+ } else {
560+ None
561+ };
376562 let query = if st.find.open {
377563 st.find.query.clone()
378564 } else {
379565 String::new()
380566 };
381− let Some(doc) = st.docs.get_mut(st.active) else {
567+ let State {
568+ docs,
569+ active,
570+ vim,
571+ vim_view,
572+ ..
573+ } = st;
574+ let Some(doc) = docs.get_mut(*active) else {
382575 return;
383576 };
384577 let p = palette(ui.ctx());
@@ -421,13 +614,8 @@ fn code(ui: &mut Ui, st: &mut State) {
421614 0.0
422615 };
423616 let gutter_w = bp_w + num_w + addr_w + 16.0;
424− let edit_id = Id::unique(("editor", doc.id));
425− let ctx = ui.ctx().clone();
426617
427− if !debug
428− && ctx.memory(|m| m.has_focus(edit_id))
429− && ctx.input_mut(|i| i.consume_key(Modifiers::NONE, Key::Tab))
430− {
618+ if !vim_on && !debug && focused && ctx.input_mut(|i| i.consume_key(Modifiers::NONE, Key::Tab)) {
431619 insert_tab(&ctx, edit_id, &mut doc.text);
432620 }
433621
@@ -459,9 +647,30 @@ fn code(ui: &mut Ui, st: &mut State) {
459647 }
460648 let hit = doc.find_hit;
461649
462− let matches = find_all(&doc.text, &query);
650+ // Vim owns the cursor: show it, or the Visual selection, in the
651+ // TextEdit, except while the mouse is dragging out a selection.
652+ let block = vim_on && vim.block_cursor();
653+ let dragging = ctx.dragged_id() == Some(edit_id);
654+ if vim_on && !dragging {
655+ let mut state = TextEditState::load(&ctx, edit_id).unwrap_or_default();
656+ let range = match vim.selection(&doc.text, doc.cursor_char) {
657+ Some((a, b)) => CCursorRange::two(CCursor::new(a), CCursor::new(b)),
658+ None => CCursorRange::one(CCursor::new(doc.cursor_char)),
659+ };
660+ state.cursor.set_char_range(Some(range));
661+ state.store(&ctx, edit_id);
662+ }
663+
664+ let matches = if !query.is_empty() {
665+ find_all(&doc.text, &query)
666+ } else if vim_on {
667+ vim.search_ranges(&doc.text)
668+ } else {
669+ Vec::new()
670+ };
463671 let mut clicked_line = None;
464672 let mut cursor = None;
673+ let mut mouse = None;
465674
466675 egui::ScrollArea::both()
467676 .id_salt(("code", doc.id))
@@ -492,7 +701,10 @@ fn code(ui: &mut Ui, st: &mut State) {
492701 } else {
493702 &mut doc.text
494703 };
495− let out = egui::TextEdit::multiline(buf)
704+ if block {
705+ ui.visuals_mut().text_cursor.stroke = Stroke::NONE;
706+ }
707+ let mut edit = egui::TextEdit::multiline(buf)
496708 .id(edit_id)
497709 .font(font.clone())
498710 .frame(egui::Frame::NONE)
@@ -500,9 +712,30 @@ fn code(ui: &mut Ui, st: &mut State) {
500712 .desired_width(f32::INFINITY)
501713 .desired_rows(1)
502714 .lock_focus(true)
503− .layouter(&mut layouter)
504− .show(ui);
715+ .layouter(&mut layouter);
716+ if vim_on {
717+ // Esc, Tab and the arrows belong to Vim, not to egui's
718+ // focus navigation.
719+ edit = edit.event_filter(EventFilter {
720+ tab: true,
721+ horizontal_arrows: true,
722+ vertical_arrows: true,
723+ escape: true,
724+ });
725+ }
726+ let out = edit.show(ui);
505727 cursor = out.cursor_range.map(|r| r.primary.index.0);
728+ let resp = &out.response.response;
729+ if vim_on
730+ && (resp.clicked()
731+ || resp.dragged()
732+ || resp.drag_stopped()
733+ || resp.double_clicked())
734+ {
735+ mouse = out
736+ .cursor_range
737+ .map(|r| (r.secondary.index.0, r.primary.index.0));
738+ }
506739
507740 let galley = &out.galley;
508741 let origin = out.galley_pos;
@@ -609,6 +842,53 @@ fn code(ui: &mut Ui, st: &mut State) {
609842 gutter_resp.on_hover_text(msg);
610843 }
611844
845+ // Vim: block cursor, visible lines, scrolling.
846+ if vim_on {
847+ let n_chars = galley.text().chars().count();
848+ let ci = doc.cursor_char.min(n_chars);
849+ let at = galley
850+ .pos_from_cursor(CCursor::new(ci))
851+ .translate(origin.to_vec2());
852+ let cell = Rect::from_min_size(at.min, vec2(char_w, row_h));
853+ if block {
854+ let ch = galley.text().chars().nth(ci).filter(|c| !c.is_whitespace());
855+ if focused {
856+ painter.rect_filled(cell, 0.0, p.text_display);
857+ if let Some(ch) = ch {
858+ painter.text(cell.min, Align2::LEFT_TOP, ch, font.clone(), p.black);
859+ }
860+ } else {
861+ painter.rect_stroke(
862+ cell,
863+ 0.0,
864+ Stroke::new(1.0, p.text_secondary),
865+ StrokeKind::Inside,
866+ );
867+ }
868+ }
869+ *vim_view = vim::View {
870+ top: first,
871+ rows: (clip.height() / row_h).floor().max(1.0) as usize,
872+ };
873+ match vim_scroll {
874+ Some(VimScroll::Cursor) => {
875+ ui.scroll_to_rect(cell.expand2(vec2(char_w * 4.0, row_h)), None)
876+ }
877+ Some(VimScroll::Align(a)) => ui.scroll_to_rect(
878+ row_rect(doc.cursor.0),
879+ Some(match a {
880+ vim::Align::Top => Align::Min,
881+ vim::Align::Center => Align::Center,
882+ vim::Align::Bottom => Align::Max,
883+ }),
884+ ),
885+ Some(VimScroll::Lines(n)) => {
886+ ui.scroll_with_delta(vec2(0.0, -(n as f32) * row_h));
887+ }
888+ None => {}
889+ }
890+ }
891+
612892 if let Some(line) = follow {
613893 ui.scroll_to_rect(row_rect(line), Some(Align::Center));
614894 }
@@ -621,7 +901,12 @@ fn code(ui: &mut Ui, st: &mut State) {
621901 });
622902 });
623903
624− if let Some(ci) = cursor {
904+ if vim_on {
905+ if let Some((tail, head)) = mouse {
906+ vim.mouse(tail, head, &doc.text, &mut doc.cursor_char);
907+ doc.cursor = line_col(&doc.text, doc.cursor_char);
908+ }
909+ } else if let Some(ci) = cursor {
625910 doc.cursor_char = ci;
626911 doc.cursor = line_col(&doc.text, ci);
627912 }
M crates/bench51-gui/src/main.rs View
@@ -10,6 +10,7 @@ mod modals;
1010 mod panes;
1111 mod state;
1212 mod theme;
13+mod vim;
1314 mod widgets;
1415
1516 use std::path::PathBuf;
@@ -32,6 +33,10 @@ struct Args {
3233 /// Colour theme, overriding the saved choice.
3334 #[arg(long, value_enum)]
3435 theme: Option<state::ThemePref>,
36+
37+ /// Editor keybindings, overriding the saved choice.
38+ #[arg(long, value_enum)]
39+ keymap: Option<state::Keymap>,
3540 }
3641
3742 fn main() -> eframe::Result {
@@ -50,7 +55,11 @@ fn main() -> eframe::Result {
5055 options,
5156 Box::new(move |cc| {
5257 Ok(Box::new(app::GuiApp::new(
53− cc, args.files, args.clock, args.theme,
58+ cc,
59+ args.files,
60+ args.clock,
61+ args.theme,
62+ args.keymap,
5463 )))
5564 }),
5665 )
M crates/bench51-gui/src/modals.rs View
@@ -8,6 +8,30 @@ use crate::state::{Action, Modal, State};
88 use crate::theme::{BODY, HERO, palette};
99 use crate::widgets::{self, PillKind};
1010
11+/// Shown first while the editor uses Vim keys.
12+const VIM: (&str, &[(&str, &str)]) = (
13+ "Editor (Vim)",
14+ &[
15+ ("i a I A o O", "insert · append · open a line"),
16+ ("Esc", "back to Normal mode"),
17+ ("h j k l w b e", "move; counts work: 5j, 3w"),
18+ ("0 ^ $ gg G :42", "line start / end, top, bottom, line 42"),
19+ ("f t % { }", "find char, matching bracket, paragraph"),
20+ ("d c y > < + motion", "delete · change · yank · indent"),
21+ ("dd yy cc ciw ci\"", "lines, words, inside quotes"),
22+ ("x p P J ~ r", "delete char, put, join, case, replace"),
23+ ("u Ctrl-R .", "undo · redo · repeat"),
24+ ("v V", "visual · visual line"),
25+ ("/ ? n N * #", "search; :noh clears the highlight"),
26+ ("gd", "go to the label's definition"),
27+ ("qa … q @a", "record and play a macro"),
28+ (":w :q :wq :e :make", "write · close · open · build"),
29+ ("gt gT", "next / previous file"),
30+ ("Ctrl+V", "paste from the clipboard (y and d copy to it)"),
31+ ("Settings → Keybindings", "switch to standard keys"),
32+ ],
33+);
34+
1135 const SHORTCUTS: &[(&str, &[(&str, &str)])] = &[
1236 (
1337 "File",
@@ -160,7 +184,10 @@ pub fn show(ctx: &Context, st: &mut State) {
160184 egui::ScrollArea::vertical()
161185 .max_height(420.0)
162186 .show(ui, |ui| {
163− for (section, keys) in SHORTCUTS {
187+ let vim_on = st.prefs.keymap == crate::state::Keymap::Vim;
188+ let sections =
189+ vim_on.then_some(&VIM).into_iter().chain(SHORTCUTS.iter());
190+ for (section, keys) in sections {
164191 ui.label(p.label(section).color(p.text_disabled));
165192 ui.add_space(4.0);
166193 egui::Grid::new(section)
M crates/bench51-gui/src/state.rs View
@@ -6,6 +6,7 @@ use bench51_core::session::{MemSpace, Session};
66 use serde::{Deserialize, Serialize};
77
88 use crate::layout::Pane;
9+use crate::vim::{self, Vim};
910
1011 /// Keil's two window layouts: building and debugging.
1112 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -14,6 +15,13 @@ pub enum Mode {
1415 Debug,
1516 }
1617
18+/// How the code editor takes keys.
19+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
20+pub enum Keymap {
21+ Vim,
22+ Standard,
23+}
24+
1725 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
1826 pub enum ThemePref {
1927 System,
@@ -22,10 +30,11 @@ pub enum ThemePref {
2230 }
2331
2432 /// Settings that survive restarts.
25−#[derive(Serialize, Deserialize)]
33+#[derive(Clone, Serialize, Deserialize)]
2634 #[serde(default)]
2735 pub struct Prefs {
2836 pub theme: ThemePref,
37+ pub keymap: Keymap,
2938 pub recent: Vec<PathBuf>,
3039 pub speed: usize,
3140 pub clock_mhz: f64,
@@ -36,6 +45,7 @@ impl Default for Prefs {
3645 fn default() -> Self {
3746 Self {
3847 theme: ThemePref::System,
48+ keymap: Keymap::Vim,
3949 recent: Vec::new(),
4050 speed: 5,
4151 clock_mhz: 11.0592,
@@ -76,6 +86,8 @@ pub struct Document {
7686 pub find_hit: Option<(usize, usize)>,
7787 /// Scroll the editor and disassembly to the PC on the next frame.
7888 pub follow_pc: bool,
89+ /// Vim undo history.
90+ pub history: vim::History,
7991 }
8092
8193 impl Document {
@@ -94,6 +106,7 @@ impl Document {
94106 select: None,
95107 find_hit: None,
96108 follow_pc: false,
109+ history: vim::History::default(),
97110 }
98111 }
99112
@@ -154,6 +167,8 @@ pub enum Action {
154167 OpenPath(PathBuf),
155168 Save,
156169 SaveAs,
170+ /// Save the active document to this path (`:w file`).
171+ SaveTo(PathBuf),
157172 /// Close a document, asking first if it has unsaved changes.
158173 Close(usize),
159174 /// Close without asking.
@@ -182,6 +197,7 @@ pub enum Action {
182197 ShowPane(Pane),
183198 ResetLayout,
184199 SetTheme(ThemePref),
200+ SetKeymap(Keymap),
185201 Zoom(f32),
186202 ZoomReset,
187203 Find,
@@ -228,6 +244,11 @@ pub struct State {
228244 pub build_tab: BuildTab,
229245 pub serial_input: String,
230246 pub find: FindBar,
247+ pub vim: Vim,
248+ /// Lines visible in the editor last frame, for Vim's `H M L` and paging.
249+ pub vim_view: vim::View,
250+ /// Give the editor keyboard focus on the next frame.
251+ pub focus_editor: bool,
231252 }
232253
233254 impl State {
@@ -254,6 +275,9 @@ impl State {
254275 query: String::new(),
255276 focus: false,
256277 },
278+ vim: Vim::default(),
279+ vim_view: vim::View::default(),
280+ focus_editor: false,
257281 }
258282 }
259283
@@ -272,6 +296,8 @@ impl State {
272296 doc.session.speed = self.prefs.speed;
273297 self.docs.push(doc);
274298 self.active = self.docs.len() - 1;
299+ self.vim.abandon();
300+ self.focus_editor = true;
275301 self.docs.last_mut().unwrap()
276302 }
277303
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,488 @@
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+ .as_chunks::<2>()
386+ .0
387+ .iter()
388+ .find(|[_, close]| pos <= *close)
389+ .map(|&[open, close]| (open, close))?;
390+ let (open, close) = pair;
391+ if around {
392+ let mut b = close + 1;
393+ while b < e && is_blank(self.c[b]) {
394+ b += 1;
395+ }
396+ Some((open, b, false))
397+ } else {
398+ Some((open + 1, close, false))
399+ }
400+ }
401+
402+ pub fn obj_paren(
403+ &self,
404+ pos: usize,
405+ open: char,
406+ close: char,
407+ around: bool,
408+ ) -> Option<(usize, usize, bool)> {
409+ let mut depth = 0usize;
410+ let mut o = None;
411+ let start = if pos < self.len() && self.c[pos] == close {
412+ pos.checked_sub(1)?
413+ } else {
414+ pos
415+ };
416+ for i in (0..=start.min(self.len().saturating_sub(1))).rev() {
417+ if self.c[i] == close && i != pos {
418+ depth += 1;
419+ } else if self.c[i] == open {
420+ if depth == 0 {
421+ o = Some(i);
422+ break;
423+ }
424+ depth -= 1;
425+ }
426+ }
427+ let o = o?;
428+ let mut depth = 0usize;
429+ let c = (o + 1..self.len()).find(|&i| {
430+ if self.c[i] == open {
431+ depth += 1;
432+ } else if self.c[i] == close {
433+ if depth == 0 {
434+ return true;
435+ }
436+ depth -= 1;
437+ }
438+ false
439+ })?;
440+ Some(if around {
441+ (o, c + 1, false)
442+ } else {
443+ (o + 1, c, false)
444+ })
445+ }
446+
447+ pub fn obj_paragraph(&self, pos: usize, around: bool) -> Option<(usize, usize, bool)> {
448+ let last = self.lines() - 1;
449+ let l = self.line_of(pos);
450+ let blank = self.line_is_blank(l);
451+ let mut a = l;
452+ while a > 0 && self.line_is_blank(a - 1) == blank {
453+ a -= 1;
454+ }
455+ let mut b = l;
456+ while b < last && self.line_is_blank(b + 1) == blank {
457+ b += 1;
458+ }
459+ if around {
460+ while b < last && self.line_is_blank(b + 1) != blank {
461+ b += 1;
462+ }
463+ }
464+ Some((self.start_of_line(a), self.start_of_line(b), true))
465+ }
466+}
467+
468+/// Indentation for a new line after `line`: its leading whitespace, or for
469+/// a `label:` line, the column where its instruction starts (8 if bare).
470+pub fn auto_indent(line: &str) -> String {
471+ let lead: String = line.chars().take_while(|&c| is_blank(c)).collect();
472+ if !lead.is_empty() || line.trim().is_empty() {
473+ return lead;
474+ }
475+ let label_end = line
476+ .char_indices()
477+ .find(|&(_, c)| !(is_keyword(c) || c == '?'))
478+ .map_or(line.len(), |(i, _)| i);
479+ if label_end > 0 && line[label_end..].starts_with(':') {
480+ let rest = &line[label_end + 1..];
481+ if rest.trim().is_empty() || rest.trim_start().starts_with(';') {
482+ return " ".repeat(8);
483+ }
484+ let pad = rest.chars().take_while(|&c| is_blank(c)).count();
485+ return " ".repeat(line[..label_end].chars().count() + 1 + pad);
486+ }
487+ String::new()
488+}