@@ -0,0 +1,1158 @@
1+//! The built-in editor (§10.6): one text-editing core over the tagged
2+//! buffer, and three keymaps on top of it — a conventional one in the manner
3+//! of micro, Vim, and Helix.
4+//!
5+//! Every change goes through `insert` and `delete`, which use the buffer's
6+//! own line operations, so each line keeps (or inherits) its owning block and
7+//! the blocks it touches become dirty (§5.2).
8+
9+mod helix;
10+mod normal;
11+mod vim;
12+
13+use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
14+use fold_core::edit::{EditBuffer, EditLine, Owner};
15+use std::collections::BTreeSet;
16+
17+/// A position: line, and column in characters.
18+#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord, Default)]
19+pub struct Pos {
20+ pub line: usize,
21+ pub col: usize,
22+}
23+
24+impl Pos {
25+ pub fn new(line: usize, col: usize) -> Pos {
26+ Pos { line, col }
27+ }
28+}
29+
30+/// Which keymap the editor speaks.
31+#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
32+pub enum Keys {
33+ #[default]
34+ Normal,
35+ Vim,
36+ Helix,
37+}
38+
39+impl Keys {
40+ pub fn parse(s: &str) -> Option<Keys> {
41+ match s.trim().to_ascii_lowercase().as_str() {
42+ "normal" | "default" | "micro" | "standard" => Some(Keys::Normal),
43+ "vim" | "vi" | "nvim" | "neovim" => Some(Keys::Vim),
44+ "helix" | "hx" | "kakoune" | "kak" => Some(Keys::Helix),
45+ _ => None,
46+ }
47+ }
48+
49+ pub fn name(self) -> &'static str {
50+ match self {
51+ Keys::Normal => "normal",
52+ Keys::Vim => "vim",
53+ Keys::Helix => "helix",
54+ }
55+ }
56+
57+ pub fn next(self) -> Keys {
58+ match self {
59+ Keys::Normal => Keys::Vim,
60+ Keys::Vim => Keys::Helix,
61+ Keys::Helix => Keys::Normal,
62+ }
63+ }
64+}
65+
66+/// The editor's own mode (Vim's and Helix's); the normal keymap is always
67+/// `Insert`.
68+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
69+pub enum Mode {
70+ Insert,
71+ Normal,
72+ /// Vim visual, charwise or linewise.
73+ Visual { line: bool },
74+ /// Helix select mode: motions extend the selection.
75+ Select,
76+}
77+
78+/// What the app has to do after a key.
79+#[derive(Default, Debug, PartialEq, Eq)]
80+pub struct Outcome {
81+ pub save: bool,
82+ pub close: bool,
83+ pub revert: bool,
84+}
85+
86+/// The clipboard, and whether it holds whole lines.
87+#[derive(Clone, Default, Debug)]
88+pub struct Clip {
89+ pub text: String,
90+ pub linewise: bool,
91+}
92+
93+/// An input line at the bottom of the editor: `:` commands or `/` search.
94+#[derive(Clone, Debug)]
95+pub struct CmdLine {
96+ pub kind: char,
97+ pub text: String,
98+}
99+
100+#[derive(Clone, Copy, PartialEq, Eq)]
101+enum Group {
102+ None,
103+ Typing,
104+}
105+
106+struct Snap {
107+ lines: Vec<EditLine>,
108+ cursor: Pos,
109+}
110+
111+pub struct Editor {
112+ pub buf: EditBuffer,
113+ pub cursor: Pos,
114+ /// The other end of the selection, if there is one.
115+ pub anchor: Option<Pos>,
116+ pub keys: Keys,
117+ pub mode: Mode,
118+ pub clip: Clip,
119+ pub cmdline: Option<CmdLine>,
120+ pub search: Option<String>,
121+ /// A one-line message for the editor's status (a failed search, …).
122+ pub message: Option<String>,
123+ /// Lines on screen, for paging (set by the renderer).
124+ pub page: usize,
125+ /// Text copied since the last frame, for the system clipboard (OSC 52).
126+ pub copied: Option<String>,
127+ /// Where a mouse drag started.
128+ drag_origin: Option<Pos>,
129+ want_col: Option<usize>,
130+ undo: Vec<Snap>,
131+ redo: Vec<Snap>,
132+ group: Group,
133+ /// Bumped by every change; keymaps compare it to see whether a command
134+ /// changed the text.
135+ pub changes: u64,
136+ vim: vim::State,
137+ helix: helix::State,
138+}
139+
140+impl Editor {
141+ pub fn new(buf: EditBuffer, keys: Keys, clip: Clip) -> Editor {
142+ let mode = match keys {
143+ Keys::Normal => Mode::Insert,
144+ Keys::Vim | Keys::Helix => Mode::Normal,
145+ };
146+ Editor {
147+ buf,
148+ cursor: Pos::default(),
149+ anchor: None,
150+ keys,
151+ mode,
152+ clip,
153+ cmdline: None,
154+ search: None,
155+ message: None,
156+ page: 20,
157+ copied: None,
158+ drag_origin: None,
159+ want_col: None,
160+ undo: Vec::new(),
161+ redo: Vec::new(),
162+ group: Group::None,
163+ changes: 0,
164+ vim: vim::State::default(),
165+ helix: helix::State::default(),
166+ }
167+ }
168+
169+ /// Switch keymaps mid-edit: the text, cursor and history stay.
170+ pub fn set_keys(&mut self, keys: Keys) {
171+ self.keys = keys;
172+ self.anchor = None;
173+ self.cmdline = None;
174+ self.vim = vim::State::default();
175+ self.helix = helix::State::default();
176+ self.mode = match keys {
177+ Keys::Normal => Mode::Insert,
178+ _ => Mode::Normal,
179+ };
180+ self.clamp_cursor();
181+ }
182+
183+ /// Handle a key in the current keymap.
184+ pub fn handle(&mut self, key: KeyEvent) -> Outcome {
185+ self.message = None;
186+ if self.cmdline.is_some() {
187+ return self.cmdline_key(key);
188+ }
189+ let out = match self.keys {
190+ Keys::Normal => normal::handle(self, key),
191+ Keys::Vim => vim::handle(self, key),
192+ Keys::Helix => helix::handle(self, key),
193+ };
194+ self.clamp_cursor();
195+ out
196+ }
197+
198+ /// Text from the terminal's paste (bracketed paste): typed in as is.
199+ pub fn paste_text(&mut self, text: &str) {
200+ let text = text.replace("\r\n", "\n").replace('\r', "\n");
201+ if self.mode != Mode::Insert && self.keys != Keys::Normal {
202+ // pasting in a normal mode puts the text after the cursor
203+ self.checkpoint();
204+ let at = self.after_cursor();
205+ self.cursor = self.insert(at, &text);
206+ return;
207+ }
208+ self.checkpoint();
209+ self.delete_selection();
210+ self.cursor = self.insert(self.cursor, &text);
211+ self.group = Group::None;
212+ }
213+
214+ // -------------------------------------------------------------- pointer
215+
216+ /// A click: the cursor goes there and any selection is dropped.
217+ pub fn click(&mut self, p: Pos) {
218+ self.anchor = None;
219+ if matches!(self.mode, Mode::Visual { .. } | Mode::Select) {
220+ self.mode = Mode::Normal;
221+ }
222+ self.set_cursor(self.clamp_pos(p));
223+ self.clamp_cursor();
224+ self.drag_origin = Some(self.cursor);
225+ }
226+
227+ /// Dragging selects from where the button went down (a Vim drag is a
228+ /// visual selection).
229+ pub fn drag_to(&mut self, p: Pos) {
230+ let Some(o) = self.drag_origin else { return };
231+ let p = self.clamp_pos(p);
232+ if p == o && self.anchor.is_none() {
233+ return;
234+ }
235+ self.anchor = Some(o);
236+ if self.keys == Keys::Vim && self.mode == Mode::Normal {
237+ self.mode = Mode::Visual { line: false };
238+ }
239+ self.set_cursor(p);
240+ self.clamp_cursor();
241+ }
242+
243+ pub fn end_drag(&mut self) {
244+ self.drag_origin = None;
245+ }
246+
247+ /// A double-click selects the word under the pointer.
248+ pub fn select_word(&mut self, p: Pos) {
249+ let (s, e) = self.word_at(self.clamp_pos(p), false);
250+ if s == e {
251+ return;
252+ }
253+ self.anchor = Some(s);
254+ self.cursor = if self.keys == Keys::Normal { e } else { self.prev(e).unwrap_or(e) };
255+ if self.keys == Keys::Vim && self.mode == Mode::Normal {
256+ self.mode = Mode::Visual { line: false };
257+ }
258+ }
259+
260+ fn clamp_pos(&self, p: Pos) -> Pos {
261+ let line = p.line.min(self.lines() - 1);
262+ Pos::new(line, p.col.min(self.len(line)))
263+ }
264+
265+ /// The selected range as `[start, end)` in document order, for drawing.
266+ pub fn selection(&self) -> Option<(Pos, Pos)> {
267+ let a = self.anchor?;
268+ let inclusive = match self.keys {
269+ Keys::Normal => false,
270+ _ => true,
271+ };
272+ if let Mode::Visual { line: true } = self.mode {
273+ let (s, e) = order(a, self.cursor);
274+ return Some((Pos::new(s.line, 0), Pos::new(e.line, self.len(e.line))));
275+ }
276+ let (s, e) = order(a, self.cursor);
277+ Some((s, if inclusive { self.next(e).unwrap_or(Pos::new(e.line, self.len(e.line))) } else { e }))
278+ }
279+
280+ /// A block cursor (Vim and Helix outside insert mode) or a bar.
281+ pub fn block_cursor(&self) -> bool {
282+ self.keys != Keys::Normal && self.mode != Mode::Insert
283+ }
284+
285+ /// A short mode name for the border.
286+ pub fn mode_name(&self) -> &'static str {
287+ match (self.keys, self.mode) {
288+ (Keys::Normal, _) => "",
289+ (_, Mode::Insert) => "INSERT",
290+ (_, Mode::Normal) => "NORMAL",
291+ (_, Mode::Visual { line: false }) => "VISUAL",
292+ (_, Mode::Visual { line: true }) => "V-LINE",
293+ (_, Mode::Select) => "SELECT",
294+ }
295+ }
296+
297+ // -------------------------------------------------------------- text
298+
299+ pub fn lines(&self) -> usize {
300+ self.buf.lines.len().max(1)
301+ }
302+
303+ pub fn line(&self, l: usize) -> &str {
304+ self.buf.lines.get(l).map(|x| x.text.as_str()).unwrap_or("")
305+ }
306+
307+ pub fn len(&self, l: usize) -> usize {
308+ self.line(l).chars().count()
309+ }
310+
311+ /// The character at a position; a line end reads as `\n` (except the last).
312+ pub fn char_at(&self, p: Pos) -> Option<char> {
313+ let line = self.line(p.line);
314+ match line.chars().nth(p.col) {
315+ Some(c) => Some(c),
316+ None if p.line + 1 < self.lines() => Some('\n'),
317+ None => None,
318+ }
319+ }
320+
321+ pub fn next(&self, p: Pos) -> Option<Pos> {
322+ if p.col < self.len(p.line) {
323+ Some(Pos::new(p.line, p.col + 1))
324+ } else if p.line + 1 < self.lines() {
325+ Some(Pos::new(p.line + 1, 0))
326+ } else {
327+ None
328+ }
329+ }
330+
331+ pub fn prev(&self, p: Pos) -> Option<Pos> {
332+ if p.col > 0 {
333+ Some(Pos::new(p.line, p.col - 1))
334+ } else if p.line > 0 {
335+ Some(Pos::new(p.line - 1, self.len(p.line - 1)))
336+ } else {
337+ None
338+ }
339+ }
340+
341+ /// The text of `[a, b)`.
342+ pub fn text(&self, a: Pos, b: Pos) -> String {
343+ let (a, b) = order(a, b);
344+ if a.line == b.line {
345+ let l = self.line(a.line);
346+ return l.chars().skip(a.col).take(b.col.saturating_sub(a.col)).collect();
347+ }
348+ let mut s: String = self.line(a.line).chars().skip(a.col).collect();
349+ for l in a.line + 1..b.line {
350+ s.push('\n');
351+ s.push_str(self.line(l));
352+ }
353+ s.push('\n');
354+ s.extend(self.line(b.line).chars().take(b.col));
355+ s
356+ }
357+
358+ /// Insert text at a position; returns the position after it. New lines
359+ /// belong to the block of the line they split (§5.2).
360+ pub fn insert(&mut self, at: Pos, s: &str) -> Pos {
361+ if s.is_empty() {
362+ return at;
363+ }
364+ if self.buf.lines.is_empty() {
365+ self.buf.lines.push(EditLine { text: String::new(), owner: Owner { file: 0, block_ord: 0 } });
366+ }
367+ self.changes += 1;
368+ let line = self.line(at.line).to_string();
369+ let b = byte(&line, at.col);
370+ let (pre, post) = line.split_at(b);
371+ let parts: Vec<&str> = s.split('\n').collect();
372+ if parts.len() == 1 {
373+ self.buf.set_line(at.line, format!("{}{}{}", pre, s, post));
374+ return Pos::new(at.line, at.col + s.chars().count());
375+ }
376+ self.buf.set_line(at.line, format!("{}{}", pre, parts[0]));
377+ let mut l = at.line;
378+ for p in &parts[1..parts.len() - 1] {
379+ self.buf.insert_line(l, p.to_string());
380+ l += 1;
381+ }
382+ let last = parts[parts.len() - 1];
383+ self.buf.insert_line(l, format!("{}{}", last, post));
384+ Pos::new(l + 1, last.chars().count())
385+ }
386+
387+ /// Delete `[a, b)`; returns what was deleted.
388+ pub fn delete(&mut self, a: Pos, b: Pos) -> String {
389+ let (a, mut b) = order(a, b);
390+ if b.line >= self.lines() {
391+ b = Pos::new(self.lines() - 1, self.len(self.lines() - 1));
392+ }
393+ let gone = self.text(a, b);
394+ if gone.is_empty() {
395+ return gone;
396+ }
397+ self.changes += 1;
398+ let first = self.line(a.line).to_string();
399+ let last = self.line(b.line).to_string();
400+ let joined = format!("{}{}", &first[..byte(&first, a.col)], &last[byte(&last, b.col)..]);
401+ for l in (a.line + 1..=b.line).rev() {
402+ self.buf.delete_line(l);
403+ }
404+ self.buf.set_line(a.line, joined);
405+ gone
406+ }
407+
408+ /// Delete whole lines `l1..=l2`; returns them, each ending in `\n`.
409+ pub fn delete_lines(&mut self, l1: usize, l2: usize) -> String {
410+ let l2 = l2.min(self.lines() - 1);
411+ let mut gone = String::new();
412+ for l in l1..=l2 {
413+ gone.push_str(self.line(l));
414+ gone.push('\n');
415+ }
416+ self.changes += 1;
417+ if l1 == 0 && l2 + 1 >= self.lines() {
418+ // the buffer keeps one (empty) line
419+ for l in (1..self.lines()).rev() {
420+ self.buf.delete_line(l);
421+ }
422+ self.buf.set_line(0, String::new());
423+ } else {
424+ for l in (l1..=l2).rev() {
425+ self.buf.delete_line(l);
426+ }
427+ }
428+ self.cursor = Pos::new(l1.min(self.lines() - 1), 0);
429+ gone
430+ }
431+
432+ /// Put whole lines (text ending in `\n`) below or above line `l`;
433+ /// returns the first new line.
434+ pub fn put_lines(&mut self, l: usize, text: &str, below: bool) -> usize {
435+ let body = text.strip_suffix('\n').unwrap_or(text);
436+ if below {
437+ let end = Pos::new(l, self.len(l));
438+ self.insert(end, &format!("\n{}", body));
439+ l + 1
440+ } else {
441+ self.insert(Pos::new(l, 0), &format!("{}\n", body));
442+ l
443+ }
444+ }
445+
446+ fn delete_selection(&mut self) -> Option<String> {
447+ let (s, e) = self.selection()?;
448+ self.anchor = None;
449+ let gone = self.delete(s, e);
450+ self.cursor = s;
451+ Some(gone)
452+ }
453+
454+ // -------------------------------------------------------------- clipboard
455+
456+ pub fn copy(&mut self, text: String, linewise: bool) {
457+ self.copied = Some(text.clone());
458+ self.clip = Clip { text, linewise };
459+ }
460+
461+ fn after_cursor(&self) -> Pos {
462+ Pos::new(self.cursor.line, (self.cursor.col + 1).min(self.len(self.cursor.line)))
463+ }
464+
465+ // -------------------------------------------------------------- undo
466+
467+ /// Remember the text before a change. Consecutive typing is one step.
468+ pub fn checkpoint(&mut self) {
469+ self.redo.clear();
470+ self.undo.push(Snap { lines: self.buf.lines.clone(), cursor: self.cursor });
471+ self.group = Group::None;
472+ }
473+
474+ fn checkpoint_typing(&mut self) {
475+ if self.group != Group::Typing {
476+ self.checkpoint();
477+ self.group = Group::Typing;
478+ }
479+ }
480+
481+ pub fn undo(&mut self) -> bool {
482+ let Some(s) = self.undo.pop() else {
483+ self.message = Some("nothing to undo".into());
484+ return false;
485+ };
486+ let cur = self.restore(s);
487+ self.redo.push(cur);
488+ true
489+ }
490+
491+ pub fn redo(&mut self) -> bool {
492+ let Some(s) = self.redo.pop() else {
493+ self.message = Some("nothing to redo".into());
494+ return false;
495+ };
496+ let cur = self.restore(s);
497+ self.undo.push(cur);
498+ true
499+ }
500+
501+ /// Put a snapshot back; every block whose text differs is dirty again.
502+ fn restore(&mut self, s: Snap) -> Snap {
503+ let cur = Snap { lines: std::mem::take(&mut self.buf.lines), cursor: self.cursor };
504+ let text_of = |lines: &[EditLine], o: Owner| -> Vec<String> {
505+ lines.iter().filter(|l| l.owner == o).map(|l| l.text.clone()).collect()
506+ };
507+ let owners: BTreeSet<Owner> = cur.lines.iter().chain(s.lines.iter()).map(|l| l.owner).collect();
508+ for o in owners {
509+ if text_of(&cur.lines, o) != text_of(&s.lines, o) {
510+ self.buf.mark_dirty(o);
511+ }
512+ }
513+ self.buf.lines = s.lines;
514+ self.cursor = s.cursor;
515+ self.anchor = None;
516+ self.changes += 1;
517+ self.group = Group::None;
518+ self.clamp_cursor();
519+ cur
520+ }
521+
522+ // -------------------------------------------------------------- motion
523+
524+ pub fn clamp_cursor(&mut self) {
525+ let last = self.lines() - 1;
526+ self.cursor.line = self.cursor.line.min(last);
527+ let len = self.len(self.cursor.line);
528+ // outside insert mode the cursor sits on a character — except the end
529+ // of a Helix selection, which may take in the line's newline
530+ let helix_sel = self.keys == Keys::Helix && self.anchor.is_some();
531+ let max = if self.block_cursor() && len > 0 && !helix_sel { len - 1 } else { len };
532+ self.cursor.col = self.cursor.col.min(max);
533+ }
534+
535+ pub fn move_vert(&mut self, delta: isize) {
536+ let want = self.want_col.unwrap_or(self.cursor.col);
537+ let line = (self.cursor.line as isize + delta).clamp(0, self.lines() as isize - 1) as usize;
538+ self.cursor = Pos::new(line, want.min(self.len(line)));
539+ self.want_col = Some(want);
540+ }
541+
542+ pub fn set_cursor(&mut self, p: Pos) {
543+ self.cursor = p;
544+ self.want_col = None;
545+ }
546+
547+ pub fn first_non_blank(&self, l: usize) -> usize {
548+ self.line(l).chars().take_while(|c| c.is_whitespace()).count()
549+ }
550+
551+ /// Start of the next word (`w`), or of the next WORD (`W`).
552+ pub fn word_fwd(&self, p: Pos, big: bool) -> Pos {
553+ let mut q = p;
554+ let start = self.char_at(q).map(|c| class(c, big));
555+ if let Some(k) = start.filter(|k| *k != Class::Space) {
556+ while let Some(c) = self.char_at(q) {
557+ if class(c, big) != k {
558+ break;
559+ }
560+ match self.next(q) {
561+ Some(n) => q = n,
562+ None => return Pos::new(q.line, self.len(q.line)),
563+ }
564+ }
565+ }
566+ // skip whitespace; an empty line is a word
567+ while let Some(c) = self.char_at(q) {
568+ if class(c, big) != Class::Space {
569+ break;
570+ }
571+ let Some(n) = self.next(q) else { break };
572+ if c == '\n' && self.len(n.line) == 0 {
573+ return n;
574+ }
575+ q = n;
576+ }
577+ q
578+ }
579+
580+ /// End of the word (`e` / `E`).
581+ pub fn word_end(&self, p: Pos, big: bool) -> Pos {
582+ let mut q = match self.next(p) {
583+ Some(n) => n,
584+ None => return p,
585+ };
586+ while let Some(c) = self.char_at(q) {
587+ if class(c, big) != Class::Space {
588+ break;
589+ }
590+ match self.next(q) {
591+ Some(n) => q = n,
592+ None => return q,
593+ }
594+ }
595+ let k = self.char_at(q).map(|c| class(c, big));
596+ while let Some(n) = self.next(q) {
597+ if self.char_at(n).map(|c| class(c, big)) != k || self.char_at(n) == Some('\n') {
598+ break;
599+ }
600+ q = n;
601+ }
602+ q
603+ }
604+
605+ /// Start of the previous word (`b` / `B`).
606+ pub fn word_back(&self, p: Pos, big: bool) -> Pos {
607+ let mut q = match self.prev(p) {
608+ Some(n) => n,
609+ None => return p,
610+ };
611+ while let Some(c) = self.char_at(q) {
612+ if class(c, big) != Class::Space || (c == '\n' && self.len(q.line) == 0) {
613+ break;
614+ }
615+ match self.prev(q) {
616+ Some(n) => q = n,
617+ None => return q,
618+ }
619+ }
620+ let k = self.char_at(q).map(|c| class(c, big));
621+ while let Some(n) = self.prev(q) {
622+ if self.char_at(n).map(|c| class(c, big)) != k || self.char_at(n) == Some('\n') {
623+ break;
624+ }
625+ q = n;
626+ }
627+ q
628+ }
629+
630+ /// The word under a position: `[start, end)`.
631+ pub fn word_at(&self, p: Pos, big: bool) -> (Pos, Pos) {
632+ let line: Vec<char> = self.line(p.line).chars().collect();
633+ if line.is_empty() {
634+ return (p, p);
635+ }
636+ let i = p.col.min(line.len() - 1);
637+ let k = class(line[i], big);
638+ let mut s = i;
639+ while s > 0 && class(line[s - 1], big) == k {
640+ s -= 1;
641+ }
642+ let mut e = i + 1;
643+ while e < line.len() && class(line[e], big) == k {
644+ e += 1;
645+ }
646+ (Pos::new(p.line, s), Pos::new(p.line, e))
647+ }
648+
649+ /// The next (or previous) blank line: `}` / `{`.
650+ pub fn paragraph(&self, from: usize, fwd: bool) -> usize {
651+ let blank = |l: usize| self.line(l).trim().is_empty();
652+ let mut l = from;
653+ if fwd {
654+ while l + 1 < self.lines() && blank(l) {
655+ l += 1;
656+ }
657+ while l + 1 < self.lines() && !blank(l) {
658+ l += 1;
659+ }
660+ } else {
661+ while l > 0 && blank(l) {
662+ l -= 1;
663+ }
664+ while l > 0 && !blank(l) {
665+ l -= 1;
666+ }
667+ }
668+ l
669+ }
670+
671+ /// Find a character on the cursor's line: `f` (forward, on it), `t`
672+ /// (forward, before it), `F`, `T`.
673+ pub fn find_char(&self, from: Pos, c: char, kind: char, count: usize) -> Option<Pos> {
674+ let line: Vec<char> = self.line(from.line).chars().collect();
675+ let mut col = from.col;
676+ for _ in 0..count.max(1) {
677+ col = match kind {
678+ 'f' | 't' => {
679+ let start = if kind == 't' { col + 2 } else { col + 1 };
680+ let i = (start.min(line.len())..line.len()).find(|&i| line[i] == c)?;
681+ if kind == 't' { i - 1 } else { i }
682+ }
683+ _ => {
684+ let end = if kind == 'T' { col.saturating_sub(1) } else { col };
685+ let i = (0..end).rev().find(|&i| line[i] == c)?;
686+ if kind == 'T' { i + 1 } else { i }
687+ }
688+ };
689+ }
690+ Some(Pos::new(from.line, col))
691+ }
692+
693+ /// The bracket matching the one at (or after) the cursor: `%`.
694+ pub fn match_bracket(&self, p: Pos) -> Option<Pos> {
695+ let line: Vec<char> = self.line(p.line).chars().collect();
696+ let i = (p.col..line.len()).find(|&i| "()[]{}".contains(line[i]))?;
697+ let (open, close, fwd) = match line[i] {
698+ '(' => ('(', ')', true),
699+ '[' => ('[', ']', true),
700+ '{' => ('{', '}', true),
701+ ')' => ('(', ')', false),
702+ ']' => ('[', ']', false),
703+ _ => ('{', '}', false),
704+ };
705+ let mut depth = 0i32;
706+ let mut q = Pos::new(p.line, i);
707+ loop {
708+ match self.char_at(q) {
709+ Some(c) if c == open => depth += if fwd { 1 } else { -1 },
710+ Some(c) if c == close => depth += if fwd { -1 } else { 1 },
711+ _ => {}
712+ }
713+ if depth == 0 {
714+ return Some(q);
715+ }
716+ q = if fwd { self.next(q)? } else { self.prev(q)? };
717+ }
718+ }
719+
720+ /// The span of a text object around the cursor: `iw`, `a(`, `i"`, `ip`, …
721+ pub fn text_object(&self, around: bool, obj: char) -> Option<(Pos, Pos)> {
722+ let p = self.cursor;
723+ match obj {
724+ 'w' | 'W' => {
725+ let (s, mut e) = self.word_at(p, obj == 'W');
726+ if around {
727+ let line: Vec<char> = self.line(p.line).chars().collect();
728+ while e.col < line.len() && line[e.col].is_whitespace() {
729+ e.col += 1;
730+ }
731+ }
732+ Some((s, e))
733+ }
734+ '"' | '\'' | '`' => {
735+ // quotes pair up from the start of the line: an odd number
736+ // before the cursor means it is inside (or on the closer)
737+ let line: Vec<char> = self.line(p.line).chars().collect();
738+ let col = p.col.min(line.len());
739+ let before = line[..col].iter().filter(|&&c| c == obj).count();
740+ let (open, close) = if before % 2 == 1 {
741+ let o = (0..col).rev().find(|&i| line[i] == obj)?;
742+ (o, (col..line.len()).find(|&i| line[i] == obj)?)
743+ } else {
744+ let o = (col..line.len()).find(|&i| line[i] == obj)?;
745+ (o, (o + 1..line.len()).find(|&i| line[i] == obj)?)
746+ };
747+ Some(if around {
748+ (Pos::new(p.line, open), Pos::new(p.line, close + 1))
749+ } else {
750+ (Pos::new(p.line, open + 1), Pos::new(p.line, close))
751+ })
752+ }
753+ '(' | ')' | 'b' | '[' | ']' | '{' | '}' | 'B' | '<' | '>' => {
754+ let (o, c) = match obj {
755+ '(' | ')' | 'b' => ('(', ')'),
756+ '[' | ']' => ('[', ']'),
757+ '<' | '>' => ('<', '>'),
758+ _ => ('{', '}'),
759+ };
760+ // walk back to the unmatched opener
761+ let mut depth = 0;
762+ let mut q = p;
763+ let open = loop {
764+ match self.char_at(q) {
765+ Some(ch) if ch == c && q != p => depth += 1,
766+ Some(ch) if ch == o => {
767+ if depth == 0 {
768+ break q;
769+ }
770+ depth -= 1;
771+ }
772+ _ => {}
773+ }
774+ q = self.prev(q)?;
775+ };
776+ let mut depth = 0;
777+ let mut q = self.next(open)?;
778+ let close = loop {
779+ match self.char_at(q) {
780+ Some(ch) if ch == o => depth += 1,
781+ Some(ch) if ch == c => {
782+ if depth == 0 {
783+ break q;
784+ }
785+ depth -= 1;
786+ }
787+ _ => {}
788+ }
789+ q = self.next(q)?;
790+ };
791+ Some(if around { (open, self.next(close)?) } else { (self.next(open)?, close) })
792+ }
793+ 'p' => {
794+ let blank = |l: usize| self.line(l).trim().is_empty();
795+ let mut s = p.line;
796+ while s > 0 && !blank(s - 1) {
797+ s -= 1;
798+ }
799+ let mut e = p.line;
800+ while e + 1 < self.lines() && !blank(e + 1) {
801+ e += 1;
802+ }
803+ if around {
804+ while e + 1 < self.lines() && blank(e + 1) {
805+ e += 1;
806+ }
807+ }
808+ let end = if e + 1 < self.lines() { Pos::new(e + 1, 0) } else { Pos::new(e, self.len(e)) };
809+ Some((Pos::new(s, 0), end))
810+ }
811+ _ => None,
812+ }
813+ }
814+
815+ // -------------------------------------------------------------- edits
816+
817+ /// Typing: replaces a selection, one undo step per run of typing.
818+ pub fn type_char(&mut self, c: char) {
819+ self.checkpoint_typing();
820+ if self.anchor.is_some() && self.keys == Keys::Normal {
821+ self.delete_selection();
822+ }
823+ self.cursor = self.insert(self.cursor, &c.to_string());
824+ self.want_col = None;
825+ }
826+
827+ /// Enter: a new line, keeping the current line's indentation.
828+ pub fn newline(&mut self) {
829+ self.checkpoint_typing();
830+ if self.anchor.is_some() && self.keys == Keys::Normal {
831+ self.delete_selection();
832+ }
833+ let indent: String = self.line(self.cursor.line).chars().take_while(|c| *c == ' ').collect();
834+ let indent: String = indent.chars().take(self.cursor.col).collect();
835+ self.cursor = self.insert(self.cursor, &format!("\n{}", indent));
836+ self.want_col = None;
837+ }
838+
839+ pub fn backspace(&mut self) {
840+ self.checkpoint_typing();
841+ if self.anchor.is_some() && self.keys == Keys::Normal {
842+ self.delete_selection();
843+ return;
844+ }
845+ if let Some(p) = self.prev(self.cursor) {
846+ // a run of indentation goes back one level at a time
847+ let line = self.line(self.cursor.line);
848+ let col = self.cursor.col;
849+ let n = if col >= 2 && col % 2 == 0 && line.chars().take(col).all(|c| c == ' ') { 2 } else { 1 };
850+ let start = if n == 2 { Pos::new(self.cursor.line, col - 2) } else { p };
851+ self.delete(start, self.cursor);
852+ self.cursor = start;
853+ }
854+ self.want_col = None;
855+ }
856+
857+ pub fn delete_forward(&mut self) {
858+ self.checkpoint_typing();
859+ if self.anchor.is_some() && self.keys == Keys::Normal {
860+ self.delete_selection();
861+ return;
862+ }
863+ if let Some(n) = self.next(self.cursor) {
864+ self.delete(self.cursor, n);
865+ }
866+ }
867+
868+ /// Delete back to the start of the word (Ctrl-W, Alt-Backspace).
869+ pub fn delete_word_back(&mut self) {
870+ self.checkpoint();
871+ let start = self.word_back(self.cursor, false);
872+ self.delete(start, self.cursor);
873+ self.cursor = start;
874+ }
875+
876+ /// Indent (`dir > 0`) or dedent lines `l1..=l2` by two spaces.
877+ pub fn indent(&mut self, l1: usize, l2: usize, dir: i32) {
878+ self.checkpoint();
879+ for l in l1..=l2.min(self.lines() - 1) {
880+ if dir > 0 {
881+ if !self.line(l).is_empty() {
882+ self.insert(Pos::new(l, 0), " ");
883+ }
884+ } else {
885+ let n = self.line(l).chars().take(2).take_while(|c| *c == ' ').count();
886+ self.delete(Pos::new(l, 0), Pos::new(l, n));
887+ }
888+ }
889+ let shift = if dir > 0 { 2 } else { 0 };
890+ self.cursor.col = if dir > 0 { self.cursor.col + shift } else { self.cursor.col.saturating_sub(2) };
891+ }
892+
893+ /// Join `count` lines below onto line `l` with single spaces (`J`).
894+ pub fn join(&mut self, l: usize, count: usize) {
895+ self.checkpoint();
896+ for _ in 0..count.max(1) {
897+ if l + 1 >= self.lines() {
898+ break;
899+ }
900+ let end = Pos::new(l, self.len(l));
901+ let next_indent = self.first_non_blank(l + 1);
902+ let sep = if self.line(l).is_empty() || self.line(l + 1).trim().is_empty() { "" } else { " " };
903+ self.delete(end, Pos::new(l + 1, next_indent));
904+ self.insert(end, sep);
905+ self.cursor = end;
906+ }
907+ }
908+
909+ /// Change the case of `[a, b)`: `u` lower, `U` upper, `~` swap.
910+ pub fn change_case(&mut self, a: Pos, b: Pos, how: char) {
911+ let t = self.text(a, b);
912+ let n: String = t
913+ .chars()
914+ .map(|c| match how {
915+ 'u' => c.to_lowercase().next().unwrap_or(c),
916+ 'U' => c.to_uppercase().next().unwrap_or(c),
917+ _ if c.is_uppercase() => c.to_lowercase().next().unwrap_or(c),
918+ _ => c.to_uppercase().next().unwrap_or(c),
919+ })
920+ .collect();
921+ if n != t {
922+ self.delete(a, b);
923+ self.insert(a, &n);
924+ }
925+ }
926+
927+ // -------------------------------------------------------------- search
928+
929+ /// The next match of the search after (or before) a position, wrapping.
930+ pub fn find(&self, pat: &str, from: Pos, fwd: bool) -> Option<Pos> {
931+ if pat.is_empty() {
932+ return None;
933+ }
934+ let pat = pat.to_lowercase();
935+ let n = self.lines();
936+ let hits = |l: usize| -> Vec<usize> {
937+ let line = self.line(l).to_lowercase();
938+ let mut out = Vec::new();
939+ let mut start = 0;
940+ while let Some(i) = line[start..].find(&pat) {
941+ out.push(line[..start + i].chars().count());
942+ start += i + pat.len().max(1);
943+ }
944+ out
945+ };
946+ for k in 0..=n {
947+ let l = if fwd { (from.line + k) % n } else { (from.line + n - k % n) % n };
948+ let hs = hits(l);
949+ let found = if fwd {
950+ hs.into_iter().find(|&c| k > 0 || c > from.col)
951+ } else {
952+ hs.into_iter().rev().find(|&c| k > 0 || c < from.col)
953+ };
954+ if let Some(c) = found {
955+ return Some(Pos::new(l, c));
956+ }
957+ }
958+ None
959+ }
960+
961+ /// Jump to the next match of the last search.
962+ pub fn search_next(&mut self, fwd: bool) -> Option<Pos> {
963+ let pat = self.search.clone()?;
964+ match self.find(&pat, self.cursor, fwd) {
965+ Some(p) => {
966+ self.set_cursor(p);
967+ Some(p)
968+ }
969+ None => {
970+ self.message = Some(format!("not found: {}", pat));
971+ None
972+ }
973+ }
974+ }
975+
976+ // -------------------------------------------------------------- command line
977+
978+ pub fn open_cmdline(&mut self, kind: char) {
979+ self.cmdline = Some(CmdLine { kind, text: String::new() });
980+ }
981+
982+ fn cmdline_key(&mut self, key: KeyEvent) -> Outcome {
983+ let Some(mut cl) = self.cmdline.take() else { return Outcome::default() };
984+ match key.code {
985+ KeyCode::Esc => {}
986+ KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {}
987+ KeyCode::Enter => return self.run_cmdline(cl),
988+ KeyCode::Backspace => {
989+ if cl.text.pop().is_some() {
990+ self.cmdline = Some(cl);
991+ }
992+ }
993+ KeyCode::Char(c) => {
994+ cl.text.push(c);
995+ self.cmdline = Some(cl);
996+ }
997+ _ => self.cmdline = Some(cl),
998+ }
999+ Outcome::default()
1000+ }
1001+
1002+ fn run_cmdline(&mut self, cl: CmdLine) -> Outcome {
1003+ let t = cl.text.trim();
1004+ match cl.kind {
1005+ '/' | '?' => {
1006+ if !t.is_empty() {
1007+ self.search = Some(t.to_string());
1008+ }
1009+ let fwd = cl.kind == '/';
1010+ if let Some(p) = self.search_next(fwd) {
1011+ if self.keys == Keys::Helix || self.keys == Keys::Normal {
1012+ // select the match
1013+ let len = self.search.as_ref().map(|s| s.chars().count()).unwrap_or(0);
1014+ let end = Pos::new(p.line, p.col + len);
1015+ match self.keys {
1016+ Keys::Helix => {
1017+ self.anchor = Some(p);
1018+ self.cursor = Pos::new(p.line, (p.col + len).saturating_sub(1));
1019+ }
1020+ _ => {
1021+ self.anchor = Some(p);
1022+ self.cursor = end;
1023+ }
1024+ }
1025+ }
1026+ }
1027+ Outcome::default()
1028+ }
1029+ _ => self.ex(t),
1030+ }
1031+ }
1032+
1033+ /// `:` commands, shared by all keymaps.
1034+ pub fn ex(&mut self, cmd: &str) -> Outcome {
1035+ let mut out = Outcome::default();
1036+ match cmd {
1037+ "w" | "write" | "save" => out.save = true,
1038+ "q" | "quit" | "close" | "wq" | "x" | "write-quit" | "exit" => out.close = true,
1039+ "q!" | "quit!" | "e!" | "reload" | "revert" | "cq" => out.revert = true,
1040+ "noh" | "nohlsearch" => self.search = None,
1041+ "" => {}
1042+ n if n.chars().all(|c| c.is_ascii_digit()) => {
1043+ let l = n.parse::<usize>().unwrap_or(1).saturating_sub(1);
1044+ self.set_cursor(Pos::new(l.min(self.lines() - 1), 0));
1045+ self.anchor = None;
1046+ }
1047+ other => self.message = Some(format!("unknown command: {}", other)),
1048+ }
1049+ out
1050+ }
1051+}
1052+
1053+#[derive(Clone, Copy, PartialEq, Eq)]
1054+enum Class {
1055+ Space,
1056+ Word,
1057+ Punct,
1058+}
1059+
1060+fn class(c: char, big: bool) -> Class {
1061+ if c.is_whitespace() {
1062+ Class::Space
1063+ } else if big || c.is_alphanumeric() || c == '_' {
1064+ Class::Word
1065+ } else {
1066+ Class::Punct
1067+ }
1068+}
1069+
1070+pub fn order(a: Pos, b: Pos) -> (Pos, Pos) {
1071+ if a <= b {
1072+ (a, b)
1073+ } else {
1074+ (b, a)
1075+ }
1076+}
1077+
1078+/// Byte offset of a character column.
1079+fn byte(s: &str, col: usize) -> usize {
1080+ s.char_indices().nth(col).map(|(i, _)| i).unwrap_or(s.len())
1081+}
1082+
1083+fn ctrl(key: &KeyEvent, c: char) -> bool {
1084+ key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char(c)
1085+}
1086+
1087+#[cfg(test)]
1088+pub(crate) mod test_util {
1089+ use super::*;
1090+ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
1091+
1092+ /// An editor over a one-file vault holding `text` (under a `# T` title).
1093+ pub fn editor(text: &str, keys: Keys) -> (tempfile::TempDir, Editor) {
1094+ let d = tempfile::tempdir().unwrap();
1095+ std::fs::write(d.path().join("root.md"), format!("# T\n\n{}", text)).unwrap();
1096+ let v = fold_core::vault::Vault::open(d.path()).unwrap();
1097+ let t = v.tree.resolved_children(v.tree.root)[0];
1098+ let buf = fold_core::edit::open_editor(&v, t);
1099+ let mut e = Editor::new(buf, keys, Clip::default());
1100+ e.cursor = Pos::new(2, 0);
1101+ (d, e)
1102+ }
1103+
1104+ /// Feed a string of keys: `<Esc>`, `<CR>`, `<BS>`, `<C-x>`, `<A-x>`,
1105+ /// `<S-Left>` and friends in angle brackets, everything else literal.
1106+ pub fn keys(e: &mut Editor, s: &str) -> Vec<Outcome> {
1107+ let mut outs = Vec::new();
1108+ let mut chars = s.chars().peekable();
1109+ while let Some(c) = chars.next() {
1110+ let key = if c == '<' && chars.peek().is_some_and(|n| n.is_ascii_alphabetic()) {
1111+ let name: String = chars.by_ref().take_while(|&c| c != '>').collect();
1112+ parse_key(&name)
1113+ } else {
1114+ KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE)
1115+ };
1116+ outs.push(e.handle(key));
1117+ }
1118+ outs
1119+ }
1120+
1121+ fn parse_key(name: &str) -> KeyEvent {
1122+ let mut mods = KeyModifiers::NONE;
1123+ let mut base = name;
1124+ while base.len() > 2 && base.as_bytes()[1] == b'-' && "CAS".contains(&base[..1]) {
1125+ mods |= match &base[..1] {
1126+ "C" => KeyModifiers::CONTROL,
1127+ "A" => KeyModifiers::ALT,
1128+ _ => KeyModifiers::SHIFT,
1129+ };
1130+ base = &base[2..];
1131+ }
1132+ let code = match base {
1133+ "Esc" => KeyCode::Esc,
1134+ "CR" | "Enter" => KeyCode::Enter,
1135+ "BS" => KeyCode::Backspace,
1136+ "Del" => KeyCode::Delete,
1137+ "Tab" => KeyCode::Tab,
1138+ "BTab" => KeyCode::BackTab,
1139+ "Left" => KeyCode::Left,
1140+ "Right" => KeyCode::Right,
1141+ "Up" => KeyCode::Up,
1142+ "Down" => KeyCode::Down,
1143+ "Home" => KeyCode::Home,
1144+ "End" => KeyCode::End,
1145+ "PgUp" => KeyCode::PageUp,
1146+ "PgDn" => KeyCode::PageDown,
1147+ "lt" => KeyCode::Char('<'),
1148+ one if one.chars().count() == 1 => KeyCode::Char(one.chars().next().unwrap()),
1149+ other => panic!("unknown key {}", other),
1150+ };
1151+ KeyEvent::new(code, mods)
1152+ }
1153+
1154+ /// The text after the `# T` title and its blank line.
1155+ pub fn body(e: &Editor) -> String {
1156+ (2..e.lines()).map(|l| e.line(l).to_string()).collect::<Vec<_>>().join("\n")
1157+ }
1158+}