#[cfg(feature = "keybindings")]
use crate::{
canvas::modes::AppMode,
canvas::state::SelectionState,
editor::behavior::{VimOperator, VimPendingOperator},
keybindings::{CanvasKeyAction, KeyEventOutcome},
textarea::actions::selection::vim::VimPending,
textarea::{TextAreaDataProvider, TextAreaState},
};
#[cfg(feature = "keybindings")]
#[derive(Clone, Copy, PartialEq, Eq)]
enum MotionKind {
Linewise,
CharExclusive,
CharInclusive,
Unsupported,
}
#[cfg(feature = "keybindings")]
fn motion_kind(action: &CanvasKeyAction) -> MotionKind {
use CanvasKeyAction::*;
match action {
MoveWordNext
| MoveBigWordNext
| MoveWordPrev
| MoveBigWordPrev
| MoveLeft
| MoveRight
| MoveLineStart
| GotoFirstNonWhitespace => MotionKind::CharExclusive,
MoveWordEnd | MoveBigWordEnd | MoveWordEndPrev | MoveBigWordEndPrev | MoveLineEnd => {
MotionKind::CharInclusive
}
MoveUp | MoveDown | MoveFirstLine | MoveLastLine => MotionKind::Linewise,
_ => MotionKind::Unsupported,
}
}
#[cfg(feature = "keybindings")]
impl<P: TextAreaDataProvider> TextAreaState<P> {
pub(crate) fn begin_operator_vim(&mut self, operator: VimOperator, count: usize) {
let anchor = (self.current_field(), self.cursor_position());
self.core
.behavior_state
.vim_mut()
.set_pending_operator(VimPendingOperator {
operator,
count: count.max(1),
anchor,
});
}
fn field_len_vim(&self, field: usize) -> usize {
self.core.data_provider().field_value(field).chars().count()
}
fn dec_position_vim(&self, pos: (usize, usize)) -> Option<(usize, usize)> {
if pos.1 > 0 {
Some((pos.0, pos.1 - 1))
} else if pos.0 > 0 {
Some((pos.0 - 1, self.field_len_vim(pos.0 - 1).saturating_sub(1)))
} else {
None
}
}
pub(crate) fn apply_operator_motion_vim(
&mut self,
action: &CanvasKeyAction,
motion_count: usize,
) -> KeyEventOutcome {
let Some(pending) = self.core.behavior_state.vim().pending_operator() else {
return self.execute_canvas_key_action(action, motion_count);
};
let total = pending.count.saturating_mul(motion_count.max(1)).max(1);
if matches!(
action,
CanvasKeyAction::OperatorDelete
| CanvasKeyAction::OperatorChange
| CanvasKeyAction::OperatorYank
) {
self.core.behavior_state.vim_mut().clear_pending_operator();
let start = pending.anchor.0;
self.apply_operator_linewise_vim(
pending.operator,
start,
start.saturating_add(total).saturating_sub(1),
);
return KeyEventOutcome::Consumed(None);
}
let pending_find = match action {
CanvasKeyAction::FindNextChar => Some((false, true)),
CanvasKeyAction::FindPrevChar => Some((false, false)),
CanvasKeyAction::TillNextChar => Some((true, true)),
CanvasKeyAction::TillPrevChar => Some((true, false)),
_ => None,
};
if let Some((till, forward)) = pending_find {
self.set_vim_pending(VimPending::Find {
till,
forward,
count: total,
});
return KeyEventOutcome::Consumed(None);
}
if matches!(
action,
CanvasKeyAction::RepeatLastFind | CanvasKeyAction::RepeatLastFindReverse
) {
self.core.behavior_state.vim_mut().clear_pending_operator();
if let Some(find) = self.vim_last_find {
let reverse = matches!(action, CanvasKeyAction::RepeatLastFindReverse);
let forward = if reverse { !find.forward } else { find.forward };
self.repeat_last_find_vim(reverse, total);
self.finish_operator_charwise_vim(pending.operator, pending.anchor, forward);
}
return KeyEventOutcome::Consumed(None);
}
let resolved = if pending.operator == VimOperator::Change {
match action {
CanvasKeyAction::MoveWordNext => CanvasKeyAction::MoveWordEnd,
CanvasKeyAction::MoveBigWordNext => CanvasKeyAction::MoveBigWordEnd,
other => other.clone(),
}
} else {
action.clone()
};
match motion_kind(&resolved) {
MotionKind::Linewise => {
self.core.behavior_state.vim_mut().clear_pending_operator();
let start_field = pending.anchor.0;
let _ = self.execute_canvas_key_action(&resolved, total);
let end_field = self.current_field();
let relative = matches!(
resolved,
CanvasKeyAction::MoveDown | CanvasKeyAction::MoveUp
);
if relative && end_field == start_field {
return KeyEventOutcome::Consumed(None);
}
self.apply_operator_linewise_vim(
pending.operator,
start_field.min(end_field),
start_field.max(end_field),
);
KeyEventOutcome::Consumed(None)
}
MotionKind::CharExclusive | MotionKind::CharInclusive => {
let mut inclusive = motion_kind(&resolved) == MotionKind::CharInclusive;
let forward_word = matches!(
resolved,
CanvasKeyAction::MoveWordNext | CanvasKeyAction::MoveBigWordNext
);
let mut stalled = false;
for _ in 0..total {
let before = (self.current_field(), self.cursor_position());
let _ = self.execute_canvas_key_action(&resolved, 1);
if (self.current_field(), self.cursor_position()) == before {
stalled = true;
break;
}
}
if forward_word && stalled {
let field = self.current_field();
let len = self.field_len_vim(field);
if len > 0 {
self.core.ui_state.set_cursor(len - 1, len, false);
inclusive = true;
}
}
self.core.behavior_state.vim_mut().clear_pending_operator();
self.finish_operator_charwise_vim(pending.operator, pending.anchor, inclusive);
KeyEventOutcome::Consumed(None)
}
MotionKind::Unsupported => {
self.core.behavior_state.vim_mut().clear_pending_operator();
KeyEventOutcome::Consumed(None)
}
}
}
pub(crate) fn finish_operator_charwise_vim(
&mut self,
operator: VimOperator,
anchor: (usize, usize),
inclusive: bool,
) {
let cursor = (self.current_field(), self.cursor_position());
if cursor == anchor {
return; }
let (lo, mut hi) = if anchor <= cursor {
(anchor, cursor)
} else {
(cursor, anchor)
};
if !inclusive {
match self.dec_position_vim(hi) {
Some(p) if p >= lo => hi = p,
_ => return, }
}
self.core.ui_state.selection = SelectionState::Characterwise { anchor: lo };
let _ = self.transition_to_field(hi.0);
let len = self.field_len_vim(hi.0);
self.core.ui_state.set_cursor(hi.1, len, false);
match operator {
VimOperator::Delete => {
self.delete_selection_once(true);
self.core.ui_state.selection = SelectionState::None;
}
VimOperator::Yank => {
self.yank_selection();
let _ = self.transition_to_field(lo.0);
self.set_cursor_position(lo.1);
self.core.ui_state.selection = SelectionState::None;
}
VimOperator::Change => {
self.delete_selection_once(true);
self.core.ui_state.selection = SelectionState::None;
self.enter_edit_mode_vim();
}
}
}
fn apply_operator_linewise_vim(
&mut self,
operator: VimOperator,
start_field: usize,
end_field: usize,
) {
let last = self.core.data_provider().field_count().saturating_sub(1);
let start = start_field.min(last);
let end = end_field.min(last);
let count = end - start + 1;
let _ = self.transition_to_field(start);
match operator {
VimOperator::Delete => {
self.core.ui_state.selection = SelectionState::Linewise {
anchor_field: start,
};
let _ = self.transition_to_field(end);
self.delete_selection_once(true);
self.core.ui_state.selection = SelectionState::None;
}
VimOperator::Yank => {
self.core.ui_state.selection = SelectionState::Linewise {
anchor_field: start,
};
let _ = self.transition_to_field(end);
self.yank_selection();
let _ = self.transition_to_field(start);
self.move_line_start();
self.core.ui_state.selection = SelectionState::None;
if self.mode() != AppMode::Nor {
self.set_mode_vim(AppMode::Nor);
}
}
VimOperator::Change => {
self.core.ui_state.selection = SelectionState::Linewise {
anchor_field: start,
};
let _ = self.transition_to_field(end);
self.yank_selection();
self.core.ui_state.selection = SelectionState::None;
let _ = self.transition_to_field(start);
if count <= 1 {
self.change_current_line();
} else {
let _ = self.transition_to_field(start + 1);
self.delete_current_lines(count - 1);
let _ = self.transition_to_field(start);
self.change_current_line();
}
}
}
}
}