diff --git a/helix-term/src/commands/evil.rs b/helix-term/src/commands/evil.rs index aa29c01c..915cc6ec 100644 --- a/helix-term/src/commands/evil.rs +++ b/helix-term/src/commands/evil.rs @@ -3,9 +3,9 @@ use std::{ sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}, }; -use helix_core::movement::move_next_word_end; use helix_core::movement::move_prev_word_start; use helix_core::movement::{is_word_boundary, Direction}; +use helix_core::{movement::move_next_word_end, Rope}; use helix_core::{Range, Selection, Transaction}; use helix_view::document::Mode; use helix_view::input::KeyEvent; @@ -19,6 +19,7 @@ use super::select_mode; enum Command { Yank, Delete, + Change, } impl TryFrom for Command { @@ -26,6 +27,7 @@ impl TryFrom for Command { fn try_from(value: char) -> Result { match value { + 'c' => Ok(Command::Change), 'd' => Ok(Command::Delete), 'y' => Ok(Command::Yank), _ => Err(()), @@ -50,7 +52,7 @@ impl TryFrom for Modifier { } } -#[derive(Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq)] enum Motion { PrevWordStart, NextWordEnd, @@ -199,6 +201,7 @@ impl EvilCommands { Self::context().modifiers.contains(&Modifier::InnerWord); if let Some(motion) = Self::context().motion.as_ref() { + log::trace!("Calculating selection using motion: {:?}", motion); // A motion was specified: Select accordingly // TODO: handle other motion keys as well selection = match motion { @@ -226,7 +229,16 @@ impl EvilCommands { // The inner word modifier isn't valid for a line-based selection if !has_inner_word_modifier { // No motion was specified: Perform a line-based selection - selection = Some(Self::get_full_line_based_selection(cx)); + log::trace!("No motion was specified: Perform a line-based selection"); + + // If the command is a change command, do not include the final line break, + // to ensure an empty line is left in place. + selection = Some(Self::get_full_line_based_selection( + cx, + !Self::context() + .command + .is_some_and(|command| command == Command::Change), + )); } } } @@ -386,7 +398,10 @@ impl EvilCommands { return Ok(selection); } - fn get_full_line_based_selection(cx: &mut Context) -> Selection { + fn get_full_line_based_selection( + cx: &mut Context, + include_final_line_break: bool, + ) -> Selection { let (view, doc) = current!(cx.editor); let lines_to_select = Self::context().count.unwrap_or(1); @@ -394,6 +409,11 @@ impl EvilCommands { let text = doc.text(); let extend = Extend::Below; + log::trace!("Calculating full line-based selection (lines to select: {}, extend below: {}, include final line break: {})", lines_to_select, match extend { + Extend::Above => false, + Extend::Below => true, + }, include_final_line_break); + // Process a number of lines: first create a temporary selection of the text to be processed return doc.selection(view.id).clone().transform(|range| { let (start_line, end_line) = range.line_range(text.slice(..)); @@ -402,7 +422,7 @@ impl EvilCommands { let end: usize = text.line_to_char((end_line + lines_to_select).min(text.len_lines())); // Extend to previous/next line if current line is selected - let (anchor, head) = if range.from() == start && range.to() == end { + let (mut anchor, mut head) = if range.from() == start && range.to() == end { match extend { Extend::Above => (end, text.line_to_char(start_line.saturating_sub(1))), Extend::Below => ( @@ -414,10 +434,37 @@ impl EvilCommands { (start, end) }; + // Strip the final line break if requested + if !include_final_line_break { + (anchor, head) = Self::strip_trailing_line_break(text, (anchor, head)); + } + Range::new(anchor, head) }); } + fn strip_trailing_line_break(text: &Rope, range: (usize, usize)) -> (usize, usize) { + let start = range.0.min(range.1); + let mut end = range.0.max(range.1); + let inversed = range.0 > range.1; + + // The end points to the next char, not to the last char which would be selected + if end.saturating_sub(start) >= 2 && text.char(end - 1) == '\n' { + end -= 1; + + // The line might end with CR & LF; in that case, strip CR as well + if end.saturating_sub(start) >= 2 && text.char(end - 1) == '\r' { + end -= 1; + } + } + + return if !inversed { + (start, end) + } else { + (end, start) + }; + } + fn yank_selection(cx: &mut Context, selection: &Selection, _set_status_message: bool) { let (_view, doc) = current!(cx.editor); @@ -482,7 +529,7 @@ impl EvilCommands { Command::Yank => { Self::yank_selection(cx, &selection, true); } - Command::Delete => { + Command::Change | Command::Delete => { Self::delete_selection(cx, &selection, true); } } @@ -525,17 +572,24 @@ impl EvilCommands { set_mode = context.set_mode; } + log::trace!("Key callback invoked, active command: {:?}", active_command); + // Is the command being executed? if let Some(command) = e.char().and_then(|c| Command::try_from(c).ok()) { // Assume this callback is called only if a command was initiated if command == active_command { + log::trace!("The active command is being executed: {:?}", active_command); Self::evil_command(cx, active_command, set_mode); + return; } else { - // A command was initiated, but another command was initiated. - Self::context_mut().reset(); + log::debug!( + "A command ({:?}) was active, but another command ({:?}) has been initiated", + active_command, + command + ); + //Self::context_mut().reset(); // TODO: proceed with initiating the other command? } - return; } // Is the command receiving a new/increased count? @@ -550,7 +604,7 @@ impl EvilCommands { if value != 0 || evil_context.count.is_some() { evil_context.count = Some(evil_context.count.map(|c| c * 10).unwrap_or(0) + value); - log::info!( + log::trace!( "Key callback: Increasing count to {}", evil_context.count.unwrap() ); @@ -567,7 +621,7 @@ impl EvilCommands { if let Some(c) = e.char() { // Is the command receiving a modifier? if let Some(modifier) = Modifier::try_from(c).ok() { - log::info!("Key callback: Detected modifier key '{}'", c); + log::trace!("Key callback: Detected modifier key '{}'", c); Self::context_mut().modifiers.push(modifier); @@ -583,7 +637,7 @@ impl EvilCommands { // Check this after the count check, because "0" could imply increasing the count, // and if it doesn't, it's probably a motion key. if let Some(motion) = e.char().and_then(|c| Motion::try_from(c).ok()) { - log::info!("Key callback: Detected motion key '{}'", c); + log::trace!("Key callback: Detected motion key '{}'", c); Self::context_mut().motion = Some(motion); // TODO; a motion key should immediately execute the command @@ -607,7 +661,10 @@ impl EvilCommands { pub fn delete(cx: &mut Context, op: Operation) { Self::evil_command( cx, - Command::Delete, + match op { + Operation::Delete => Command::Delete, + Operation::Change => Command::Change, + }, Some(match op { Operation::Delete => Mode::Normal, Operation::Change => Mode::Insert,