1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
//! Key handling per mode: which keys reach the vim engine, which nm keeps
//! from it, and the nm commands the engine does not have.
//!
//! The engine (behind `editor.rs`) does the vim work. What lives here:
//! - the register rule: only a yank reaches the system
//! clipboard, and `p` falls back to the clipboard when the register is
//! empty;
//! - `yr`, which yanks the line's formatted result;
//! - the status messages shown after a yank and at the ends of the
//! undo history;
//! - insert-mode arrows that wrap across lines, `$` that sticks to the
//! line end on `j`/`k`;
//! - `:` command mode, `?` help, `q`, `ZZ` and `ZQ` (the commands
//! themselves are in `commands.rs`);
//! - the completion keys `Tab`, `Ctrl+N`, `Ctrl+P` (the completion
//! itself is in `completion.rs`);
//! - engine prompts nm has no UI for (`/`, `?`, `q` macros).
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use super::app::App;
use super::editor::Mode;
use crate::eval::format_number;
/// Shorthand for the bare character of a key with no Ctrl or Alt.
fn plain_char(key: KeyEvent) -> Option<char> {
let mods = key.modifiers - KeyModifiers::SHIFT;
match key.code {
KeyCode::Char(c) if mods.is_empty() => Some(c),
_ => None,
}
}
/// What nm remembers about `f F t T ; ,` (see [`App::send_vim_key`]).
#[derive(Debug, Default)]
pub(super) struct Finds {
/// The next key is the character a find looks for; true when the
/// find goes forward (`f`, `t`).
waiting: Option<bool>,
/// The direction of the last completed find, for `;` and `,`.
last_forward: Option<bool>,
/// The last key was `i` or `a` after an operator: the next key names
/// a text object, not a motion.
text_object: bool,
}
fn plural(n: usize, one: &str, many: &str) -> String {
if n == 1 {
format!("{n} {one}")
} else {
format!("{n} {many}")
}
}
impl App {
/// Handles an insert-mode key. Returns false for the completion keys,
/// which must not recompute the completion they act on.
pub(super) fn insert_key(&mut self, key: KeyEvent) -> bool {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
let (row, col) = self.cursor();
match key.code {
// Completion keys. The engine never sees these: Tab would
// insert a tab, and Ctrl+N/Ctrl+P mean other things to it.
KeyCode::Char('n') if ctrl => {
self.cycle_completion(1);
return false;
}
KeyCode::Char('p') if ctrl => {
self.cycle_completion(-1);
return false;
}
KeyCode::Tab => {
self.accept_completion();
return false;
}
// Left at column 0 goes to the end of the
// previous line, Right at the end goes to the start of the
// next. Vim (and hjkl) stop instead, so send the engine keys
// that do the same thing through its own insert motions.
KeyCode::Left if col == 0 && row > 0 => {
self.editor.send(KeyEvent::from(KeyCode::Up));
self.editor.send(KeyEvent::from(KeyCode::End));
}
KeyCode::Right
if col >= self.editor.line_len(row) && row + 1 < self.editor.line_count() =>
{
self.editor.send(KeyEvent::from(KeyCode::Down));
self.editor.send(KeyEvent::from(KeyCode::Home));
}
_ => {
self.editor.send(key);
}
}
true
}
pub(super) fn normal_key(&mut self, key: KeyEvent) {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
let motion_next = self.finds.waiting.is_none() && !self.finds.text_object;
if self.editor.pending_op() == Some('y') && motion_next && plain_char(key) == Some('r') {
// `yr`: nm's own command. Drop the engine's pending `y`.
self.editor.send(KeyEvent::from(KeyCode::Esc));
self.yank_result();
return;
}
if std::mem::take(&mut self.pending_z) {
match plain_char(key) {
Some('Z') => self.exit(None),
Some('Q') => self.quit(true),
// Vim drops an unknown `Z` command.
_ => {}
}
return;
}
if self.editor.is_chord_pending() {
self.send_vim_key(key);
return;
}
// The keys nm handles itself ignore a count; Esc drops
// the engine's half-typed count so it does not apply to the next
// motion. hjkl would start a macro on `q`, a search prompt on `?`
// (nm has no UI for one) and nothing useful on `:` or `Z`.
match key.code {
KeyCode::Char('q') if !ctrl => self.drop_count().quit(false),
KeyCode::Char(':') => self.drop_count().command = Some(String::new()),
KeyCode::Char('?') => self.drop_count().help = true,
KeyCode::Char('Z') if !ctrl => self.drop_count().pending_z = true,
KeyCode::Char('/') => {}
KeyCode::Char('u') if !ctrl => {
if !self.editor.can_undo() {
self.status_msg = Some("Already at oldest change".into());
}
self.editor.send(key);
}
KeyCode::Char('r') if ctrl => {
if !self.editor.can_redo() {
self.status_msg = Some("Already at newest change".into());
}
self.editor.send(key);
}
KeyCode::Char('p' | 'P') if !ctrl => {
if self.fill_register_from_clipboard() {
self.editor.send(key);
} else {
self.drop_count().nothing_to_paste();
}
}
KeyCode::Char('$') | KeyCode::End => {
self.editor.send(key);
self.editor.stick_to_line_end();
}
_ => self.send_vim_key(key),
}
}
/// Drops a count typed before a key nm handles itself.
fn drop_count(&mut self) -> &mut Self {
self.editor.send(KeyEvent::from(KeyCode::Esc));
self
}
pub(super) fn visual_key(&mut self, key: KeyEvent) {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
if self.editor.is_chord_pending() {
self.send_vim_key(key);
return;
}
match (key.code, self.mode()) {
// Search prompt and macro recording: nm has neither.
(KeyCode::Char('/' | '?' | 'q'), _) => {}
// Vim: `v` in charwise visual and `V` in linewise visual
// leave it. hjkl stays.
(KeyCode::Char('v'), Mode::Visual) | (KeyCode::Char('V'), Mode::VisualLine)
if !ctrl =>
{
self.editor.send(KeyEvent::from(KeyCode::Esc));
}
// With nothing to paste, vim refuses and keeps the selection;
// hjkl would delete it.
(KeyCode::Char('p' | 'P'), _) if !ctrl => {
if self.fill_register_from_clipboard() {
self.editor.send(key);
} else {
self.nothing_to_paste();
}
}
_ => self.send_vim_key(key),
}
}
/// Sends a normal- or visual-mode key to the engine, keeping track of
/// `f F t T ; ,` so an operator with a backward find can be fixed.
///
/// hjkl treats every find as inclusive, so `dF(` and `dT(` also
/// delete the character under the cursor. In vim (and in nm) `F`
/// and `T` are exclusive. nm lets the engine run the
/// command, then redoes it over the right range.
fn send_vim_key(&mut self, key: KeyEvent) {
let c = plain_char(key);
let op = self.editor.pending_op();
let free = !self.editor.is_chord_pending();
let waiting = self.finds.waiting.take();
let after_object_key = std::mem::take(&mut self.finds.text_object);
let mut backward = false;
if let Some(forward) = waiting {
// This key is the character the find looks for.
if c.is_some() {
self.finds.last_forward = Some(forward);
backward = !forward;
}
} else if free || (op.is_some() && !after_object_key) {
match c {
Some(ch @ ('f' | 'F' | 't' | 'T')) => {
self.finds.waiting = Some(matches!(ch, 'f' | 't'));
}
Some(ch @ (';' | ',')) if op.is_some() => {
if let Some(forward) = self.finds.last_forward {
backward = forward == (ch == ',');
}
}
Some('i' | 'a') if op.is_some() => self.finds.text_object = true,
_ => {}
}
}
match op {
Some(op @ ('d' | 'c' | 'y')) if backward => self.backward_find_op(op, key),
_ => self.send_tracking_yank(key),
}
}
/// Runs the key that completes `dF{c}`, `cT{c}`, `y,` and the like,
/// then corrects the range to stop before the cursor character.
fn backward_find_op(&mut self, op: char, key: KeyEvent) {
let (row, col) = self.cursor();
if !self.editor.send(key) {
// Not found: the engine did nothing.
return;
}
// The engine took `target..=col`; its register says how much.
// (The cursor cannot tell: it is clamped when the line got
// shorter.)
let (taken, _) = self.editor.register();
let n = taken.chars().count();
// One character means the range was empty; hjkl already treats `T`
// next to its target as not found, so this does not happen.
if taken.contains('\n') || n < 2 || n > col + 1 {
return;
}
let target = col + 1 - n;
let text: String = taken.chars().take(n - 1).collect();
if op == 'y' {
self.editor.set_register(text.clone(), false);
self.yank_to_clipboard(&text, false);
return;
}
if op == 'c' {
self.editor.send(KeyEvent::from(KeyCode::Esc));
}
// Take the engine's edit back and redo it on a visual selection
// from just before the old cursor back to the target. Selecting
// backwards leaves the cursor at the target, which is where undo
// will put it back.
self.editor.send(KeyEvent::from(KeyCode::Char('u')));
self.editor.set_cursor(row, col - 1);
self.editor.send(KeyEvent::from(KeyCode::Char('v')));
self.editor.set_cursor(row, target);
self.editor.send(KeyEvent::from(KeyCode::Char(op)));
}
/// Sends a normal- or visual-mode key and, if it was a yank, copies
/// the register to the clipboard and says so in the status bar.
///
/// A yank is the key that completes a pending `y` (`yy`, `y$`, `yG`),
/// `Y`, or `y`/`Y` in visual mode. hjkl reports deletes through the
/// same hook, so the key decides, not the hook.
fn send_tracking_yank(&mut self, key: KeyEvent) {
let c = plain_char(key);
let yank_key = match self.mode() {
Mode::Normal => self.editor.pending_op() == Some('y') || c == Some('Y'),
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => matches!(c, Some('y' | 'Y')),
Mode::Insert => false,
};
let stored = self.editor.send(key);
if yank_key && stored {
let (text, linewise) = self.editor.register();
self.yank_to_clipboard(&text, linewise);
}
}
/// `yr`: yank the formatted result of the cursor line.
fn yank_result(&mut self) {
let row = self.cursor().0;
let value = self
.results
.get(row)
.filter(|r| r.error.is_none())
.and_then(|r| r.value);
match value {
Some(v) => {
let text = format_number(v);
self.editor.set_register(text.clone(), false);
self.status_msg = Some("Yanked result".into());
self.write_clipboard(&text);
}
None => self.status_msg = Some("No result to yank".into()),
}
}
/// Writes a yank to the clipboard and reports it: `Yanked 3 lines`,
/// `Yanked 5 characters`. Linewise text goes out without its final
/// newline.
fn yank_to_clipboard(&mut self, text: &str, linewise: bool) {
let (out, msg) = if linewise {
let body = text.strip_suffix('\n').unwrap_or(text);
let n = body.split('\n').count();
(body, format!("Yanked {}", plural(n, "line", "lines")))
} else {
let n = text.chars().count();
(
text,
format!("Yanked {}", plural(n, "character", "characters")),
)
};
self.status_msg = Some(msg);
self.write_clipboard(out);
}
fn write_clipboard(&mut self, text: &str) {
if let Err(e) = self.editor.clipboard().set(text) {
self.status_msg = Some(format!("Clipboard error: {e}"));
}
}
/// Vim's refusal of `p`/`P` when there is nothing to paste.
fn nothing_to_paste(&mut self) {
self.status_msg = Some("E353: Nothing in register \"".into());
}
/// Before `p`/`P`: an empty register is filled from the clipboard.
/// Text with a newline in it pastes as whole lines.
/// Returns false when there is still nothing to paste.
fn fill_register_from_clipboard(&mut self) -> bool {
if !self.editor.register().0.is_empty() {
return true;
}
let Some(text) = self.editor.clipboard().get() else {
return false;
};
if text.is_empty() {
return false;
}
if text.contains('\n') {
let mut text = text;
if !text.ends_with('\n') {
text.push('\n');
}
self.editor.import_register(text, true);
} else {
self.editor.import_register(text, false);
}
true
}
}