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
//! Visual mode (charwise `v` and linewise `V`): motions extend the
//! selection, operators consume it. The escape hatch of 0001 §2.2.
use strop_core::Range;
use strop_grammar::{self as grammar, Op, Parse};
use super::{Editor, Key, Mode};
impl Editor {
pub(crate) fn feed_visual(&mut self, key: Key) {
match key {
Key::Esc => {
self.mode = Mode::Normal;
self.pending.clear();
}
Key::Char('d') | Key::Char('y') | Key::Char('c') | Key::Char('x')
if self.pending.is_empty() =>
{
let op = match key {
Key::Char('d') | Key::Char('x') => Op::Delete,
Key::Char('y') => Op::Yank,
_ => Op::Change,
};
if self.buf().readonly && op != Op::Yank {
self.message = "readonly buffer".into();
self.mode = Mode::Normal;
return;
}
let Some(range) = self.visual_range() else {
return;
};
let linewise = self.mode == Mode::VisualLine;
if op == Op::Yank {
let text = self.buf().slice_string(range);
self.set_register(None, text, linewise);
self.flash(range);
} else {
self.tx_begin();
let text = self.buf_mut().delete(range);
self.tx_commit();
self.set_register(None, text, linewise);
self.cursor = range.start;
self.flash(Range::charwise(self.cursor, self.cursor));
}
self.mode = Mode::Normal;
self.clamp_cursor();
if op == Op::Change {
self.enter_insert_from(if linewise { "V..." } else { "v..." });
}
}
Key::Char(c) => {
if self.pending == "S" {
// visual S<char>: wrap the selection (sandwich)
self.pending.clear();
if let Some(range) = self.visual_range() {
let pair = match c {
'b' | '(' | ')' => ('(', ')'),
'B' | '{' | '}' => ('{', '}'),
'r' | '[' | ']' => ('[', ']'),
'a' | '<' | '>' => ('<', '>'),
q => (q, q),
};
self.tx_begin();
self.buf_mut().insert(range.end, &pair.1.to_string());
self.buf_mut().insert(range.start, &pair.0.to_string());
self.tx_commit();
self.mode = Mode::Normal;
self.flash(Range::charwise(range.start, range.end + 2));
self.last_cmd_keys = format!("vS{c}"); // replay is visual-mode replay; approximated
self.last_insert = None;
}
return;
}
self.pending.push(c);
if let Parse::Complete(cmd) = grammar::parse(&self.pending) {
if cmd.op.is_none() {
self.pending.clear();
// objects in visual mode select the object (vi[, va"):
// the anchor jumps to the range start, the cursor to
// its end — inclusive, vim semantics (0001 §5.5)
if let grammar::Target::Object { .. } = cmd.target {
if let Some(r) = grammar::resolve(self.buf(), self.cursor, &cmd) {
self.anchor = r.range.start;
self.cursor = r.range.end.saturating_sub(1);
}
} else {
self.move_cursor(&cmd);
}
}
}
}
_ => {}
}
}
pub fn visual_range(&self) -> Option<Range> {
match self.mode {
Mode::Visual => {
let (s, e) = (
self.anchor.min(self.cursor),
self.anchor.max(self.cursor) + 1,
);
Some(Range::charwise(s, e.min(self.buf().len_bytes())))
}
Mode::VisualLine => {
let (a, b) = (
self.buf().line_of(self.anchor),
self.buf().line_of(self.cursor),
);
let (a, b) = (a.min(b), a.max(b));
let start = self.buf().line_start(a);
let end = if b + 1 >= self.buf().len_lines() {
self.buf().len_bytes()
} else {
self.buf().line_start(b + 1)
};
Some(Range::linewise(start, end))
}
_ => None,
}
}
}