Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(evil): change line #45

Merged
merged 2 commits into from
Oct 7, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 70 additions & 13 deletions helix-term/src/commands/evil.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,13 +19,15 @@ use super::select_mode;
enum Command {
Yank,
Delete,
Change,
}

impl TryFrom<char> for Command {
type Error = ();

fn try_from(value: char) -> Result<Self, Self::Error> {
match value {
'c' => Ok(Command::Change),
'd' => Ok(Command::Delete),
'y' => Ok(Command::Yank),
_ => Err(()),
Expand All @@ -50,7 +52,7 @@ impl TryFrom<char> for Modifier {
}
}

#[derive(Eq, PartialEq)]
#[derive(Debug, Eq, PartialEq)]
enum Motion {
PrevWordStart,
NextWordEnd,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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),
));
}
}
}
Expand Down Expand Up @@ -386,14 +398,22 @@ 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);

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(..));
Expand All @@ -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 => (
Expand All @@ -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);

Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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?
Expand All @@ -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()
);
Expand All @@ -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);

Expand All @@ -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
Expand All @@ -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,
Expand Down