strop_engine/editor/
visual.rs1use strop_core::Range;
7use strop_grammar::{self as grammar, Op};
8
9use super::input::Action;
10use super::{Editor, Key, Mode};
11
12impl Editor {
13 pub(crate) fn feed_visual(&mut self, key: Key) {
14 let p = self.sels().primary();
17 self.last_visual = Some((p.anchor, p.head));
18 match key {
19 Key::Esc => {
20 self.mode = Mode::Normal;
21 self.walker.clear();
22 }
23 Key::Up if self.walker.is_ground() => self.run_motion("k"),
24 Key::Down if self.walker.is_ground() => self.run_motion("j"),
25 Key::Left if self.walker.is_ground() => self.run_motion("h"),
26 Key::Right if self.walker.is_ground() => self.run_motion("l"),
27 Key::Char('>') | Key::Char('<') if self.walker.is_ground() => {
28 let Some(range) = self.visual_range() else {
31 return;
32 };
33 if self.buf().readonly {
34 self.message = "readonly buffer".into();
35 self.mode = Mode::Normal;
36 return;
37 }
38 let right = key == Key::Char('>');
39 self.tx_begin();
40 self.apply_indent(range, right);
41 self.tx_commit();
42 self.mode = Mode::Normal;
43 self.set_head(range.start.get());
44 self.clamp_cursor();
45 self.flash(Range::charwise(self.head(), self.head()));
46 self.last_cmd_keys = if right { "V>" } else { "V<" }.into();
47 self.last_insert = None;
48 }
49 Key::Char('d') | Key::Char('y') | Key::Char('c') | Key::Char('x')
50 if self.walker.is_ground() && self.mode == Mode::VisualBlock =>
51 {
52 match key {
53 Key::Char('y') => self.block_yank(),
54 Key::Char('c') => self.block_change(),
55 _ => self.block_delete(),
56 }
57 }
58 Key::Char('I') if self.mode == Mode::VisualBlock && self.walker.is_ground() => {
59 self.block_insert(false)
60 }
61 Key::Char('A') if self.mode == Mode::VisualBlock && self.walker.is_ground() => {
62 self.block_insert(true)
63 }
64 Key::Char('d') | Key::Char('y') | Key::Char('c') | Key::Char('x')
65 if self.walker.is_ground() =>
66 {
67 let op = match key {
68 Key::Char('d') | Key::Char('x') => Op::Delete,
69 Key::Char('y') => Op::Yank,
70 _ => Op::Change,
71 };
72 if self.buf().readonly && op != Op::Yank {
73 self.message = "readonly buffer".into();
74 self.mode = Mode::Normal;
75 return;
76 }
77 let Some(range) = self.visual_range() else {
78 return;
79 };
80 let linewise = self.mode == Mode::VisualLine;
81 if op == Op::Yank {
82 let text = self.buf().slice_string(range);
83 self.set_register(
84 None,
85 if linewise {
86 super::Register::linewise(text)
87 } else {
88 super::Register::characterwise(text)
89 },
90 );
91 self.flash(range);
92 } else {
93 let text = self.buf().slice_string(range);
94 let changes = crate::editor::transact::ChangeSet {
95 edits: vec![strop_core::Replacement::new(range, String::new())],
96 undo_open: false,
97 };
98 if let Err(error) = self.apply(self.current(), self.buf().revision(), changes) {
99 self.message = error.to_string();
100 return;
101 }
102 self.set_register(
103 None,
104 if linewise {
105 super::Register::linewise(text)
106 } else {
107 super::Register::characterwise(text)
108 },
109 );
110 self.set_head(range.start.get());
111 self.flash(Range::charwise(self.head(), self.head()));
112 }
113 self.mode = Mode::Normal;
114 self.clamp_cursor();
115 if op == Op::Change {
116 self.enter_insert_from(if linewise { "V..." } else { "v..." });
117 }
118 }
119 _ => {
120 let action = self.walker.feed_visual(key);
121 self.dispatch_visual_action(action);
122 }
123 }
124 }
125
126 pub(crate) fn select_visual_object(&mut self, command: &grammar::Command) {
127 if self.defer_resolution(
128 command,
129 self.all_cursors(),
130 super::resolution::ResolutionPurpose::VisualObject,
131 ) {
132 return;
133 }
134 match self
135 .resolved_many(command, &self.all_cursors())
136 .map(|resolved| resolved.into_iter().next().flatten())
137 {
138 Ok(Some(resolved)) => {
139 let head = self.head();
140 self.sels_mut()
141 .stretch_primary(resolved.range.start.get(), head);
142 self.set_head(
143 self.buf()
144 .clamp_boundary(resolved.range.end.get().saturating_sub(1)),
145 );
146 }
147 Ok(None) => {}
148 Err(error) => self.message = error,
149 }
150 }
151
152 fn dispatch_visual_action(&mut self, action: Action) {
156 match action {
157 Action::Pending => {}
158 Action::Invalid(keys) => self.message = format!("not an editor command: {keys}"),
159 Action::QueryError(error) => self.message = error.to_string(),
160 Action::EnterText { sigil, state } => self.begin_text_line(sigil, state),
161 Action::Grammar(command) if command.op.is_none() => {
162 if let grammar::Target::Object { .. } = command.target {
163 self.select_visual_object(&command);
164 } else {
165 self.move_cursor(&command);
166 }
167 }
168 Action::Grammar(_) => self.message = "operator needs a visual selection".into(),
169 Action::Row { row, .. } if row.id == "git-file-history" => {
170 let a = self.buf().line_of(self.anchor()) + 1;
172 let b = self.buf().line_of(self.head()) + 1;
173 self.open_line_history(a.min(b), a.max(b));
174 }
175 Action::Row { row, .. } if row.id == "clip-yank" => {
176 if let Some(range) = self.visual_range() {
178 let linewise = self.mode == Mode::VisualLine;
179 let text = self.buf().slice_string(range);
180 self.set_register(
181 Some('+'),
182 if linewise {
183 super::Register::linewise(text)
184 } else {
185 super::Register::characterwise(text)
186 },
187 );
188 self.flash(range);
189 }
190 self.mode = Mode::Normal;
191 self.clamp_cursor();
192 }
193 Action::Row { .. } => self.message = "command unavailable in visual mode".into(),
194 Action::VisualSurround(c) => {
195 if self.buf().readonly {
196 self.message = "readonly buffer".into();
197 return;
198 }
199 let Some(range) = self.visual_range() else {
200 return;
201 };
202 let pair = match c {
203 'b' | '(' | ')' => ('(', ')'),
204 'B' | '{' | '}' => ('{', '}'),
205 'r' | '[' | ']' => ('[', ']'),
206 'a' | '<' | '>' => ('<', '>'),
207 q => (q, q),
208 };
209 let changes = crate::editor::transact::ChangeSet {
213 edits: vec![
214 strop_core::Replacement::new(
215 Range::charwise(range.start, range.start),
216 pair.0.to_string(),
217 ),
218 strop_core::Replacement::new(
219 Range::charwise(range.end, range.end),
220 pair.1.to_string(),
221 ),
222 ],
223 undo_open: false,
224 };
225 if let Err(error) = self.apply(self.current(), self.buf().revision(), changes) {
226 self.message = error.to_string();
227 return;
228 }
229 self.mode = Mode::Normal;
230 self.flash(Range::charwise(
231 range.start,
232 range.end.get() + pair.0.len_utf8() + pair.1.len_utf8(),
233 ));
234 self.last_cmd_keys = format!("vS{c}"); self.last_insert = None;
236 }
237 }
238 }
239
240 pub fn visual_range(&self) -> Option<Range> {
241 match self.mode {
242 Mode::Visual => {
243 let (s, e) = (
246 self.anchor().min(self.head()),
247 self.buf().ceil_boundary(self.anchor().max(self.head()) + 1),
248 );
249 Some(Range::charwise(s, e.min(self.buf().len_bytes())))
250 }
251 Mode::VisualLine => {
252 let (a, b) = (
253 self.buf().line_of(self.anchor()),
254 self.buf().line_of(self.head()),
255 );
256 let (a, b) = (a.min(b), a.max(b));
257 let start = self.buf().line_start(a);
258 let end = if b + 1 >= self.buf().len_lines() {
259 self.buf().len_bytes()
260 } else {
261 self.buf().line_start(b + 1)
262 };
263 Some(Range::linewise(start, end))
264 }
265 _ => None,
266 }
267 }
268}