use {
reovim_driver_command::{Command, CommandContext, CommandHandler, CommandResult},
reovim_driver_session::{
BufferApi, SessionRuntime, TransitionContext,
api::{ChangeTracker, ModeApi, RegisterContent, Selection, SelectionMode},
},
reovim_kernel::api::v1::{CommandId, Position},
};
use crate::{ids, modes::VimMode};
fn expand_selection_range(
selection: &Selection,
end_line_len: Option<usize>,
total_lines: usize,
) -> (Position, Position, bool) {
let start = selection.start;
let end = selection.end;
match selection.mode {
SelectionMode::Line => {
let start = Position::new(start.line, 0);
let end_line_len = end_line_len.unwrap_or(0);
let end = if end.line < total_lines {
Position::new(end.line, 0)
} else {
Position::new(end.line - 1, end_line_len)
};
(start, end, true)
}
SelectionMode::Character | SelectionMode::Block => (start, end, false),
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct DeleteSelection;
impl Command for DeleteSelection {
fn id(&self) -> CommandId {
ids::DELETE_SELECTION
}
fn description(&self) -> &'static str {
"Delete visual selection"
}
}
impl CommandHandler for DeleteSelection {
#[cfg_attr(coverage_nightly, coverage(off))]
fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
let Some(buffer_id) = args.buffer_id() else {
return CommandResult::error("No active buffer");
};
let Some(selection) = runtime.windows().active().and_then(|w| w.selection.clone()) else {
return CommandResult::Success; };
let end_line_len = runtime.buffer_line_len(buffer_id, selection.end.line);
let total_lines = runtime.buffer_line_count(buffer_id).unwrap_or(1);
let (start, end, is_linewise) =
expand_selection_range(&selection, end_line_len, total_lines);
let cursor_pos = start;
if let Some(text) = runtime.buffer_text_range(buffer_id, start, end) {
let content = if is_linewise {
RegisterContent::linewise(&text)
} else {
RegisterContent::characterwise(&text)
};
runtime.store_register_with_sync(args.register(), content);
}
runtime.delete_range(buffer_id, start, end);
if let Some(window) = runtime.windows_mut().active_mut() {
window.selection = None;
window.cursor = cursor_pos.into();
}
runtime.record_selection_change(buffer_id);
runtime.set_mode(VimMode::NORMAL_ID, TransitionContext::new());
CommandResult::Success
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct YankSelection;
impl Command for YankSelection {
fn id(&self) -> CommandId {
ids::YANK_SELECTION
}
fn description(&self) -> &'static str {
"Yank visual selection"
}
}
impl CommandHandler for YankSelection {
#[cfg_attr(coverage_nightly, coverage(off))]
fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
let Some(buffer_id) = args.buffer_id() else {
return CommandResult::error("No active buffer");
};
let Some(selection) = runtime.windows().active().and_then(|w| w.selection.clone()) else {
return CommandResult::Success; };
let end_line_len = runtime.buffer_line_len(buffer_id, selection.end.line);
let total_lines = runtime.buffer_line_count(buffer_id).unwrap_or(1);
let (start, end, is_linewise) =
expand_selection_range(&selection, end_line_len, total_lines);
if let Some(text) = runtime.buffer_text_range(buffer_id, start, end) {
let content = if is_linewise {
RegisterContent::linewise(&text)
} else {
RegisterContent::characterwise(&text)
};
runtime.store_register_with_sync(args.register(), content);
}
if let Some(window) = runtime.windows_mut().active_mut() {
window.selection = None;
}
runtime.record_selection_change(buffer_id);
runtime.set_mode(VimMode::NORMAL_ID, TransitionContext::new());
CommandResult::Success
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ChangeSelection;
impl Command for ChangeSelection {
fn id(&self) -> CommandId {
ids::CHANGE_SELECTION
}
fn description(&self) -> &'static str {
"Change visual selection (delete and enter insert mode)"
}
}
impl CommandHandler for ChangeSelection {
#[cfg_attr(coverage_nightly, coverage(off))]
fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
let Some(buffer_id) = args.buffer_id() else {
return CommandResult::error("No active buffer");
};
let Some(selection) = runtime.windows().active().and_then(|w| w.selection.clone()) else {
return CommandResult::Success; };
let end_line_len = runtime.buffer_line_len(buffer_id, selection.end.line);
let total_lines = runtime.buffer_line_count(buffer_id).unwrap_or(1);
let (start, end, is_linewise) =
expand_selection_range(&selection, end_line_len, total_lines);
let cursor_pos = start;
if let Some(text) = runtime.buffer_text_range(buffer_id, start, end) {
let content = if is_linewise {
RegisterContent::linewise(&text)
} else {
RegisterContent::characterwise(&text)
};
runtime.store_register_with_sync(args.register(), content);
}
runtime.delete_range(buffer_id, start, end);
if let Some(window) = runtime.windows_mut().active_mut() {
window.selection = None;
window.cursor = cursor_pos.into();
}
runtime.record_selection_change(buffer_id);
runtime.set_mode(VimMode::INSERT_ID, TransitionContext::new());
CommandResult::Success
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct IndentSelection;
impl Command for IndentSelection {
fn id(&self) -> CommandId {
ids::INDENT_SELECTION
}
fn description(&self) -> &'static str {
"Indent visual selection"
}
}
impl CommandHandler for IndentSelection {
#[cfg_attr(coverage_nightly, coverage(off))]
fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
let Some(buffer_id) = args.buffer_id() else {
return CommandResult::error("No active buffer");
};
let Some(selection) = runtime.windows().active().and_then(|w| w.selection.clone()) else {
return CommandResult::Success; };
let start_line = selection.start.line;
let end_line = selection.end.line;
let indent = " ";
for line_idx in start_line..end_line {
runtime.insert_text(buffer_id, Position::new(line_idx, 0), indent);
}
if let Some(window) = runtime.windows_mut().active_mut() {
window.selection = None;
}
runtime.record_selection_change(buffer_id);
runtime.set_mode(VimMode::NORMAL_ID, TransitionContext::new());
CommandResult::Success
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct DedentSelection;
impl Command for DedentSelection {
fn id(&self) -> CommandId {
ids::DEDENT_SELECTION
}
fn description(&self) -> &'static str {
"Dedent visual selection"
}
}
impl CommandHandler for DedentSelection {
#[cfg_attr(coverage_nightly, coverage(off))]
fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
let Some(buffer_id) = args.buffer_id() else {
return CommandResult::error("No active buffer");
};
let Some(selection) = runtime.windows().active().and_then(|w| w.selection.clone()) else {
return CommandResult::Success; };
let start_line = selection.start.line;
let end_line = selection.end.line;
for line_idx in start_line..end_line {
if let Some(line) = runtime.buffer_line(buffer_id, line_idx) {
let mut chars_to_remove = 0;
for (i, c) in line.chars().enumerate() {
if c == '\t' {
chars_to_remove = i + 1;
break;
} else if c == ' ' && i < 4 {
chars_to_remove = i + 1;
} else {
break;
}
}
if chars_to_remove > 0 {
let start = Position::new(line_idx, 0);
let end = Position::new(line_idx, chars_to_remove);
runtime.delete_range(buffer_id, start, end);
}
}
}
if let Some(window) = runtime.windows_mut().active_mut() {
window.selection = None;
}
runtime.record_selection_change(buffer_id);
runtime.set_mode(VimMode::NORMAL_ID, TransitionContext::new());
CommandResult::Success
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn execute_case_selection(
runtime: &mut SessionRuntime<'_>,
args: &CommandContext,
transform: fn(&str) -> String,
) -> CommandResult {
let Some(buffer_id) = args.buffer_id() else {
return CommandResult::error("No active buffer");
};
let Some(selection) = runtime.windows().active().and_then(|w| w.selection.clone()) else {
return CommandResult::Success;
};
let end_line_len = runtime.buffer_line_len(buffer_id, selection.end.line);
let total_lines = runtime.buffer_line_count(buffer_id).unwrap_or(1);
let (start, end, _is_linewise) = expand_selection_range(&selection, end_line_len, total_lines);
if let Some(text) = runtime.buffer_text_range(buffer_id, start, end) {
let transformed = transform(&text);
if transformed != text {
runtime.delete_range(buffer_id, start, end);
runtime.insert_text(buffer_id, start, &transformed);
}
}
if let Some(window) = runtime.windows_mut().active_mut() {
window.selection = None;
window.cursor = start.into();
}
runtime.record_selection_change(buffer_id);
runtime.set_mode(VimMode::NORMAL_ID, TransitionContext::new());
CommandResult::Success
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ToggleCaseSelection;
impl Command for ToggleCaseSelection {
fn id(&self) -> CommandId {
ids::TOGGLE_CASE_SELECTION
}
fn description(&self) -> &'static str {
"Toggle case of visual selection"
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl CommandHandler for ToggleCaseSelection {
fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
execute_case_selection(runtime, args, |s| {
s.chars()
.map(|c| {
if c.is_uppercase() {
c.to_lowercase().next().unwrap_or(c)
} else if c.is_lowercase() {
c.to_uppercase().next().unwrap_or(c)
} else {
c
}
})
.collect()
})
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct LowercaseSelection;
impl Command for LowercaseSelection {
fn id(&self) -> CommandId {
ids::LOWERCASE_SELECTION
}
fn description(&self) -> &'static str {
"Lowercase visual selection"
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl CommandHandler for LowercaseSelection {
fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
execute_case_selection(runtime, args, str::to_lowercase)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct UppercaseSelection;
impl Command for UppercaseSelection {
fn id(&self) -> CommandId {
ids::UPPERCASE_SELECTION
}
fn description(&self) -> &'static str {
"Uppercase visual selection"
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl CommandHandler for UppercaseSelection {
fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
execute_case_selection(runtime, args, str::to_uppercase)
}
}
#[cfg(test)]
#[allow(clippy::significant_drop_tightening, clippy::uninlined_format_args)]
#[path = "tests/operators.rs"]
mod tests;