Skip to main content

reovim_module_vim/commands/
mode_entry.rs

1//! Mode entry commands.
2//!
3//! Provides specialized commands for entering insert mode:
4//! - `EnterInsertFirstNonBlank` (I)
5//! - `EnterInsertEndOfLine` (A)
6//! - `OpenLineBelow` (o)
7//! - `OpenLineAbove` (O)
8//!
9//! # Epic #372 - Mode Ownership
10//!
11//! These commands use `VimMode::INSERT_ID` to transition to insert mode,
12//! which is why they belong in the vim module.
13
14use {
15    reovim_driver_command::{Command, CommandContext, CommandHandler, CommandResult},
16    reovim_driver_session::{BufferApi, SessionRuntime, TransitionContext, api::ModeApi},
17    reovim_driver_undo::{UndoKey, UndoProviderRegistry},
18    reovim_kernel::api::v1::{BufferId, CommandId, OptionScopeId, Position},
19};
20
21use {
22    crate::{ids, modes::VimMode},
23    reovim_module_editor::command::get_line_indent,
24};
25
26/// Helper to get cursor position from the active window.
27fn get_cursor_position(runtime: &SessionRuntime<'_>) -> Option<Position> {
28    let window = runtime.windows().active()?;
29    Some(Position::new(window.cursor.line, window.cursor.column))
30}
31
32/// Helper to set cursor position on the active window.
33#[cfg_attr(coverage_nightly, coverage(off))]
34fn set_cursor_position(runtime: &mut SessionRuntime<'_>, pos: Position) {
35    if let Some(window) = runtime.windows_mut().active_mut() {
36        window.cursor = pos.into();
37    }
38}
39
40/// Start undo batching for insert mode.
41#[cfg_attr(coverage_nightly, coverage(off))]
42fn begin_insert_batch(runtime: &SessionRuntime<'_>, buffer_id: BufferId) {
43    if let Some(pos) = get_cursor_position(runtime)
44        && let Some(undo_registry) = runtime.kernel().services.get::<UndoProviderRegistry>()
45        && let Some(undo_provider) = undo_registry.get(&UndoKey::Buffer)
46    {
47        undo_provider.begin_batch(buffer_id, pos);
48    }
49}
50
51/// Enter insert mode at first non-blank character (I).
52#[derive(Debug, Clone, Copy, Default)]
53pub struct EnterInsertFirstNonBlank;
54
55impl Command for EnterInsertFirstNonBlank {
56    fn id(&self) -> CommandId {
57        ids::ENTER_INSERT_BOL
58    }
59
60    fn description(&self) -> &'static str {
61        "Enter insert mode at first non-blank character"
62    }
63}
64
65impl CommandHandler for EnterInsertFirstNonBlank {
66    fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
67        if let Some(buffer_id) = args.buffer_id() {
68            if let Some(pos) = get_cursor_position(runtime) {
69                // Find first non-blank character on current line
70                let first_non_blank = runtime
71                    .buffer_line(buffer_id, pos.line)
72                    .map_or(0, |line| line.chars().position(|c| !c.is_whitespace()).unwrap_or(0));
73
74                set_cursor_position(runtime, Position::new(pos.line, first_non_blank));
75            }
76            // Start undo batching for insert mode
77            begin_insert_batch(runtime, buffer_id);
78        }
79
80        runtime.set_mode(VimMode::INSERT_ID, TransitionContext::new());
81
82        CommandResult::Success
83    }
84}
85
86/// Enter insert mode at end of line (A).
87#[derive(Debug, Clone, Copy, Default)]
88pub struct EnterInsertEndOfLine;
89
90impl Command for EnterInsertEndOfLine {
91    fn id(&self) -> CommandId {
92        ids::ENTER_INSERT_EOL
93    }
94
95    fn description(&self) -> &'static str {
96        "Enter insert mode at end of line"
97    }
98}
99
100impl CommandHandler for EnterInsertEndOfLine {
101    fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
102        if let Some(buffer_id) = args.buffer_id() {
103            if let Some(pos) = get_cursor_position(runtime) {
104                // Move cursor to end of current line
105                let line_len = runtime.buffer_line_len(buffer_id, pos.line).unwrap_or(0);
106                set_cursor_position(runtime, Position::new(pos.line, line_len));
107            }
108            // Start undo batching for insert mode
109            begin_insert_batch(runtime, buffer_id);
110        }
111
112        runtime.set_mode(VimMode::INSERT_ID, TransitionContext::new());
113
114        CommandResult::Success
115    }
116}
117
118/// Open line below and enter insert mode (o).
119#[derive(Debug, Clone, Copy, Default)]
120pub struct OpenLineBelow;
121
122impl Command for OpenLineBelow {
123    fn id(&self) -> CommandId {
124        ids::OPEN_LINE_BELOW
125    }
126
127    fn description(&self) -> &'static str {
128        "Open line below and enter insert mode"
129    }
130}
131
132impl CommandHandler for OpenLineBelow {
133    fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
134        let Some(buffer_id) = args.buffer_id() else {
135            return CommandResult::error("No active buffer");
136        };
137
138        // Check autoindent option (escape hatch - OptionsApi not yet available)
139        let autoindent = runtime
140            .kernel()
141            .options
142            .get("autoindent", OptionScopeId::Buffer(buffer_id))
143            .and_then(|v| v.as_bool())
144            .unwrap_or(true);
145
146        let Some(pos) = get_cursor_position(runtime) else {
147            return CommandResult::error("Failed to get buffer position");
148        };
149
150        // Get indent from current line if autoindent is enabled
151        let indent = if autoindent {
152            runtime
153                .buffer_line(buffer_id, pos.line)
154                .map(|line| get_line_indent(&line).to_owned())
155                .unwrap_or_default()
156        } else {
157            String::new()
158        };
159
160        // Get end of current line position
161        let line_len = runtime.buffer_line_len(buffer_id, pos.line).unwrap_or(0);
162        let insert_pos = Position::new(pos.line, line_len);
163
164        // Insert newline + indent at end of current line
165        let insert_text = format!("\n{indent}");
166        runtime.insert_text(buffer_id, insert_pos, &insert_text);
167
168        // Position cursor at end of indent on new line
169        let indent_len = indent.chars().count();
170        set_cursor_position(runtime, Position::new(pos.line + 1, indent_len));
171
172        // Start undo batching for insert mode
173        begin_insert_batch(runtime, buffer_id);
174
175        runtime.set_mode(VimMode::INSERT_ID, TransitionContext::new());
176
177        CommandResult::Success
178    }
179}
180
181/// Open line above and enter insert mode (O).
182#[derive(Debug, Clone, Copy, Default)]
183pub struct OpenLineAbove;
184
185impl Command for OpenLineAbove {
186    fn id(&self) -> CommandId {
187        ids::OPEN_LINE_ABOVE
188    }
189
190    fn description(&self) -> &'static str {
191        "Open line above and enter insert mode"
192    }
193}
194
195impl CommandHandler for OpenLineAbove {
196    fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
197        let Some(buffer_id) = args.buffer_id() else {
198            return CommandResult::error("No active buffer");
199        };
200
201        // Check autoindent option (escape hatch - OptionsApi not yet available)
202        let autoindent = runtime
203            .kernel()
204            .options
205            .get("autoindent", OptionScopeId::Buffer(buffer_id))
206            .and_then(|v| v.as_bool())
207            .unwrap_or(true);
208
209        let Some(pos) = get_cursor_position(runtime) else {
210            return CommandResult::error("Failed to get buffer position");
211        };
212
213        // Get indent from current line if autoindent is enabled
214        let indent = if autoindent {
215            runtime
216                .buffer_line(buffer_id, pos.line)
217                .map(|line| get_line_indent(&line).to_owned())
218                .unwrap_or_default()
219        } else {
220            String::new()
221        };
222
223        // Insert indent + newline at start of current line
224        let insert_pos = Position::new(pos.line, 0);
225        let insert_text = format!("{indent}\n");
226        runtime.insert_text(buffer_id, insert_pos, &insert_text);
227
228        // Position cursor at end of indent on the new line (which is now at pos.line)
229        let indent_len = indent.chars().count();
230        set_cursor_position(runtime, Position::new(pos.line, indent_len));
231
232        // Start undo batching for insert mode
233        begin_insert_batch(runtime, buffer_id);
234
235        runtime.set_mode(VimMode::INSERT_ID, TransitionContext::new());
236
237        CommandResult::Success
238    }
239}
240
241#[cfg(test)]
242#[allow(clippy::uninlined_format_args, clippy::significant_drop_tightening)]
243#[path = "tests/mode_entry.rs"]
244mod tests;