reovim_module_vim/operators/
commands.rs1use {
15 reovim_driver_command::{Command, CommandContext, CommandHandler, CommandResult},
16 reovim_driver_session::{BufferApi, SessionRuntime, TransitionContext, api::ModeApi},
17 reovim_kernel::api::v1::{CommandId, Position},
18};
19
20use {
21 super::{
22 DeleteOperator, LowercaseOperator, Operator, OperatorContext, Range, ToggleCaseOperator,
23 UppercaseOperator, YankOperator,
24 },
25 crate::ids::MODULE,
26};
27
28#[derive(Debug, Clone, Copy, Default)]
38pub struct DeleteCommand;
39
40impl Command for DeleteCommand {
41 fn id(&self) -> CommandId {
42 CommandId::new(MODULE, "delete")
43 }
44
45 fn description(&self) -> &'static str {
46 "Delete text in range"
47 }
48}
49
50#[cfg_attr(coverage_nightly, coverage(off))]
51impl CommandHandler for DeleteCommand {
52 fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
53 execute_operator(&DeleteOperator, runtime, args)
54 }
55}
56
57#[derive(Debug, Clone, Copy, Default)]
67pub struct YankCommand;
68
69impl Command for YankCommand {
70 fn id(&self) -> CommandId {
71 CommandId::new(MODULE, "yank")
72 }
73
74 fn description(&self) -> &'static str {
75 "Yank text in range to register"
76 }
77}
78
79#[cfg_attr(coverage_nightly, coverage(off))]
80impl CommandHandler for YankCommand {
81 #[cfg_attr(coverage_nightly, coverage(off))]
82 fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
83 let (start_line, start_col) = args.range_start().unwrap_or((0, 0));
86 let restore_pos = Position::new(start_line, start_col);
87
88 let cur_pos = runtime
89 .windows()
90 .active()
91 .map(|w| Position::new(w.cursor.line, w.cursor.column));
92 tracing::debug!(
93 range_start = ?restore_pos,
94 current_pos = ?cur_pos,
95 "yank command: before execute"
96 );
97
98 let result = execute_operator(&YankOperator, runtime, args);
99
100 if matches!(result, CommandResult::Success) && args.buffer_id().is_some() {
102 if let Some(window) = runtime.windows_mut().active_mut() {
103 window.cursor = restore_pos.into();
104 }
105
106 let cur_pos = runtime
107 .windows()
108 .active()
109 .map(|w| Position::new(w.cursor.line, w.cursor.column));
110 tracing::debug!(
111 restored_to = ?restore_pos,
112 current_pos = ?cur_pos,
113 "yank command: cursor restored"
114 );
115
116 if let Some(buffer_id) = args.buffer_id() {
118 use reovim_driver_session::api::ExtensionApi;
119 let (end_line, end_col) = args.range_end().unwrap_or((0, 0));
120 let state = runtime.ext_mut::<super::yank_flash::YankFlashState>();
121 state.record(
122 buffer_id,
123 start_line,
124 start_col,
125 end_line,
126 end_col,
127 args.is_linewise(),
128 );
129 }
130 }
131
132 result
133 }
134}
135
136#[derive(Debug, Clone, Copy, Default)]
146pub struct ChangeCommand;
147
148impl Command for ChangeCommand {
149 fn id(&self) -> CommandId {
150 CommandId::new(MODULE, "change")
151 }
152
153 fn description(&self) -> &'static str {
154 "Change text in range (delete and enter insert)"
155 }
156}
157
158#[cfg_attr(coverage_nightly, coverage(off))]
159impl CommandHandler for ChangeCommand {
160 #[cfg_attr(coverage_nightly, coverage(off))]
161 fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
162 use {
163 super::ChangeOperator,
164 crate::modes::VimMode,
165 reovim_driver_undo::{UndoKey, UndoProviderRegistry},
166 };
167
168 if let Some(buffer_id) = args.buffer_id()
171 && let Some(window) = runtime.windows().active()
172 && let Some(undo_registry) = runtime.kernel().services.get::<UndoProviderRegistry>()
173 && let Some(undo_provider) = undo_registry.get(&UndoKey::Buffer)
174 {
175 let pos = Position::new(window.cursor.line, window.cursor.column);
176 undo_provider.begin_batch(buffer_id, pos);
177 }
178
179 let result = execute_operator(&ChangeOperator, runtime, args);
181
182 if matches!(result, CommandResult::Success) {
184 runtime.set_mode(VimMode::INSERT_ID, TransitionContext::new());
185 }
186
187 result
188 }
189}
190
191#[derive(Debug, Clone, Copy, Default)]
197pub struct LowercaseCommand;
198
199impl Command for LowercaseCommand {
200 fn id(&self) -> CommandId {
201 CommandId::new(MODULE, "lowercase")
202 }
203
204 fn description(&self) -> &'static str {
205 "Lowercase text in range"
206 }
207}
208
209#[cfg_attr(coverage_nightly, coverage(off))]
210impl CommandHandler for LowercaseCommand {
211 fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
212 execute_operator(&LowercaseOperator, runtime, args)
213 }
214}
215
216#[derive(Debug, Clone, Copy, Default)]
222pub struct UppercaseCommand;
223
224impl Command for UppercaseCommand {
225 fn id(&self) -> CommandId {
226 CommandId::new(MODULE, "uppercase")
227 }
228
229 fn description(&self) -> &'static str {
230 "Uppercase text in range"
231 }
232}
233
234#[cfg_attr(coverage_nightly, coverage(off))]
235impl CommandHandler for UppercaseCommand {
236 fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
237 execute_operator(&UppercaseOperator, runtime, args)
238 }
239}
240
241#[derive(Debug, Clone, Copy, Default)]
247pub struct ToggleCaseCommand;
248
249impl Command for ToggleCaseCommand {
250 fn id(&self) -> CommandId {
251 CommandId::new(MODULE, "toggle-case-op")
252 }
253
254 fn description(&self) -> &'static str {
255 "Toggle case of text in range"
256 }
257}
258
259#[cfg_attr(coverage_nightly, coverage(off))]
260impl CommandHandler for ToggleCaseCommand {
261 fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
262 execute_operator(&ToggleCaseOperator, runtime, args)
263 }
264}
265
266#[cfg_attr(coverage_nightly, coverage(off))]
278fn execute_operator(
279 operator: &dyn Operator,
280 runtime: &mut SessionRuntime<'_>,
281 args: &CommandContext,
282) -> CommandResult {
283 let Some(buffer_id) = args.buffer_id() else {
285 return CommandResult::error("No active buffer");
286 };
287
288 let (start_line, start_col) = args.range_start().unwrap_or((0, 0));
290 let (end_line, end_col) = args.range_end().unwrap_or((0, 0));
291 let linewise = args.is_linewise();
292
293 let start = Position::new(start_line, start_col);
294 let end = Position::new(end_line, end_col);
295
296 let range = if linewise {
298 Range::linewise(start, end)
299 } else {
300 Range::new(start, end)
301 };
302
303 let count = args.count().unwrap_or(1);
305 let register = super::registers::option_char_to_register(args.register());
306
307 let cursor_position = runtime
309 .windows()
310 .active()
311 .map_or_else(Position::origin, |w| Position::new(w.cursor.line, w.cursor.column));
312
313 let (kernel, registers, clipboard_history) = runtime.kernel_and_registers();
316 let mut op_ctx = OperatorContext {
317 kernel,
318 registers,
319 clipboard_history,
320 buffer_id,
321 register,
322 count,
323 cursor_position,
324 cursor_after: None,
325 };
326
327 match operator.execute(&mut op_ctx, range) {
329 Ok(()) => {
330 let cursor_after = op_ctx.cursor_after;
335
336 if operator.is_text_modifying() {
338 let new_cursor = cursor_after.unwrap_or_else(|| {
341 let line_count = runtime.buffer_line_count(buffer_id).unwrap_or(1);
342 let clamped_line = start.line.min(line_count.saturating_sub(1));
343 let line_len = runtime
344 .buffer_line_len(buffer_id, clamped_line)
345 .unwrap_or(0);
346 let clamped_col = if line_len == 0 {
347 0
348 } else {
349 start.column.min(line_len.saturating_sub(1))
350 };
351 Position::new(clamped_line, clamped_col)
352 });
353
354 if let Some(window) = runtime.windows_mut().active_mut() {
355 window.cursor = new_cursor.into();
356 tracing::debug!(
357 ?new_cursor,
358 operator = operator.id(),
359 "Updated cursor after text-modifying operator"
360 );
361 }
362 }
363
364 if operator.is_text_modifying() {
370 runtime.record_buffer_modified(buffer_id);
371 }
372
373 CommandResult::Success
374 }
375 Err(e) => CommandResult::error(&e.to_string()),
376 }
377}
378
379#[must_use]
385pub fn operator_commands() -> Vec<Box<dyn CommandHandler>> {
386 vec![
387 Box::new(DeleteCommand),
388 Box::new(YankCommand),
389 Box::new(ChangeCommand),
390 Box::new(LowercaseCommand),
391 Box::new(UppercaseCommand),
392 Box::new(ToggleCaseCommand),
393 ]
394}
395
396