use super::buffer::VimEditor;
use super::MotionRange;
impl VimEditor {
pub fn move_left(&mut self, count: usize) {
self.cursor_col = self.cursor_col.saturating_sub(count);
}
pub fn move_right(&mut self, count: usize) {
let max = match self.mode {
super::VimMode::Insert => self.current_line_len(),
_ => self.current_line_len().saturating_sub(1),
};
self.cursor_col = (self.cursor_col + count).min(max);
}
pub fn move_down(&mut self, count: usize) {
self.cursor_row = (self.cursor_row + count).min(self.lines.len().saturating_sub(1));
self.clamp_cursor();
}
pub fn move_up(&mut self, count: usize) {
self.cursor_row = self.cursor_row.saturating_sub(count);
self.clamp_cursor();
}
pub fn move_to_line_start(&mut self) {
self.cursor_col = 0;
}
pub fn move_to_first_non_blank(&mut self) {
let line = self.current_line();
self.cursor_col = line.len() - line.trim_start().len();
}
pub fn move_to_line_end(&mut self) {
let len = self.current_line_len();
self.cursor_col = match self.mode {
super::VimMode::Insert => len,
_ => len.saturating_sub(1),
};
}
pub fn move_to_top(&mut self) {
self.cursor_row = 0;
self.cursor_col = 0;
self.clamp_cursor();
}
pub fn move_to_bottom(&mut self) {
self.cursor_row = self.lines.len().saturating_sub(1);
self.clamp_cursor();
}
pub fn move_to_line(&mut self, line_num: usize) {
if line_num > 0 {
self.cursor_row = (line_num - 1).min(self.lines.len().saturating_sub(1));
}
self.clamp_cursor();
}
pub fn half_page_down(&mut self) {
let half = self.visible_height / 2;
self.scroll_offset = self.scroll_offset.saturating_add(half);
self.cursor_row = (self.cursor_row + half).min(self.lines.len().saturating_sub(1));
self.clamp_cursor();
}
pub fn half_page_up(&mut self) {
let half = self.visible_height / 2;
self.scroll_offset = self.scroll_offset.saturating_sub(half);
self.cursor_row = self.cursor_row.saturating_sub(half);
self.clamp_cursor();
}
pub fn move_word_forward(&mut self, count: usize, big_word: bool) {
for _ in 0..count {
self.next_word_start(big_word);
}
}
pub fn move_word_end(&mut self, count: usize, big_word: bool) {
for _ in 0..count {
self.next_word_end(big_word);
}
}
pub fn move_word_back(&mut self, count: usize, big_word: bool) {
for _ in 0..count {
self.prev_word_start(big_word);
}
}
fn is_word_char(c: char, big_word: bool) -> bool {
if big_word {
!c.is_whitespace()
} else {
c.is_alphanumeric() || c == '_'
}
}
fn next_word_start(&mut self, big_word: bool) {
let line = self.current_line().to_string();
let line_len = line.len();
let mut col = self.cursor_col;
if col < line_len {
let chars: Vec<char> = line.chars().collect();
let in_word = col < chars.len() && Self::is_word_char(chars[col], big_word);
if in_word {
while col < chars.len() && Self::is_word_char(chars[col], big_word) {
col += 1;
}
} else {
while col < chars.len()
&& !Self::is_word_char(chars[col], big_word)
&& !chars[col].is_whitespace()
{
col += 1;
}
}
while col < chars.len() && chars[col].is_whitespace() {
col += 1;
}
}
if col >= line_len {
if self.cursor_row + 1 < self.lines.len() {
self.cursor_row += 1;
let next_line = self.current_line().to_string();
let chars: Vec<char> = next_line.chars().collect();
col = 0;
while col < chars.len() && chars[col].is_whitespace() {
col += 1;
}
self.cursor_col = col;
}
} else {
self.cursor_col = col;
}
}
fn next_word_end(&mut self, big_word: bool) {
let mut col = self.cursor_col + 1;
let line = self.current_line().to_string();
let chars: Vec<char> = line.chars().collect();
while col < chars.len() && chars[col].is_whitespace() {
col += 1;
}
if col >= chars.len() {
if self.cursor_row + 1 < self.lines.len() {
self.cursor_row += 1;
let next = self.current_line().to_string();
let nc: Vec<char> = next.chars().collect();
col = 0;
while col < nc.len() && nc[col].is_whitespace() {
col += 1;
}
if col < nc.len() {
let in_word = Self::is_word_char(nc[col], big_word);
while col + 1 < nc.len() {
let next_is_word = Self::is_word_char(nc[col + 1], big_word);
if in_word != next_is_word {
break;
}
col += 1;
}
}
self.cursor_col = col;
}
return;
}
let in_word = Self::is_word_char(chars[col], big_word);
while col + 1 < chars.len() {
let next_is_word = Self::is_word_char(chars[col + 1], big_word);
if in_word != next_is_word {
break;
}
col += 1;
}
self.cursor_col = col;
}
fn prev_word_start(&mut self, big_word: bool) {
if self.cursor_col == 0 {
if self.cursor_row > 0 {
self.cursor_row -= 1;
self.cursor_col = self.current_line_len().saturating_sub(1);
}
return;
}
let line = self.current_line().to_string();
let chars: Vec<char> = line.chars().collect();
let mut col = self.cursor_col.min(chars.len()).saturating_sub(1);
while col > 0 && chars[col].is_whitespace() {
col -= 1;
}
let in_word = col < chars.len() && Self::is_word_char(chars[col], big_word);
while col > 0 {
let prev_is_word = Self::is_word_char(chars[col - 1], big_word);
if in_word != prev_is_word {
break;
}
col -= 1;
}
self.cursor_col = col;
}
pub fn compute_motion_range(
&self,
motion: &Motion,
count: usize,
) -> Option<MotionRange> {
let sr = self.cursor_row;
let sc = self.cursor_col;
match motion {
Motion::Left => Some(MotionRange {
start_row: sr,
start_col: sc.saturating_sub(count),
end_row: sr,
end_col: sc,
linewise: false,
}),
Motion::Right => {
let max = self.current_line_len();
Some(MotionRange {
start_row: sr,
start_col: sc,
end_row: sr,
end_col: (sc + count).min(max),
linewise: false,
})
}
Motion::Down => {
let er = (sr + count).min(self.lines.len().saturating_sub(1));
Some(MotionRange {
start_row: sr,
start_col: 0,
end_row: er,
end_col: 0,
linewise: true,
})
}
Motion::Up => {
let er = sr.saturating_sub(count);
Some(MotionRange {
start_row: er,
start_col: 0,
end_row: sr,
end_col: 0,
linewise: true,
})
}
Motion::LineStart => Some(MotionRange {
start_row: sr,
start_col: 0,
end_row: sr,
end_col: sc,
linewise: false,
}),
Motion::LineEnd => Some(MotionRange {
start_row: sr,
start_col: sc,
end_row: sr,
end_col: self.current_line_len(),
linewise: false,
}),
Motion::WordForward => {
let mut sim = self.clone_position();
sim.move_word_forward(count, false);
if sim.cursor_row == sr {
Some(MotionRange {
start_row: sr,
start_col: sc,
end_row: sr,
end_col: sim.cursor_col,
linewise: false,
})
} else {
Some(MotionRange {
start_row: sr,
start_col: sc,
end_row: sim.cursor_row,
end_col: sim.cursor_col,
linewise: false,
})
}
}
Motion::WordEnd => {
let mut sim = self.clone_position();
sim.move_word_end(count, false);
Some(MotionRange {
start_row: sr,
start_col: sc,
end_row: sim.cursor_row,
end_col: (sim.cursor_col + 1).min(
sim.lines
.get(sim.cursor_row)
.map(|l| l.len())
.unwrap_or(0),
),
linewise: false,
})
}
Motion::WordBack => {
let mut sim = self.clone_position();
sim.move_word_back(count, false);
Some(MotionRange {
start_row: sim.cursor_row,
start_col: sim.cursor_col,
end_row: sr,
end_col: sc,
linewise: false,
})
}
Motion::BigWordForward => {
let mut sim = self.clone_position();
sim.move_word_forward(count, true);
Some(MotionRange {
start_row: sr,
start_col: sc,
end_row: sim.cursor_row,
end_col: sim.cursor_col,
linewise: false,
})
}
Motion::BigWordEnd => {
let mut sim = self.clone_position();
sim.move_word_end(count, true);
Some(MotionRange {
start_row: sr,
start_col: sc,
end_row: sim.cursor_row,
end_col: (sim.cursor_col + 1).min(
sim.lines
.get(sim.cursor_row)
.map(|l| l.len())
.unwrap_or(0),
),
linewise: false,
})
}
Motion::BigWordBack => {
let mut sim = self.clone_position();
sim.move_word_back(count, true);
Some(MotionRange {
start_row: sim.cursor_row,
start_col: sim.cursor_col,
end_row: sr,
end_col: sc,
linewise: false,
})
}
Motion::Line => Some(MotionRange {
start_row: sr,
start_col: 0,
end_row: (sr + count).saturating_sub(1).min(self.lines.len().saturating_sub(1)),
end_col: 0,
linewise: true,
}),
Motion::ToTop => Some(MotionRange {
start_row: 0,
start_col: 0,
end_row: sr,
end_col: 0,
linewise: true,
}),
Motion::ToBottom => Some(MotionRange {
start_row: sr,
start_col: 0,
end_row: self.lines.len().saturating_sub(1),
end_col: 0,
linewise: true,
}),
Motion::FirstNonBlank => {
let line = self.current_line();
let first_non_blank = line.len() - line.trim_start().len();
let (s, e) = if first_non_blank <= sc {
(first_non_blank, sc)
} else {
(sc, first_non_blank)
};
Some(MotionRange {
start_row: sr,
start_col: s,
end_row: sr,
end_col: e,
linewise: false,
})
}
Motion::InnerWord => {
let (start, end) = self.find_inner_word();
Some(MotionRange {
start_row: sr,
start_col: start,
end_row: sr,
end_col: end,
linewise: false,
})
}
Motion::InnerQuote(quote_char) => {
if let Some((start, end)) = self.find_inner_delimited(*quote_char, *quote_char) {
Some(MotionRange {
start_row: sr,
start_col: start,
end_row: sr,
end_col: end,
linewise: false,
})
} else {
None
}
}
Motion::InnerParen(open, close) => {
if let Some((start, end)) = self.find_inner_delimited(*open, *close) {
Some(MotionRange {
start_row: sr,
start_col: start,
end_row: sr,
end_col: end,
linewise: false,
})
} else {
None
}
}
}
}
fn clone_position(&self) -> PositionSim<'_> {
PositionSim {
lines: &self.lines,
cursor_row: self.cursor_row,
cursor_col: self.cursor_col,
}
}
fn find_inner_word(&self) -> (usize, usize) {
let line = self.current_line();
let chars: Vec<char> = line.chars().collect();
if chars.is_empty() {
return (0, 0);
}
let col = self.cursor_col.min(chars.len().saturating_sub(1));
let c = chars[col];
let is_word = c.is_alphanumeric() || c == '_';
let mut start = col;
let mut end = col;
if is_word {
while start > 0 && (chars[start - 1].is_alphanumeric() || chars[start - 1] == '_') {
start -= 1;
}
while end + 1 < chars.len()
&& (chars[end + 1].is_alphanumeric() || chars[end + 1] == '_')
{
end += 1;
}
} else if c.is_whitespace() {
while start > 0 && chars[start - 1].is_whitespace() {
start -= 1;
}
while end + 1 < chars.len() && chars[end + 1].is_whitespace() {
end += 1;
}
} else {
while start > 0
&& !chars[start - 1].is_alphanumeric()
&& chars[start - 1] != '_'
&& !chars[start - 1].is_whitespace()
{
start -= 1;
}
while end + 1 < chars.len()
&& !chars[end + 1].is_alphanumeric()
&& chars[end + 1] != '_'
&& !chars[end + 1].is_whitespace()
{
end += 1;
}
}
(start, end + 1)
}
fn find_inner_delimited(&self, open: char, close: char) -> Option<(usize, usize)> {
let line = self.current_line();
let chars: Vec<char> = line.chars().collect();
let col = self.cursor_col.min(chars.len().saturating_sub(1));
let mut start = None;
for i in (0..=col).rev() {
if chars[i] == open {
start = Some(i + 1);
break;
}
}
let start = start?;
let mut end = None;
for (i, &ch) in chars.iter().enumerate().skip(col.max(start)) {
if ch == close {
end = Some(i);
break;
}
}
let end = end?;
if start <= end {
Some((start, end))
} else {
None
}
}
}
struct PositionSim<'a> {
lines: &'a [String],
cursor_row: usize,
cursor_col: usize,
}
impl<'a> PositionSim<'a> {
fn current_line_len(&self) -> usize {
self.lines.get(self.cursor_row).map(|l| l.len()).unwrap_or(0)
}
fn move_word_forward(&mut self, count: usize, big_word: bool) {
for _ in 0..count {
let line = self.lines.get(self.cursor_row).map(|s| s.as_str()).unwrap_or("");
let chars: Vec<char> = line.chars().collect();
let mut col = self.cursor_col;
if col < chars.len() {
let in_word = VimEditor::is_word_char(chars[col], big_word);
if in_word {
while col < chars.len() && VimEditor::is_word_char(chars[col], big_word) {
col += 1;
}
} else {
while col < chars.len()
&& !VimEditor::is_word_char(chars[col], big_word)
&& !chars[col].is_whitespace()
{
col += 1;
}
}
while col < chars.len() && chars[col].is_whitespace() {
col += 1;
}
}
if col >= line.len() && self.cursor_row + 1 < self.lines.len() {
self.cursor_row += 1;
let next = self.lines.get(self.cursor_row).map(|s| s.as_str()).unwrap_or("");
let nc: Vec<char> = next.chars().collect();
col = 0;
while col < nc.len() && nc[col].is_whitespace() {
col += 1;
}
self.cursor_col = col;
} else {
self.cursor_col = col.min(self.current_line_len());
}
}
}
fn move_word_end(&mut self, count: usize, big_word: bool) {
for _ in 0..count {
let line = self.lines.get(self.cursor_row).map(|s| s.as_str()).unwrap_or("");
let chars: Vec<char> = line.chars().collect();
let mut col = self.cursor_col + 1;
while col < chars.len() && chars[col].is_whitespace() {
col += 1;
}
if col >= chars.len() {
if self.cursor_row + 1 < self.lines.len() {
self.cursor_row += 1;
let next = self.lines.get(self.cursor_row).map(|s| s.as_str()).unwrap_or("");
let nc: Vec<char> = next.chars().collect();
col = 0;
while col < nc.len() && nc[col].is_whitespace() {
col += 1;
}
if col < nc.len() {
let in_word = VimEditor::is_word_char(nc[col], big_word);
while col + 1 < nc.len() {
if VimEditor::is_word_char(nc[col + 1], big_word) != in_word {
break;
}
col += 1;
}
}
self.cursor_col = col;
}
continue;
}
let in_word = VimEditor::is_word_char(chars[col], big_word);
while col + 1 < chars.len() {
if VimEditor::is_word_char(chars[col + 1], big_word) != in_word {
break;
}
col += 1;
}
self.cursor_col = col;
}
}
fn move_word_back(&mut self, count: usize, big_word: bool) {
for _ in 0..count {
if self.cursor_col == 0 {
if self.cursor_row > 0 {
self.cursor_row -= 1;
self.cursor_col = self.current_line_len().saturating_sub(1);
}
continue;
}
let line = self.lines.get(self.cursor_row).map(|s| s.as_str()).unwrap_or("");
let chars: Vec<char> = line.chars().collect();
let mut col = self.cursor_col.min(chars.len()).saturating_sub(1);
while col > 0 && chars[col].is_whitespace() {
col -= 1;
}
let in_word = col < chars.len() && VimEditor::is_word_char(chars[col], big_word);
while col > 0 && VimEditor::is_word_char(chars[col - 1], big_word) == in_word {
col -= 1;
}
self.cursor_col = col;
}
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub enum Motion {
Left,
Right,
Up,
Down,
WordForward,
WordEnd,
WordBack,
BigWordForward,
BigWordEnd,
BigWordBack,
LineStart,
LineEnd,
FirstNonBlank,
Line, ToTop, ToBottom, InnerWord, InnerQuote(char), InnerParen(char, char), }