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
//! 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('y') if self.pending == " " => {
// Space y: yank the selection to the system clipboard
self.pending.clear();
if let Some(range) = self.visual_range() {
let linewise = self.mode == Mode::VisualLine;
let text = self.buf().slice_string(range);
self.set_register(Some('+'), text, linewise);
self.flash(range);
}
self.mode = Mode::Normal;
self.clamp_cursor();
}
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,
}
}
}