Skip to main content

reovim_module_vim/visual/
manipulation.rs

1//! Selection manipulation commands.
2//!
3//! Provides commands for manipulating the visual selection:
4//! - `o` - Swap cursor and anchor positions
5//! - `v` (in visual mode) - Toggle to character-wise mode
6//! - `V` (in visual mode) - Toggle to line-wise mode
7//! - `Ctrl-V` (in visual mode) - Toggle to block mode
8//! - `gv` - Reselect last visual selection
9
10use {
11    reovim_driver_command::{Command, CommandContext, CommandHandler, CommandResult},
12    reovim_driver_session::{
13        SessionRuntime, TransitionContext,
14        api::{ModeApi, SelectionMode},
15    },
16    reovim_kernel::api::v1::CommandId,
17};
18
19use crate::{ids, modes::VimMode};
20
21/// Swap cursor and anchor positions (o in visual mode).
22///
23/// In Vim, pressing 'o' in visual mode swaps the cursor position with the
24/// anchor position, allowing you to adjust the other end of the selection.
25#[derive(Debug, Clone, Copy, Default)]
26pub struct SwapAnchor;
27
28impl Command for SwapAnchor {
29    fn id(&self) -> CommandId {
30        ids::VISUAL_SWAP_ANCHOR
31    }
32
33    fn description(&self) -> &'static str {
34        "Swap cursor and anchor in visual mode"
35    }
36}
37
38impl CommandHandler for SwapAnchor {
39    #[cfg_attr(coverage_nightly, coverage(off))]
40    fn execute(&self, runtime: &mut SessionRuntime<'_>, _args: &CommandContext) -> CommandResult {
41        // Only operate if selection is active
42        let Some(selection) = runtime.windows().active().and_then(|w| w.selection.clone()) else {
43            return CommandResult::Success; // No selection - no-op
44        };
45
46        // Swap anchor and cursor - swap start and end
47        let swapped = reovim_driver_session::api::Selection {
48            start: selection.end,
49            end: selection.start,
50            mode: selection.mode,
51        };
52
53        if let Some(window) = runtime.windows_mut().active_mut() {
54            window.selection = Some(swapped);
55        }
56
57        CommandResult::Success
58    }
59}
60
61/// Toggle to character-wise visual mode (v in visual mode).
62///
63/// If already in character-wise mode, exit to normal mode.
64/// Otherwise, switch to character-wise selection.
65#[derive(Debug, Clone, Copy, Default)]
66pub struct ToggleVisualChar;
67
68impl Command for ToggleVisualChar {
69    fn id(&self) -> CommandId {
70        ids::TOGGLE_VISUAL_CHAR
71    }
72
73    fn description(&self) -> &'static str {
74        "Toggle to character-wise visual mode"
75    }
76}
77
78impl CommandHandler for ToggleVisualChar {
79    #[cfg_attr(coverage_nightly, coverage(off))]
80    fn execute(&self, runtime: &mut SessionRuntime<'_>, _args: &CommandContext) -> CommandResult {
81        let selection = runtime.windows().active().and_then(|w| w.selection.clone());
82
83        let target_mode = match selection {
84            Some(sel) if sel.mode == SelectionMode::Character => {
85                // Already in character mode - exit to normal
86                if let Some(window) = runtime.windows_mut().active_mut() {
87                    window.selection = None;
88                }
89                VimMode::NORMAL_ID
90            }
91            Some(mut sel) => {
92                // Switch to character mode
93                sel.mode = SelectionMode::Character;
94                if let Some(window) = runtime.windows_mut().active_mut() {
95                    window.selection = Some(sel);
96                }
97                VimMode::VISUAL_ID
98            }
99            None => {
100                // No selection - nothing to do
101                return CommandResult::Success;
102            }
103        };
104
105        runtime.set_mode(target_mode, TransitionContext::new());
106
107        CommandResult::Success
108    }
109}
110
111/// Toggle to line-wise visual mode (V in visual mode).
112///
113/// If already in line-wise mode, exit to normal mode.
114/// Otherwise, switch to line-wise selection.
115#[derive(Debug, Clone, Copy, Default)]
116pub struct ToggleVisualLine;
117
118impl Command for ToggleVisualLine {
119    fn id(&self) -> CommandId {
120        ids::TOGGLE_VISUAL_LINE
121    }
122
123    fn description(&self) -> &'static str {
124        "Toggle to line-wise visual mode"
125    }
126}
127
128impl CommandHandler for ToggleVisualLine {
129    #[cfg_attr(coverage_nightly, coverage(off))]
130    fn execute(&self, runtime: &mut SessionRuntime<'_>, _args: &CommandContext) -> CommandResult {
131        let selection = runtime.windows().active().and_then(|w| w.selection.clone());
132
133        let target_mode = match selection {
134            Some(sel) if sel.mode == SelectionMode::Line => {
135                // Already in line mode - exit to normal
136                if let Some(window) = runtime.windows_mut().active_mut() {
137                    window.selection = None;
138                }
139                VimMode::NORMAL_ID
140            }
141            Some(mut sel) => {
142                // Switch to line mode
143                sel.mode = SelectionMode::Line;
144                if let Some(window) = runtime.windows_mut().active_mut() {
145                    window.selection = Some(sel);
146                }
147                VimMode::VISUAL_LINE_ID
148            }
149            None => {
150                // No selection - nothing to do
151                return CommandResult::Success;
152            }
153        };
154
155        runtime.set_mode(target_mode, TransitionContext::new());
156
157        CommandResult::Success
158    }
159}
160
161/// Toggle to block-wise visual mode (Ctrl-V in visual mode).
162///
163/// If already in block mode, exit to normal mode.
164/// Otherwise, switch to block selection.
165#[derive(Debug, Clone, Copy, Default)]
166pub struct ToggleVisualBlock;
167
168impl Command for ToggleVisualBlock {
169    fn id(&self) -> CommandId {
170        ids::TOGGLE_VISUAL_BLOCK
171    }
172
173    fn description(&self) -> &'static str {
174        "Toggle to block-wise visual mode"
175    }
176}
177
178impl CommandHandler for ToggleVisualBlock {
179    #[cfg_attr(coverage_nightly, coverage(off))]
180    fn execute(&self, runtime: &mut SessionRuntime<'_>, _args: &CommandContext) -> CommandResult {
181        let selection = runtime.windows().active().and_then(|w| w.selection.clone());
182
183        let target_mode = match selection {
184            Some(sel) if sel.mode == SelectionMode::Block => {
185                // Already in block mode - exit to normal
186                if let Some(window) = runtime.windows_mut().active_mut() {
187                    window.selection = None;
188                }
189                VimMode::NORMAL_ID
190            }
191            Some(mut sel) => {
192                // Switch to block mode
193                sel.mode = SelectionMode::Block;
194                if let Some(window) = runtime.windows_mut().active_mut() {
195                    window.selection = Some(sel);
196                }
197                VimMode::VISUAL_BLOCK_ID
198            }
199            None => {
200                // No selection - nothing to do
201                return CommandResult::Success;
202            }
203        };
204
205        runtime.set_mode(target_mode, TransitionContext::new());
206
207        CommandResult::Success
208    }
209}
210
211/// Reselect the last visual selection (gv in normal mode).
212///
213/// Restores the previous visual selection bounds and enters the appropriate
214/// visual mode. If no previous selection exists, this is a no-op.
215///
216/// Note: The actual restoration logic is handled by the event loop since it
217/// requires access to `AppState.last_visual_selection`. This command just signals
218/// the intent to reselect.
219#[derive(Debug, Clone, Copy, Default)]
220pub struct ReselectLast;
221
222impl Command for ReselectLast {
223    fn id(&self) -> CommandId {
224        ids::RESELECT_LAST
225    }
226
227    fn description(&self) -> &'static str {
228        "Reselect the last visual selection (gv)"
229    }
230}
231
232impl CommandHandler for ReselectLast {
233    #[cfg_attr(coverage_nightly, coverage(off))]
234    fn execute(&self, _runtime: &mut SessionRuntime<'_>, _args: &CommandContext) -> CommandResult {
235        // TODO(#394): Implement via SessionRuntime (escape hatch until API supports this)
236        // Will signal intent to restore the last visual selection
237        CommandResult::Success
238    }
239}