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
//! 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) {
// the pipe input line: `|sort<cr>` pipes the selection through
// the command (helix's pipe) — its own tiny input state
if self.pending.starts_with('|') {
match key {
Key::Esc => self.pending.clear(),
Key::Backspace => {
self.pending.pop();
if self.pending.len() <= 1 {
self.pending.clear();
}
}
Key::Enter => {
let cmd = self.pending[1..].to_string();
self.pending.clear();
if let Some(r) = self.visual_range() {
self.pipe_run(r.start, r.end, &cmd);
}
self.mode = Mode::Normal;
}
Key::Char(c) => self.pending.push(c),
_ => {}
}
return;
}
match key {
Key::Esc => {
self.mode = Mode::Normal;
self.pending.clear();
}
Key::Char('>') | Key::Char('<') if self.pending.is_empty() => {
// visual indent: apply to every selected line, one undo
// unit, back to normal (vim re-selects with gv)
let Some(range) = self.visual_range() else {
return;
};
if self.buf().readonly {
self.message = "readonly buffer".into();
self.mode = Mode::Normal;
return;
}
let right = key == Key::Char('>');
self.tx_begin();
self.apply_indent(range, right);
self.tx_commit();
self.mode = Mode::Normal;
self.cursor = range.start;
self.clamp_cursor();
self.flash(Range::charwise(self.cursor, self.cursor));
self.last_cmd_keys = if right { "V>" } else { "V<" }.into();
self.last_insert = None;
}
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 c == '|' && self.pending.is_empty() {
self.pending = "|".into();
return;
}
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 self.pending == "S" || self.pending == " " {
return; // surround/leader await their second key
}
if matches!(grammar::parse(&self.pending), Parse::Invalid) {
// invalid keys never squat in pending (visual had no
// clear — `>x` used to swallow every later key)
self.message = format!("not an editor command: {}", self.pending);
self.pending.clear();
return;
}
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,
}
}
}