reovim_module_vim/visual/
operators.rs1use {
11 reovim_driver_command::{Command, CommandContext, CommandHandler, CommandResult},
12 reovim_driver_session::{
13 BufferApi, SessionRuntime, TransitionContext,
14 api::{ChangeTracker, ModeApi, RegisterContent, Selection, SelectionMode},
15 },
16 reovim_kernel::api::v1::{CommandId, Position},
17};
18
19use crate::{ids, modes::VimMode};
20
21fn expand_selection_range(
34 selection: &Selection,
35 end_line_len: Option<usize>,
36 total_lines: usize,
37) -> (Position, Position, bool) {
38 let start = selection.start;
39 let end = selection.end;
40
41 match selection.mode {
42 SelectionMode::Line => {
43 let start = Position::new(start.line, 0);
45 let end_line_len = end_line_len.unwrap_or(0);
49 let end = if end.line < total_lines {
50 Position::new(end.line, 0)
51 } else {
52 Position::new(end.line - 1, end_line_len)
54 };
55 (start, end, true)
56 }
57 SelectionMode::Character | SelectionMode::Block => (start, end, false),
59 }
60}
61
62#[derive(Debug, Clone, Copy, Default)]
67pub struct DeleteSelection;
68
69impl Command for DeleteSelection {
70 fn id(&self) -> CommandId {
71 ids::DELETE_SELECTION
72 }
73
74 fn description(&self) -> &'static str {
75 "Delete visual selection"
76 }
77}
78
79impl CommandHandler for DeleteSelection {
80 #[cfg_attr(coverage_nightly, coverage(off))]
81 fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
82 let Some(buffer_id) = args.buffer_id() else {
83 return CommandResult::error("No active buffer");
84 };
85
86 let Some(selection) = runtime.windows().active().and_then(|w| w.selection.clone()) else {
88 return CommandResult::Success; };
90
91 let end_line_len = runtime.buffer_line_len(buffer_id, selection.end.line);
93 let total_lines = runtime.buffer_line_count(buffer_id).unwrap_or(1);
94
95 let (start, end, is_linewise) =
97 expand_selection_range(&selection, end_line_len, total_lines);
98 let cursor_pos = start;
99
100 if let Some(text) = runtime.buffer_text_range(buffer_id, start, end) {
102 let content = if is_linewise {
103 RegisterContent::linewise(&text)
104 } else {
105 RegisterContent::characterwise(&text)
106 };
107 runtime.store_register_with_sync(args.register(), content);
108 }
109
110 runtime.delete_range(buffer_id, start, end);
112
113 if let Some(window) = runtime.windows_mut().active_mut() {
115 window.selection = None;
116 window.cursor = cursor_pos.into();
117 }
118
119 runtime.record_selection_change(buffer_id);
121
122 runtime.set_mode(VimMode::NORMAL_ID, TransitionContext::new());
124
125 CommandResult::Success
126 }
127}
128
129#[derive(Debug, Clone, Copy, Default)]
134pub struct YankSelection;
135
136impl Command for YankSelection {
137 fn id(&self) -> CommandId {
138 ids::YANK_SELECTION
139 }
140
141 fn description(&self) -> &'static str {
142 "Yank visual selection"
143 }
144}
145
146impl CommandHandler for YankSelection {
147 #[cfg_attr(coverage_nightly, coverage(off))]
148 fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
149 let Some(buffer_id) = args.buffer_id() else {
150 return CommandResult::error("No active buffer");
151 };
152
153 let Some(selection) = runtime.windows().active().and_then(|w| w.selection.clone()) else {
155 return CommandResult::Success; };
157
158 let end_line_len = runtime.buffer_line_len(buffer_id, selection.end.line);
160 let total_lines = runtime.buffer_line_count(buffer_id).unwrap_or(1);
161
162 let (start, end, is_linewise) =
164 expand_selection_range(&selection, end_line_len, total_lines);
165
166 if let Some(text) = runtime.buffer_text_range(buffer_id, start, end) {
168 let content = if is_linewise {
169 RegisterContent::linewise(&text)
170 } else {
171 RegisterContent::characterwise(&text)
172 };
173 runtime.store_register_with_sync(args.register(), content);
174 }
175
176 if let Some(window) = runtime.windows_mut().active_mut() {
178 window.selection = None;
179 }
180
181 runtime.record_selection_change(buffer_id);
183
184 runtime.set_mode(VimMode::NORMAL_ID, TransitionContext::new());
186
187 CommandResult::Success
188 }
189}
190
191#[derive(Debug, Clone, Copy, Default)]
195pub struct ChangeSelection;
196
197impl Command for ChangeSelection {
198 fn id(&self) -> CommandId {
199 ids::CHANGE_SELECTION
200 }
201
202 fn description(&self) -> &'static str {
203 "Change visual selection (delete and enter insert mode)"
204 }
205}
206
207impl CommandHandler for ChangeSelection {
208 #[cfg_attr(coverage_nightly, coverage(off))]
209 fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
210 let Some(buffer_id) = args.buffer_id() else {
211 return CommandResult::error("No active buffer");
212 };
213
214 let Some(selection) = runtime.windows().active().and_then(|w| w.selection.clone()) else {
216 return CommandResult::Success; };
218
219 let end_line_len = runtime.buffer_line_len(buffer_id, selection.end.line);
221 let total_lines = runtime.buffer_line_count(buffer_id).unwrap_or(1);
222
223 let (start, end, is_linewise) =
225 expand_selection_range(&selection, end_line_len, total_lines);
226 let cursor_pos = start;
227
228 if let Some(text) = runtime.buffer_text_range(buffer_id, start, end) {
230 let content = if is_linewise {
231 RegisterContent::linewise(&text)
232 } else {
233 RegisterContent::characterwise(&text)
234 };
235 runtime.store_register_with_sync(args.register(), content);
236 }
237
238 runtime.delete_range(buffer_id, start, end);
240
241 if let Some(window) = runtime.windows_mut().active_mut() {
243 window.selection = None;
244 window.cursor = cursor_pos.into();
245 }
246
247 runtime.record_selection_change(buffer_id);
249
250 runtime.set_mode(VimMode::INSERT_ID, TransitionContext::new());
252
253 CommandResult::Success
254 }
255}
256
257#[derive(Debug, Clone, Copy, Default)]
262pub struct IndentSelection;
263
264impl Command for IndentSelection {
265 fn id(&self) -> CommandId {
266 ids::INDENT_SELECTION
267 }
268
269 fn description(&self) -> &'static str {
270 "Indent visual selection"
271 }
272}
273
274impl CommandHandler for IndentSelection {
275 #[cfg_attr(coverage_nightly, coverage(off))]
276 fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
277 let Some(buffer_id) = args.buffer_id() else {
278 return CommandResult::error("No active buffer");
279 };
280
281 let Some(selection) = runtime.windows().active().and_then(|w| w.selection.clone()) else {
283 return CommandResult::Success; };
285
286 let start_line = selection.start.line;
289 let end_line = selection.end.line; let indent = " ";
294 for line_idx in start_line..end_line {
295 runtime.insert_text(buffer_id, Position::new(line_idx, 0), indent);
296 }
297
298 if let Some(window) = runtime.windows_mut().active_mut() {
300 window.selection = None;
301 }
302
303 runtime.record_selection_change(buffer_id);
305
306 runtime.set_mode(VimMode::NORMAL_ID, TransitionContext::new());
307
308 CommandResult::Success
309 }
310}
311
312#[derive(Debug, Clone, Copy, Default)]
317pub struct DedentSelection;
318
319impl Command for DedentSelection {
320 fn id(&self) -> CommandId {
321 ids::DEDENT_SELECTION
322 }
323
324 fn description(&self) -> &'static str {
325 "Dedent visual selection"
326 }
327}
328
329impl CommandHandler for DedentSelection {
330 #[cfg_attr(coverage_nightly, coverage(off))]
331 fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
332 let Some(buffer_id) = args.buffer_id() else {
333 return CommandResult::error("No active buffer");
334 };
335
336 let Some(selection) = runtime.windows().active().and_then(|w| w.selection.clone()) else {
338 return CommandResult::Success; };
340
341 let start_line = selection.start.line;
344 let end_line = selection.end.line; for line_idx in start_line..end_line {
348 if let Some(line) = runtime.buffer_line(buffer_id, line_idx) {
349 let mut chars_to_remove = 0;
350 for (i, c) in line.chars().enumerate() {
351 if c == '\t' {
352 chars_to_remove = i + 1;
353 break;
354 } else if c == ' ' && i < 4 {
355 chars_to_remove = i + 1;
356 } else {
357 break;
358 }
359 }
360 if chars_to_remove > 0 {
361 let start = Position::new(line_idx, 0);
362 let end = Position::new(line_idx, chars_to_remove);
363 runtime.delete_range(buffer_id, start, end);
364 }
365 }
366 }
367
368 if let Some(window) = runtime.windows_mut().active_mut() {
370 window.selection = None;
371 }
372
373 runtime.record_selection_change(buffer_id);
375
376 runtime.set_mode(VimMode::NORMAL_ID, TransitionContext::new());
377
378 CommandResult::Success
379 }
380}
381
382#[cfg_attr(coverage_nightly, coverage(off))]
388fn execute_case_selection(
389 runtime: &mut SessionRuntime<'_>,
390 args: &CommandContext,
391 transform: fn(&str) -> String,
392) -> CommandResult {
393 let Some(buffer_id) = args.buffer_id() else {
394 return CommandResult::error("No active buffer");
395 };
396
397 let Some(selection) = runtime.windows().active().and_then(|w| w.selection.clone()) else {
398 return CommandResult::Success;
399 };
400
401 let end_line_len = runtime.buffer_line_len(buffer_id, selection.end.line);
402 let total_lines = runtime.buffer_line_count(buffer_id).unwrap_or(1);
403 let (start, end, _is_linewise) = expand_selection_range(&selection, end_line_len, total_lines);
404
405 if let Some(text) = runtime.buffer_text_range(buffer_id, start, end) {
407 let transformed = transform(&text);
408 if transformed != text {
409 runtime.delete_range(buffer_id, start, end);
410 runtime.insert_text(buffer_id, start, &transformed);
411 }
412 }
413
414 if let Some(window) = runtime.windows_mut().active_mut() {
416 window.selection = None;
417 window.cursor = start.into();
418 }
419
420 runtime.record_selection_change(buffer_id);
421 runtime.set_mode(VimMode::NORMAL_ID, TransitionContext::new());
422
423 CommandResult::Success
424}
425
426#[derive(Debug, Clone, Copy, Default)]
428pub struct ToggleCaseSelection;
429
430impl Command for ToggleCaseSelection {
431 fn id(&self) -> CommandId {
432 ids::TOGGLE_CASE_SELECTION
433 }
434
435 fn description(&self) -> &'static str {
436 "Toggle case of visual selection"
437 }
438}
439
440#[cfg_attr(coverage_nightly, coverage(off))]
442impl CommandHandler for ToggleCaseSelection {
443 fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
444 execute_case_selection(runtime, args, |s| {
445 s.chars()
446 .map(|c| {
447 if c.is_uppercase() {
448 c.to_lowercase().next().unwrap_or(c)
449 } else if c.is_lowercase() {
450 c.to_uppercase().next().unwrap_or(c)
451 } else {
452 c
453 }
454 })
455 .collect()
456 })
457 }
458}
459
460#[derive(Debug, Clone, Copy, Default)]
462pub struct LowercaseSelection;
463
464impl Command for LowercaseSelection {
465 fn id(&self) -> CommandId {
466 ids::LOWERCASE_SELECTION
467 }
468
469 fn description(&self) -> &'static str {
470 "Lowercase visual selection"
471 }
472}
473
474#[cfg_attr(coverage_nightly, coverage(off))]
475impl CommandHandler for LowercaseSelection {
476 fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
477 execute_case_selection(runtime, args, str::to_lowercase)
478 }
479}
480
481#[derive(Debug, Clone, Copy, Default)]
483pub struct UppercaseSelection;
484
485impl Command for UppercaseSelection {
486 fn id(&self) -> CommandId {
487 ids::UPPERCASE_SELECTION
488 }
489
490 fn description(&self) -> &'static str {
491 "Uppercase visual selection"
492 }
493}
494
495#[cfg_attr(coverage_nightly, coverage(off))]
496impl CommandHandler for UppercaseSelection {
497 fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
498 execute_case_selection(runtime, args, str::to_uppercase)
499 }
500}
501
502#[cfg(test)]
503#[allow(clippy::significant_drop_tightening, clippy::uninlined_format_args)]
504#[path = "tests/operators.rs"]
505mod tests;