use {
reovim_driver_command::{Command, CommandContext, CommandHandler, CommandResult},
reovim_driver_session::{BufferApi, SessionRuntime, TransitionContext, api::ModeApi},
reovim_kernel::api::v1::{CommandId, Position},
};
use {
super::{
DeleteOperator, LowercaseOperator, Operator, OperatorContext, Range, ToggleCaseOperator,
UppercaseOperator, YankOperator,
},
crate::ids::MODULE,
};
#[derive(Debug, Clone, Copy, Default)]
pub struct DeleteCommand;
impl Command for DeleteCommand {
fn id(&self) -> CommandId {
CommandId::new(MODULE, "delete")
}
fn description(&self) -> &'static str {
"Delete text in range"
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl CommandHandler for DeleteCommand {
fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
execute_operator(&DeleteOperator, runtime, args)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct YankCommand;
impl Command for YankCommand {
fn id(&self) -> CommandId {
CommandId::new(MODULE, "yank")
}
fn description(&self) -> &'static str {
"Yank text in range to register"
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl CommandHandler for YankCommand {
#[cfg_attr(coverage_nightly, coverage(off))]
fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
let (start_line, start_col) = args.range_start().unwrap_or((0, 0));
let restore_pos = Position::new(start_line, start_col);
let cur_pos = runtime
.windows()
.active()
.map(|w| Position::new(w.cursor.line, w.cursor.column));
tracing::debug!(
range_start = ?restore_pos,
current_pos = ?cur_pos,
"yank command: before execute"
);
let result = execute_operator(&YankOperator, runtime, args);
if matches!(result, CommandResult::Success) && args.buffer_id().is_some() {
if let Some(window) = runtime.windows_mut().active_mut() {
window.cursor = restore_pos.into();
}
let cur_pos = runtime
.windows()
.active()
.map(|w| Position::new(w.cursor.line, w.cursor.column));
tracing::debug!(
restored_to = ?restore_pos,
current_pos = ?cur_pos,
"yank command: cursor restored"
);
if let Some(buffer_id) = args.buffer_id() {
use reovim_driver_session::api::ExtensionApi;
let (end_line, end_col) = args.range_end().unwrap_or((0, 0));
let state = runtime.ext_mut::<super::yank_flash::YankFlashState>();
state.record(
buffer_id,
start_line,
start_col,
end_line,
end_col,
args.is_linewise(),
);
}
}
result
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ChangeCommand;
impl Command for ChangeCommand {
fn id(&self) -> CommandId {
CommandId::new(MODULE, "change")
}
fn description(&self) -> &'static str {
"Change text in range (delete and enter insert)"
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl CommandHandler for ChangeCommand {
#[cfg_attr(coverage_nightly, coverage(off))]
fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
use {
super::ChangeOperator,
crate::modes::VimMode,
reovim_driver_undo::{UndoKey, UndoProviderRegistry},
};
if let Some(buffer_id) = args.buffer_id()
&& let Some(window) = runtime.windows().active()
&& let Some(undo_registry) = runtime.kernel().services.get::<UndoProviderRegistry>()
&& let Some(undo_provider) = undo_registry.get(&UndoKey::Buffer)
{
let pos = Position::new(window.cursor.line, window.cursor.column);
undo_provider.begin_batch(buffer_id, pos);
}
let result = execute_operator(&ChangeOperator, runtime, args);
if matches!(result, CommandResult::Success) {
runtime.set_mode(VimMode::INSERT_ID, TransitionContext::new());
}
result
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct LowercaseCommand;
impl Command for LowercaseCommand {
fn id(&self) -> CommandId {
CommandId::new(MODULE, "lowercase")
}
fn description(&self) -> &'static str {
"Lowercase text in range"
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl CommandHandler for LowercaseCommand {
fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
execute_operator(&LowercaseOperator, runtime, args)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct UppercaseCommand;
impl Command for UppercaseCommand {
fn id(&self) -> CommandId {
CommandId::new(MODULE, "uppercase")
}
fn description(&self) -> &'static str {
"Uppercase text in range"
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl CommandHandler for UppercaseCommand {
fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
execute_operator(&UppercaseOperator, runtime, args)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ToggleCaseCommand;
impl Command for ToggleCaseCommand {
fn id(&self) -> CommandId {
CommandId::new(MODULE, "toggle-case-op")
}
fn description(&self) -> &'static str {
"Toggle case of text in range"
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl CommandHandler for ToggleCaseCommand {
fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
execute_operator(&ToggleCaseOperator, runtime, args)
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn execute_operator(
operator: &dyn Operator,
runtime: &mut SessionRuntime<'_>,
args: &CommandContext,
) -> CommandResult {
let Some(buffer_id) = args.buffer_id() else {
return CommandResult::error("No active buffer");
};
let (start_line, start_col) = args.range_start().unwrap_or((0, 0));
let (end_line, end_col) = args.range_end().unwrap_or((0, 0));
let linewise = args.is_linewise();
let start = Position::new(start_line, start_col);
let end = Position::new(end_line, end_col);
let range = if linewise {
Range::linewise(start, end)
} else {
Range::new(start, end)
};
let count = args.count().unwrap_or(1);
let register = super::registers::option_char_to_register(args.register());
let cursor_position = runtime
.windows()
.active()
.map_or_else(Position::origin, |w| Position::new(w.cursor.line, w.cursor.column));
let (kernel, registers, clipboard_history) = runtime.kernel_and_registers();
let mut op_ctx = OperatorContext {
kernel,
registers,
clipboard_history,
buffer_id,
register,
count,
cursor_position,
cursor_after: None,
};
match operator.execute(&mut op_ctx, range) {
Ok(()) => {
let cursor_after = op_ctx.cursor_after;
if operator.is_text_modifying() {
let new_cursor = cursor_after.unwrap_or_else(|| {
let line_count = runtime.buffer_line_count(buffer_id).unwrap_or(1);
let clamped_line = start.line.min(line_count.saturating_sub(1));
let line_len = runtime
.buffer_line_len(buffer_id, clamped_line)
.unwrap_or(0);
let clamped_col = if line_len == 0 {
0
} else {
start.column.min(line_len.saturating_sub(1))
};
Position::new(clamped_line, clamped_col)
});
if let Some(window) = runtime.windows_mut().active_mut() {
window.cursor = new_cursor.into();
tracing::debug!(
?new_cursor,
operator = operator.id(),
"Updated cursor after text-modifying operator"
);
}
}
if operator.is_text_modifying() {
runtime.record_buffer_modified(buffer_id);
}
CommandResult::Success
}
Err(e) => CommandResult::error(&e.to_string()),
}
}
#[must_use]
pub fn operator_commands() -> Vec<Box<dyn CommandHandler>> {
vec![
Box::new(DeleteCommand),
Box::new(YankCommand),
Box::new(ChangeCommand),
Box::new(LowercaseCommand),
Box::new(UppercaseCommand),
Box::new(ToggleCaseCommand),
]
}