reovim_module_vim/commands/
mode_entry.rs1use {
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
26fn 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#[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#[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#[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 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 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#[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 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 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#[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 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 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 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 let insert_text = format!("\n{indent}");
166 runtime.insert_text(buffer_id, insert_pos, &insert_text);
167
168 let indent_len = indent.chars().count();
170 set_cursor_position(runtime, Position::new(pos.line + 1, indent_len));
171
172 begin_insert_batch(runtime, buffer_id);
174
175 runtime.set_mode(VimMode::INSERT_ID, TransitionContext::new());
176
177 CommandResult::Success
178 }
179}
180
181#[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 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 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 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 let indent_len = indent.chars().count();
230 set_cursor_position(runtime, Position::new(pos.line, indent_len));
231
232 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;