Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
use crate::vim::{Direction, FindCharDestination, FindCharMotion};
|
||||
use warpui::text::TextBuffer;
|
||||
|
||||
/// Find the destination column for Vim's f/F/t/T motions on a single line.
|
||||
///
|
||||
/// This search has four variants based on the direction of search
|
||||
/// and where the cursor is expected to end up relative to the target character.
|
||||
/// When moving the cursor to the character before the target,
|
||||
/// ve must start searching further away from the cursor to
|
||||
/// ensure we don't re-match the previous target.
|
||||
///
|
||||
/// Given the string `abcdefgh` and a cursor on `d`,
|
||||
/// here's where each type of search starts looking for the target character
|
||||
/// when repeated:
|
||||
///
|
||||
/// 0 1 2 3 4 5 6 7
|
||||
/// a b c d e f g h
|
||||
/// __F_| ^ |__f___ (also t/T initially)
|
||||
/// T_| |__t_ (when repeated)
|
||||
///
|
||||
/// F: (Backward, at target) starts right at the cursor.
|
||||
/// T: (Backward, before target)` starts one before the cursor when repeated.
|
||||
/// f: (Forward, at target)` starts one after the cursor.
|
||||
/// t: (Forward, before target)` starts two after the cursor when repeated.
|
||||
///
|
||||
/// Returns Some(new_column) if the target is found on the line based on the parameters; otherwise None.
|
||||
pub fn vim_find_char_on_line(
|
||||
line: &str,
|
||||
current_column: usize,
|
||||
motion: &FindCharMotion,
|
||||
occurrence_count: u32,
|
||||
keep_selection: bool,
|
||||
) -> Option<usize> {
|
||||
let FindCharMotion {
|
||||
direction,
|
||||
destination,
|
||||
is_repetition,
|
||||
c,
|
||||
} = motion;
|
||||
|
||||
let search_start_column = match (direction, destination, is_repetition) {
|
||||
(Direction::Backward, FindCharDestination::AtChar, _)
|
||||
| (Direction::Backward, FindCharDestination::BeforeChar, false) => current_column,
|
||||
(Direction::Backward, FindCharDestination::BeforeChar, true) => {
|
||||
current_column.saturating_sub(1)
|
||||
}
|
||||
|
||||
// When moving forward, skip the current character under the cursor.
|
||||
(Direction::Forward, FindCharDestination::AtChar, _)
|
||||
| (Direction::Forward, FindCharDestination::BeforeChar, false) => current_column + 1,
|
||||
(Direction::Forward, FindCharDestination::BeforeChar, true) => current_column + 2,
|
||||
};
|
||||
|
||||
let found_char = match direction {
|
||||
Direction::Backward => {
|
||||
line.chars_rev_at(search_start_column.into())
|
||||
.ok()
|
||||
.and_then(|iter| {
|
||||
iter.enumerate()
|
||||
.filter(|(_, ch)| ch == c)
|
||||
.nth(occurrence_count.saturating_sub(1) as usize)
|
||||
})
|
||||
}
|
||||
Direction::Forward => line
|
||||
.chars_at(search_start_column.into())
|
||||
.ok()
|
||||
.and_then(|iter| {
|
||||
iter.enumerate()
|
||||
.filter(|(_, ch)| ch == c)
|
||||
.nth(occurrence_count.saturating_sub(1) as usize)
|
||||
}),
|
||||
};
|
||||
|
||||
if let Some((i, _)) = found_char {
|
||||
let move_distance = match (destination, is_repetition) {
|
||||
(FindCharDestination::AtChar, _) | (FindCharDestination::BeforeChar, true) => i + 1,
|
||||
(FindCharDestination::BeforeChar, false) => i,
|
||||
};
|
||||
|
||||
let mut new_column = match direction {
|
||||
Direction::Backward => current_column.saturating_sub(move_distance),
|
||||
Direction::Forward => current_column + move_distance,
|
||||
};
|
||||
|
||||
// When moving to the right and keeping selection, we include the matched character,
|
||||
// so we have to add 1 in this particular case.
|
||||
if keep_selection && direction == &Direction::Forward {
|
||||
new_column += 1;
|
||||
}
|
||||
|
||||
Some(new_column)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
mod matching_brackets;
|
||||
pub use matching_brackets::vim_find_matching_bracket;
|
||||
|
||||
mod paragraph_iterator;
|
||||
pub use paragraph_iterator::{find_next_paragraph_end, find_previous_paragraph_start};
|
||||
pub mod register;
|
||||
|
||||
mod text_objects;
|
||||
pub use text_objects::*;
|
||||
|
||||
mod word_iterator;
|
||||
pub use word_iterator::vim_word_iterator_from_offset;
|
||||
|
||||
mod find_char;
|
||||
pub use find_char::vim_find_char_on_line;
|
||||
|
||||
pub mod vim;
|
||||
@@ -0,0 +1,50 @@
|
||||
use itertools::Itertools;
|
||||
use string_offset::CharOffset;
|
||||
use warpui::text::TextBuffer;
|
||||
|
||||
use crate::vim::{BracketChar, BracketEnd};
|
||||
|
||||
/// This method looks for the bracket that complements/pairs with the passed in BracketChar,
|
||||
/// starting the search at the passed in offset.
|
||||
pub fn vim_find_matching_bracket<'a, T, C>(
|
||||
buffer: &'a T,
|
||||
bracket_char: &BracketChar,
|
||||
offset: C,
|
||||
) -> Option<CharOffset>
|
||||
where
|
||||
T: TextBuffer + ?Sized + 'a,
|
||||
C: Into<CharOffset>,
|
||||
{
|
||||
let offset = offset.into();
|
||||
|
||||
// If we're matching for an opening bracket, search forward, and vice versa.
|
||||
let mut iter: Box<dyn Iterator<Item = char>> = match bracket_char.end {
|
||||
BracketEnd::Opening => Box::new(buffer.chars_at(offset + 1).ok()?),
|
||||
BracketEnd::Closing => Box::new(buffer.chars_rev_at(offset).ok()?),
|
||||
};
|
||||
|
||||
// If we encounter more instances of the same bracket, i.e. we're matching "(" and we pass by
|
||||
// another "(", we must first match each additional "(" we encounter. Keep a count of those in
|
||||
// this "depth" variable.
|
||||
let mut depth: u32 = 0;
|
||||
let (i, _) = iter.find_position(|c| {
|
||||
if bracket_char.is_char(*c) {
|
||||
depth += 1;
|
||||
} else if bracket_char.complements(*c) {
|
||||
if depth == 0 {
|
||||
return true;
|
||||
} else {
|
||||
depth -= 1;
|
||||
}
|
||||
}
|
||||
false
|
||||
})?;
|
||||
match bracket_char.end {
|
||||
BracketEnd::Opening => Some(offset + i + 1),
|
||||
BracketEnd::Closing => Some(offset - i - 1),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "matching_brackets_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,63 @@
|
||||
use super::*;
|
||||
|
||||
impl BracketChar {
|
||||
fn from_char(c: char) -> Self {
|
||||
Self::try_from(c).unwrap_or_else(|_| panic!("invalid bracket char: {c}"))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vim_find_matching_bracket() {
|
||||
assert_eq!(
|
||||
vim_find_matching_bracket("", &BracketChar::from_char('('), 0),
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
vim_find_matching_bracket("foo(bar)baz", &BracketChar::from_char('('), 3),
|
||||
Some(7.into())
|
||||
);
|
||||
assert_eq!(
|
||||
vim_find_matching_bracket("foo(bar)baz", &BracketChar::from_char(')'), 7),
|
||||
Some(3.into())
|
||||
);
|
||||
assert_eq!(
|
||||
vim_find_matching_bracket("foo(bar)baz", &BracketChar::from_char('('), 8),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
vim_find_matching_bracket("foo[bar]baz", &BracketChar::from_char('('), 3),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
vim_find_matching_bracket("foo(bar(hello) world)baz", &BracketChar::from_char('('), 3),
|
||||
Some(20.into())
|
||||
);
|
||||
assert_eq!(
|
||||
vim_find_matching_bracket("foo(bar(hello) world)baz", &BracketChar::from_char(')'), 20),
|
||||
Some(3.into())
|
||||
);
|
||||
assert_eq!(
|
||||
vim_find_matching_bracket(
|
||||
"foo(bar(h[(])llo) world)baz",
|
||||
&BracketChar::from_char('('),
|
||||
3
|
||||
),
|
||||
Some(23.into())
|
||||
);
|
||||
assert_eq!(
|
||||
vim_find_matching_bracket(
|
||||
"function foo() {\necho hello world\necho hi\n}\nfoo",
|
||||
&BracketChar::from_char('{'),
|
||||
15
|
||||
),
|
||||
Some(42.into())
|
||||
);
|
||||
assert_eq!(
|
||||
vim_find_matching_bracket(
|
||||
"function foo() {\necho hello world\necho hi\n}\nfoo",
|
||||
&BracketChar::from_char('}'),
|
||||
42
|
||||
),
|
||||
Some(15.into())
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
use string_offset::CharOffset;
|
||||
use warpui::text::TextBuffer;
|
||||
|
||||
/// Returns the offset of the first newline above a paragraph start before the current position.
|
||||
pub fn find_previous_paragraph_start<'a, T, C>(buffer: &'a T, offset: C) -> Option<CharOffset>
|
||||
where
|
||||
T: TextBuffer + ?Sized + 'a,
|
||||
C: Into<CharOffset>,
|
||||
{
|
||||
let offset = offset.into();
|
||||
|
||||
// Skip newlines between the current position and prior paragraph end
|
||||
let iter = buffer
|
||||
.chars_rev_at(offset + 1) // chars_rev_at doesn't include the current offset
|
||||
.ok()?
|
||||
.enumerate()
|
||||
.skip_while(|(_, c)| *c == '\n');
|
||||
|
||||
// Scan from current position backward until we find two newlines in a row
|
||||
let mut prev_was_newline = false;
|
||||
for (curr, c) in iter {
|
||||
if c == '\n' {
|
||||
if prev_was_newline {
|
||||
return Some(offset + 1 - curr); // + 1 because the enumerate is shifted above
|
||||
}
|
||||
prev_was_newline = true;
|
||||
} else {
|
||||
prev_was_newline = false;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
/// Returns the offset of the first newline below a paragraph end after the current position.
|
||||
pub fn find_next_paragraph_end<'a, T, C>(buffer: &'a T, offset: C) -> Option<CharOffset>
|
||||
where
|
||||
T: TextBuffer + ?Sized + 'a,
|
||||
C: Into<CharOffset>,
|
||||
{
|
||||
let offset = offset.into();
|
||||
|
||||
// Skip newlines between the current position and next paragraph start
|
||||
let iter = buffer
|
||||
.chars_at(offset)
|
||||
.ok()?
|
||||
.enumerate()
|
||||
.skip_while(|(_, c)| *c == '\n');
|
||||
|
||||
// Scan from current position forward until we find two newlines in a row
|
||||
let mut prev_was_newline = false;
|
||||
for (curr, c) in iter {
|
||||
if c == '\n' {
|
||||
if prev_was_newline {
|
||||
return Some(offset + curr);
|
||||
}
|
||||
prev_was_newline = true;
|
||||
} else {
|
||||
prev_was_newline = false;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
#[cfg(test)]
|
||||
#[path = "paragraph_iterator_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,108 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_single_paragraph_returns_none_both_directions() {
|
||||
// Single paragraph: no double-newline separator anywhere
|
||||
let text = "p1 line1\np1 line2\np1 line3";
|
||||
let pos = text.find("line2").unwrap();
|
||||
|
||||
assert_eq!(find_previous_paragraph_start(text, pos), None);
|
||||
assert_eq!(find_next_paragraph_end(text, pos), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_paragraph_surrounded_returns_prev_and_next() {
|
||||
// Single paragraph: no double-newline separator anywhere
|
||||
let text = "\n\n\np1 line1\np1 line2\np1 line3\n\n\n";
|
||||
let pos = text.find("line2").unwrap();
|
||||
|
||||
// The first empty line after the end of the paragraph (the second newline character)
|
||||
let goal_end = text.find("line3").unwrap() + "line3".len() + 1;
|
||||
|
||||
assert_eq!(find_previous_paragraph_start(text, pos), Some(2.into()));
|
||||
assert_eq!(find_next_paragraph_end(text, pos), Some(goal_end.into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_next_none_in_last_paragraph_previous_goes_up() {
|
||||
// Two paragraphs separated by one blank line (double newline)
|
||||
let text = "p1\n\np2";
|
||||
let pos_in_p2 = text.find("p2").unwrap();
|
||||
|
||||
// previous should find the boundary before p2: index of the second newline in the pair
|
||||
// for both cursor positioning on the `p` and `2`
|
||||
assert_eq!(
|
||||
find_previous_paragraph_start(text, pos_in_p2),
|
||||
Some((pos_in_p2 - 1).into())
|
||||
);
|
||||
assert_eq!(
|
||||
find_previous_paragraph_start(text, pos_in_p2 + 1),
|
||||
Some((pos_in_p2 - 1).into())
|
||||
);
|
||||
// next should find nothing after the last paragraph
|
||||
assert_eq!(find_next_paragraph_end(text, pos_in_p2), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_previous_none_in_first_paragraph_next_goes_down() {
|
||||
let text = "p1\n\np2";
|
||||
let pos_in_p1 = text.find("p1").unwrap();
|
||||
|
||||
// previous should find nothing before the first paragraph
|
||||
assert_eq!(find_previous_paragraph_start(text, pos_in_p1), None);
|
||||
// next should find the boundary after p1: index of the second newline in the pair
|
||||
// for both cursor positioning on the `p` and `1`
|
||||
assert_eq!(
|
||||
find_next_paragraph_end(text, pos_in_p1),
|
||||
Some((pos_in_p1 + "p1".len() + 1).into())
|
||||
);
|
||||
assert_eq!(
|
||||
find_next_paragraph_end(text, pos_in_p1 + 1),
|
||||
Some((pos_in_p1 + "p1".len() + 1).into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_three_paragraphs_lots_of_newlines_middle_para() {
|
||||
// Three paragraphs, separated by four newlines each
|
||||
let text = "p1\n\n\n\n".to_string() + "p2\n\n\n\n" + "p3";
|
||||
let pos_in_p2 = text.find("p2").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
find_previous_paragraph_start(text.as_str(), pos_in_p2),
|
||||
Some((pos_in_p2 - 1).into())
|
||||
);
|
||||
assert_eq!(
|
||||
find_next_paragraph_end(text.as_str(), pos_in_p2),
|
||||
Some((pos_in_p2 + "p2".len() + 1).into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cursor_in_middle_of_newline_patch_quad_runs() {
|
||||
// Lots of newlines between paras, but cursor starts in the middle of the newlines
|
||||
let text = "p1\n\n\n\n\n\n".to_string() + "p2\n\n\n\n\n\n" + "p3";
|
||||
let pos_between_p1_p2 = "p1".len() + 3;
|
||||
let pos_p2 = text.find("p2").unwrap();
|
||||
let pos_between_p2_p3 = pos_p2 + "p2".len() + 3;
|
||||
|
||||
// Place cursor amongst newlines between p1 and p2
|
||||
assert_eq!(
|
||||
find_previous_paragraph_start(text.as_str(), pos_between_p1_p2),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
find_next_paragraph_end(text.as_str(), pos_between_p1_p2),
|
||||
Some((pos_p2 + "p2".len() + 1).into())
|
||||
);
|
||||
|
||||
// Place cursor amongst newlines between p2 and p3
|
||||
assert_eq!(
|
||||
find_previous_paragraph_start(text.as_str(), pos_between_p2_p3),
|
||||
Some((pos_p2 - 1).into())
|
||||
);
|
||||
assert_eq!(
|
||||
find_next_paragraph_end(text.as_str(), pos_between_p2_p3),
|
||||
None
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/// In Vim, see ":help quote_".
|
||||
/// The black hole register can be written to but it always reads as empty.
|
||||
pub const BLACK_HOLE_REGISTER: char = '_';
|
||||
|
||||
/// The registers we currently support.
|
||||
pub fn valid_register_name(c: char) -> bool {
|
||||
matches!(c, 'a'..='z' | 'A'..='Z' | '+' | '*' | '"')
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
//! This module is for "block objects" i.e. text objects delimited by brackets/parentheses.
|
||||
|
||||
use std::ops::Range;
|
||||
|
||||
use itertools::Itertools;
|
||||
use string_offset::CharOffset;
|
||||
use warpui::text::TextBuffer;
|
||||
|
||||
use crate::{
|
||||
vim::{BracketChar, BracketEnd, BracketType},
|
||||
vim_find_matching_bracket,
|
||||
};
|
||||
|
||||
/// Vim's block-based text objects, e.g. `di{`. This includes a string of text enclosed by any pair
|
||||
/// of [`BracketType`], not including the brackets themselves.
|
||||
/// See https://vimdoc.sourceforge.net/htmldoc/motion.html#ib
|
||||
/// or enter ":help i{" in Vim.
|
||||
///
|
||||
/// There may be whitespace in within the brackets which is also not included. If the closing
|
||||
/// bracket is on a different line then the opening bracket, and there is only whitespace between
|
||||
/// the closing bracket and the newline preceding it, all that space plus the newline are _not_
|
||||
/// included in the text object. (AFAIK this is not documented.) We can refer to that space as
|
||||
/// "trailing padding". Furthermore, the `c` and `d` commands differ in how they respect "leading
|
||||
/// padding", i.e. the whitespace plus newline after the opening bracket, in that `d` will delete
|
||||
/// the leading padding but `c` will not. The `preserve_leading_padding` parameter controls this.
|
||||
///
|
||||
/// Returns None when the buffer is empty, the offset is out of bounds, or there is no matching
|
||||
/// pair of brackets of the type we're looking for around the cursor.
|
||||
pub fn vim_inner_block<T, C>(
|
||||
buffer: &T,
|
||||
offset: C,
|
||||
bracket_type: BracketType,
|
||||
preserve_leading_padding: bool,
|
||||
) -> Option<Range<CharOffset>>
|
||||
where
|
||||
T: TextBuffer + ?Sized,
|
||||
C: Into<CharOffset>,
|
||||
{
|
||||
let block_range = vim_a_block(buffer, offset, bracket_type)?;
|
||||
let (mut block_start, mut block_end) = (block_range.start, block_range.end);
|
||||
|
||||
// Move bounds by 1 to remove the brackets, though we may need to trim any trailing or leading
|
||||
// padding.
|
||||
block_start += 1;
|
||||
block_end -= 1;
|
||||
|
||||
// First, check if we need to remove trailing padding from the range.
|
||||
if let Some((i, _)) = buffer
|
||||
.chars_rev_at(block_end)
|
||||
.ok()?
|
||||
.take_while(|c| c.is_whitespace())
|
||||
.find_position(|c| *c == '\n')
|
||||
{
|
||||
block_end -= i + 1;
|
||||
}
|
||||
|
||||
// Finally, if we're doing a command that preserves leading padding, e.g. `c`, check if we need
|
||||
// to remove that from the range.
|
||||
if preserve_leading_padding {
|
||||
if let Some((i, _)) = buffer
|
||||
.chars_at(block_start)
|
||||
.ok()?
|
||||
.take_while(|c| c.is_whitespace())
|
||||
.find_position(|c| *c == '\n')
|
||||
{
|
||||
block_start += i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
Some(block_start..block_end)
|
||||
}
|
||||
|
||||
/// Vim's block-based text objects, e.g. `da{`. This includes a string of text enclosed by any pair
|
||||
/// of [`BracketType`], including the brackets themselves.
|
||||
/// See https://vimdoc.sourceforge.net/htmldoc/motion.html#ab
|
||||
/// or enter ":help a{" in Vim.
|
||||
///
|
||||
/// Returns None when the buffer is empty, the offset is out of bounds, or there is no matching
|
||||
/// pair of brackets of the type we're looking for around the cursor.
|
||||
pub fn vim_a_block<T, C>(
|
||||
buffer: &T,
|
||||
offset: C,
|
||||
bracket_type: BracketType,
|
||||
) -> Option<Range<CharOffset>>
|
||||
where
|
||||
T: TextBuffer + ?Sized,
|
||||
C: Into<CharOffset>,
|
||||
{
|
||||
let offset = offset.into();
|
||||
// If the buffer is empty, return early.
|
||||
let c = buffer.chars_at(offset).ok()?.next()?;
|
||||
|
||||
// Check if the cursor is already on a bracket of the type we're looking for.
|
||||
match BracketChar::try_from(c) {
|
||||
// If it is, just call [`vim_find_matching_bracket`] for the other bracket.
|
||||
Ok(bracket) if bracket.kind == bracket_type => {
|
||||
let other_offset = vim_find_matching_bracket(buffer, &bracket, offset)?;
|
||||
// We may have just moved backwards or forwards. Make sure the smaller offset comes
|
||||
// first in the range bounds.
|
||||
let (start_offset, end_offset) = if other_offset > offset {
|
||||
(offset, other_offset)
|
||||
} else {
|
||||
(other_offset, offset)
|
||||
};
|
||||
Some(start_offset..end_offset + 1)
|
||||
}
|
||||
// If not, perform the search in both directions.
|
||||
_ => {
|
||||
let end_offset = vim_find_matching_bracket(
|
||||
buffer,
|
||||
&BracketChar {
|
||||
end: BracketEnd::Opening,
|
||||
kind: bracket_type,
|
||||
},
|
||||
offset,
|
||||
)?;
|
||||
let start_offset = vim_find_matching_bracket(
|
||||
buffer,
|
||||
&BracketChar {
|
||||
end: BracketEnd::Closing,
|
||||
kind: bracket_type,
|
||||
},
|
||||
end_offset,
|
||||
)?;
|
||||
Some(start_offset..end_offset + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "block_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,147 @@
|
||||
use super::*;
|
||||
use unindent::Unindent;
|
||||
|
||||
const BLOCK: &str = "match BracketChar::try_from(c) {
|
||||
Ok(bracket) => todo!(),
|
||||
Err(_) => {
|
||||
let end_offset = vim_find_matching_bracket(
|
||||
buffer,
|
||||
BracketChar {
|
||||
end: BracketEnd::Opening,
|
||||
kind: bracket_kind,
|
||||
},
|
||||
offset,
|
||||
)?;
|
||||
let start_offset = vim_find_matching_bracket(
|
||||
buffer,
|
||||
BracketChar {
|
||||
end: BracketEnd::Closing,
|
||||
kind: bracket_kind,
|
||||
},
|
||||
end_offset,
|
||||
)?;
|
||||
Some(start_offset..end_offset)
|
||||
}
|
||||
}";
|
||||
|
||||
#[test]
|
||||
fn test_vim_a_block() {
|
||||
let block = BLOCK.unindent();
|
||||
for i in 27..=29 {
|
||||
assert_eq!(
|
||||
vim_a_block(block.as_str(), i, BracketType::Parenthesis),
|
||||
Some(27.into()..30.into())
|
||||
);
|
||||
}
|
||||
for i in (31..=74).chain(573..=574) {
|
||||
assert_eq!(
|
||||
vim_a_block(block.as_str(), i, BracketType::CurlyBrace),
|
||||
Some(31.into()..575.into())
|
||||
);
|
||||
}
|
||||
for i in (75..=172).chain(266..=397).chain(491..=572) {
|
||||
assert_eq!(
|
||||
vim_a_block(block.as_str(), i, BracketType::CurlyBrace),
|
||||
Some(75.into()..573.into())
|
||||
);
|
||||
}
|
||||
for i in 173..=265 {
|
||||
assert_eq!(
|
||||
vim_a_block(block.as_str(), i, BracketType::CurlyBrace),
|
||||
Some(173.into()..266.into())
|
||||
);
|
||||
}
|
||||
for i in 398..=490 {
|
||||
assert_eq!(
|
||||
vim_a_block(block.as_str(), i, BracketType::CurlyBrace),
|
||||
Some(398.into()..491.into())
|
||||
);
|
||||
}
|
||||
for i in 39..=47 {
|
||||
assert_eq!(
|
||||
vim_a_block(block.as_str(), i, BracketType::Parenthesis),
|
||||
Some(39.into()..48.into())
|
||||
);
|
||||
}
|
||||
for i in 57..=58 {
|
||||
assert_eq!(
|
||||
vim_a_block(block.as_str(), i, BracketType::Parenthesis),
|
||||
Some(57.into()..59.into())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vim_inner_block() {
|
||||
let block = BLOCK.unindent();
|
||||
for i in 27..=29 {
|
||||
assert_eq!(
|
||||
vim_inner_block(block.as_str(), i, BracketType::Parenthesis, false),
|
||||
Some(28.into()..29.into())
|
||||
);
|
||||
assert_eq!(
|
||||
vim_inner_block(block.as_str(), i, BracketType::Parenthesis, true),
|
||||
Some(28.into()..29.into())
|
||||
);
|
||||
}
|
||||
for i in (31..=74).chain(573..=574) {
|
||||
assert_eq!(
|
||||
vim_inner_block(block.as_str(), i, BracketType::CurlyBrace, false),
|
||||
Some(32.into()..573.into())
|
||||
);
|
||||
assert_eq!(
|
||||
vim_inner_block(block.as_str(), i, BracketType::CurlyBrace, true),
|
||||
Some(33.into()..573.into())
|
||||
);
|
||||
}
|
||||
for i in (75..=172).chain(266..=397).chain(491..=572) {
|
||||
assert_eq!(
|
||||
vim_inner_block(block.as_str(), i, BracketType::CurlyBrace, false),
|
||||
Some(76.into()..567.into())
|
||||
);
|
||||
assert_eq!(
|
||||
vim_inner_block(block.as_str(), i, BracketType::CurlyBrace, true),
|
||||
Some(77.into()..567.into())
|
||||
);
|
||||
}
|
||||
for i in 173..=265 {
|
||||
assert_eq!(
|
||||
vim_inner_block(block.as_str(), i, BracketType::CurlyBrace, false),
|
||||
Some(174.into()..252.into())
|
||||
);
|
||||
assert_eq!(
|
||||
vim_inner_block(block.as_str(), i, BracketType::CurlyBrace, true),
|
||||
Some(175.into()..252.into())
|
||||
);
|
||||
}
|
||||
for i in 398..=490 {
|
||||
assert_eq!(
|
||||
vim_inner_block(block.as_str(), i, BracketType::CurlyBrace, false),
|
||||
Some(399.into()..477.into())
|
||||
);
|
||||
assert_eq!(
|
||||
vim_inner_block(block.as_str(), i, BracketType::CurlyBrace, true),
|
||||
Some(400.into()..477.into())
|
||||
);
|
||||
}
|
||||
for i in 39..=47 {
|
||||
assert_eq!(
|
||||
vim_inner_block(block.as_str(), i, BracketType::Parenthesis, false),
|
||||
Some(40.into()..47.into())
|
||||
);
|
||||
assert_eq!(
|
||||
vim_inner_block(block.as_str(), i, BracketType::Parenthesis, true),
|
||||
Some(40.into()..47.into())
|
||||
);
|
||||
}
|
||||
for i in 57..=58 {
|
||||
assert_eq!(
|
||||
vim_inner_block(block.as_str(), i, BracketType::Parenthesis, false),
|
||||
Some(58.into()..58.into())
|
||||
);
|
||||
assert_eq!(
|
||||
vim_inner_block(block.as_str(), i, BracketType::Parenthesis, true),
|
||||
Some(58.into()..58.into())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
//! This module is for text-objects, e.g. `diw` and the like.
|
||||
//!
|
||||
//! See https://vimdoc.sourceforge.net/htmldoc/motion.html#text-objects
|
||||
//! or enter ":help text-objects" in Vim.
|
||||
mod block;
|
||||
mod paragraph;
|
||||
mod quote;
|
||||
mod word;
|
||||
|
||||
pub use self::block::*;
|
||||
pub use paragraph::*;
|
||||
pub use quote::*;
|
||||
pub use word::*;
|
||||
@@ -0,0 +1,102 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use string_offset::CharOffset;
|
||||
use warpui::text::TextBuffer;
|
||||
|
||||
use crate::{find_next_paragraph_end, find_previous_paragraph_start};
|
||||
|
||||
/// Vim's "inner paragraph" text object, e.g. `dip`. This includes lines surrounding the cursor
|
||||
/// until blank lines are encountered in either direction, not including either blank line.
|
||||
/// See https://vimdoc.sourceforge.net/htmldoc/motion.html#ip
|
||||
/// or enter ":help ip" in Vim.
|
||||
///
|
||||
/// Returns None when the buffer is empty or the offset is out of bounds.
|
||||
pub fn vim_inner_paragraph<T, C>(buffer: &T, offset: C) -> Option<Range<CharOffset>>
|
||||
where
|
||||
T: TextBuffer + ?Sized,
|
||||
C: Into<CharOffset>,
|
||||
{
|
||||
let offset = offset.into();
|
||||
let mut chars = buffer.chars_at(offset).ok()?.peekable();
|
||||
if chars.peek().is_some_and(|c| *c == '\n') {
|
||||
let paragraph_end = offset - 1 + chars.take_while(|c| *c == '\n').count();
|
||||
let paragraph_start = offset + 1
|
||||
- buffer
|
||||
.chars_rev_at(offset)
|
||||
.ok()?
|
||||
.take_while(|c| *c == '\n')
|
||||
.count();
|
||||
return Some(paragraph_start..paragraph_end);
|
||||
}
|
||||
let paragraph_start = find_previous_paragraph_start(buffer, offset)
|
||||
.map(|i| i + 1)
|
||||
.unwrap_or_default();
|
||||
let paragraph_end = find_next_paragraph_end(buffer, offset)
|
||||
.map(|i| i - 1)
|
||||
.unwrap_or_else(|| offset + chars.count());
|
||||
|
||||
Some(paragraph_start..paragraph_end)
|
||||
}
|
||||
|
||||
/// Vim's "a paragraph" text object, e.g. `dap`. This includes lines surrounding the cursor until
|
||||
/// blank lines are encountered in either direction, including one blank line. Usually the blank
|
||||
/// line to be included is the one following, unless the paragraph is at the end of the buffer in
|
||||
/// which case we include the one preceding.
|
||||
/// See https://vimdoc.sourceforge.net/htmldoc/motion.html#ap
|
||||
/// or enter ":help ap" in Vim.
|
||||
///
|
||||
/// Returns None when the buffer is empty or the offset is out of bounds.
|
||||
pub fn vim_a_paragraph<T, C>(buffer: &T, offset: C) -> Option<Range<CharOffset>>
|
||||
where
|
||||
T: TextBuffer + ?Sized,
|
||||
C: Into<CharOffset>,
|
||||
{
|
||||
let offset = offset.into();
|
||||
let mut chars = buffer.chars_at(offset).ok()?.peekable();
|
||||
if chars.peek().is_some_and(|c| *c == '\n') {
|
||||
let paragraph_end = find_next_paragraph_end(buffer, offset)
|
||||
.map(|i| i - 1)
|
||||
.unwrap_or_else(|| offset + chars.count());
|
||||
let paragraph_start = offset + 1
|
||||
- buffer
|
||||
.chars_rev_at(offset)
|
||||
.ok()?
|
||||
.take_while(|c| *c == '\n')
|
||||
.count();
|
||||
return Some(paragraph_start..paragraph_end);
|
||||
}
|
||||
let end_offset = find_next_paragraph_end(buffer, offset);
|
||||
// We either need to include all the consecutive blank lines above or below the paragraph
|
||||
// content.
|
||||
let paragraph_start = find_previous_paragraph_start(buffer, offset)
|
||||
.map(|i| {
|
||||
// If end_offset is Some, we'll include the consecutive blank lines below.
|
||||
if end_offset.is_some() {
|
||||
i + 1
|
||||
} else {
|
||||
// Calculate the number of blank lines above the start of the paragraph content.
|
||||
i + 1
|
||||
- buffer
|
||||
.chars_rev_at(i)
|
||||
.map(|chars| chars.take_while(|c| *c == '\n').count())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let paragraph_end = end_offset
|
||||
.map(|i| {
|
||||
// Calculate the number of blank lines below the end of the paragraph content.
|
||||
i - 1
|
||||
+ buffer
|
||||
.chars_at(i)
|
||||
.map(|chars| chars.take_while(|c| *c == '\n').count())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.unwrap_or_else(|| offset + chars.count());
|
||||
|
||||
Some(paragraph_start..paragraph_end)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "paragraph_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,208 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn vim_inner_paragraph_empty_buffer() {
|
||||
assert_eq!(vim_inner_paragraph("", 0), Some(0.into()..0.into()));
|
||||
assert_eq!(vim_inner_paragraph("", 1), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vim_a_paragraph_empty_buffer() {
|
||||
assert_eq!(vim_a_paragraph("", 0), Some(0.into()..0.into()));
|
||||
assert_eq!(vim_a_paragraph("", 1), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vim_inner_paragraph_single_paragraph() {
|
||||
let text = "foo bar\nnext line\n";
|
||||
|
||||
for (i, ch) in text.chars().enumerate() {
|
||||
if ch != '\n' {
|
||||
let range = vim_inner_paragraph(text, i).unwrap();
|
||||
assert_eq!(range, 0.into()..text.len().into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vim_a_paragraph_single_paragraph() {
|
||||
let text = "foo bar\nnext line\n";
|
||||
|
||||
for (i, ch) in text.chars().enumerate() {
|
||||
if ch != '\n' {
|
||||
let range = vim_a_paragraph(text, i).unwrap();
|
||||
assert_eq!(range, 0.into()..text.len().into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vim_inner_paragraph_two_paragraphs() {
|
||||
let text = "first line\nof first para\n\nsecond para line\n";
|
||||
let blank_index = text.find("\n\n").unwrap();
|
||||
let second_start = blank_index + 2;
|
||||
|
||||
for (i, ch) in text.chars().take(blank_index).enumerate() {
|
||||
if ch != '\n' {
|
||||
let range = vim_inner_paragraph(text, i).unwrap();
|
||||
assert_eq!(range, 0.into()..blank_index.into());
|
||||
}
|
||||
}
|
||||
|
||||
for (i, ch) in text.chars().enumerate().skip(second_start) {
|
||||
if ch != '\n' {
|
||||
let range = vim_inner_paragraph(text, i).unwrap();
|
||||
assert_eq!(range, second_start.into()..text.len().into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vim_inner_paragraph_three_paragraphs() {
|
||||
let text = "first\n\nsecond\n\nthird\n";
|
||||
let (first_blank, second_blank) = {
|
||||
let mut it = text.match_indices("\n\n");
|
||||
(it.next().unwrap().0, it.next().unwrap().0)
|
||||
};
|
||||
let second_start = first_blank + 2;
|
||||
let third_start = second_blank + 2;
|
||||
|
||||
for (i, ch) in text.chars().take(first_blank).enumerate() {
|
||||
if ch != '\n' {
|
||||
let range = vim_inner_paragraph(text, i).unwrap();
|
||||
assert_eq!(range, 0.into()..first_blank.into());
|
||||
}
|
||||
}
|
||||
|
||||
for (i, ch) in text
|
||||
.chars()
|
||||
.enumerate()
|
||||
.skip(second_start)
|
||||
.take(second_blank - second_start)
|
||||
{
|
||||
if ch != '\n' {
|
||||
let range = vim_inner_paragraph(text, i).unwrap();
|
||||
assert_eq!(range, second_start.into()..second_blank.into());
|
||||
}
|
||||
}
|
||||
|
||||
for (i, ch) in text.chars().enumerate().skip(third_start) {
|
||||
if ch != '\n' {
|
||||
let range = vim_inner_paragraph(text, i).unwrap();
|
||||
assert_eq!(range, third_start.into()..text.len().into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vim_a_paragraph_two_paragraphs() {
|
||||
let text = "first line\nof first para\n\nsecond para line\n";
|
||||
let blank_index = text.find("\n\n").unwrap();
|
||||
|
||||
for (i, ch) in text.chars().take(blank_index).enumerate() {
|
||||
if ch != '\n' {
|
||||
let range = vim_a_paragraph(text, i).unwrap();
|
||||
assert_eq!(range, 0.into()..(blank_index + 1).into());
|
||||
}
|
||||
}
|
||||
|
||||
let second_range_start = (blank_index + 1).into();
|
||||
let second_range_end = text.len().into();
|
||||
let second_start = blank_index + 2;
|
||||
|
||||
for (i, ch) in text.chars().enumerate().skip(second_start) {
|
||||
if ch != '\n' {
|
||||
let range = vim_a_paragraph(text, i).unwrap();
|
||||
assert_eq!(range, second_range_start..second_range_end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vim_a_paragraph_three_paragraphs() {
|
||||
let text = "first\n\nsecond\n\nthird\n";
|
||||
let (first_blank, second_blank) = {
|
||||
let mut it = text.match_indices("\n\n");
|
||||
(it.next().unwrap().0, it.next().unwrap().0)
|
||||
};
|
||||
let second_start = first_blank + 2;
|
||||
let third_start = second_blank + 2;
|
||||
|
||||
for (i, ch) in text.chars().take(first_blank).enumerate() {
|
||||
if ch != '\n' {
|
||||
let range = vim_a_paragraph(text, i).unwrap();
|
||||
assert_eq!(range, 0.into()..(first_blank + 1).into());
|
||||
}
|
||||
}
|
||||
|
||||
for (i, ch) in text
|
||||
.chars()
|
||||
.enumerate()
|
||||
.skip(second_start)
|
||||
.take(second_blank - second_start)
|
||||
{
|
||||
if ch != '\n' {
|
||||
let range = vim_a_paragraph(text, i).unwrap();
|
||||
assert_eq!(range, second_start.into()..(second_blank + 1).into());
|
||||
}
|
||||
}
|
||||
|
||||
let last_range = (second_blank + 1).into()..text.len().into();
|
||||
for (i, ch) in text.chars().enumerate().skip(third_start) {
|
||||
if ch != '\n' {
|
||||
let range = vim_a_paragraph(text, i).unwrap();
|
||||
assert_eq!(range, last_range);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vim_inner_paragraph_blank_lines() {
|
||||
let text = "first\n\n\nsecond\n";
|
||||
|
||||
for offset in 5..=7 {
|
||||
let range = vim_inner_paragraph(text, offset).unwrap();
|
||||
assert_eq!(range, 6.into()..7.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vim_a_paragraph_blank_lines() {
|
||||
let text = "first\n\n\nsecond\n";
|
||||
|
||||
for offset in 5..=7 {
|
||||
let range = vim_a_paragraph(text, offset).unwrap();
|
||||
assert_eq!(range, 6.into()..text.len().into());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vim_a_paragraph_many_trailing_blank_lines() {
|
||||
let text = "first\n\n\nsecond\n\n\n\n\nthird";
|
||||
|
||||
for offset in 0..=4 {
|
||||
let range = vim_a_paragraph(text, offset).unwrap();
|
||||
assert_eq!(range, 0.into()..7.into());
|
||||
}
|
||||
|
||||
for offset in 8..=13 {
|
||||
let range = vim_a_paragraph(text, offset).unwrap();
|
||||
assert_eq!(range, 8.into()..18.into());
|
||||
}
|
||||
|
||||
for offset in 19..=23 {
|
||||
let range = vim_a_paragraph(text, offset).unwrap();
|
||||
assert_eq!(range, 15.into()..24.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vim_paragraph_lines_with_spaces_included() {
|
||||
// Despite being invisible, a line containing spaces still counts as "content".
|
||||
let text = "first\n\n \nsecond";
|
||||
|
||||
let range = vim_a_paragraph(text, 3).unwrap();
|
||||
assert_eq!(range, 0.into()..6.into());
|
||||
let range = vim_a_paragraph(text, 14).unwrap();
|
||||
assert_eq!(range, 6.into()..18.into());
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use itertools::Itertools;
|
||||
use string_offset::CharOffset;
|
||||
use warpui::text::TextBuffer;
|
||||
|
||||
use crate::vim::QuoteType;
|
||||
|
||||
/// Vim's "inner quote" text object, e.g. `di"`. This includes characters enclosed by quotes.
|
||||
/// See https://vimdoc.sourceforge.net/htmldoc/motion.html#iquote
|
||||
/// or enter ":help iquote" in Vim.
|
||||
///
|
||||
/// Returns None when the line is empty, doesn't contain a pair of the quotes we're looking for, or
|
||||
/// the offset is out of bounds.
|
||||
pub fn vim_inner_quote<T, C>(
|
||||
buffer: &T,
|
||||
offset: C,
|
||||
quote_type: QuoteType,
|
||||
) -> Option<Range<CharOffset>>
|
||||
where
|
||||
T: TextBuffer + ?Sized,
|
||||
C: Into<CharOffset>,
|
||||
{
|
||||
vim_a_quote(buffer, offset, quote_type).map(|range| (range.start + 1)..(range.end - 1))
|
||||
}
|
||||
|
||||
/// Vim's "a quote" text object, e.g. `da"`. This includes characters enclosed by quotes along with
|
||||
/// the quotes themselves.
|
||||
/// See https://vimdoc.sourceforge.net/htmldoc/motion.html#aquote
|
||||
/// or enter ":help aquote" in Vim.
|
||||
///
|
||||
/// Returns None when the line is empty, doesn't contain a pair of the quotes we're looking for, or
|
||||
/// the offset is out of bounds.
|
||||
pub fn vim_a_quote<T, C>(buffer: &T, offset: C, quote_type: QuoteType) -> Option<Range<CharOffset>>
|
||||
where
|
||||
T: TextBuffer + ?Sized,
|
||||
C: Into<CharOffset>,
|
||||
{
|
||||
let offset = offset.into();
|
||||
let mut forward_iter = buffer
|
||||
.chars_at(offset)
|
||||
.ok()?
|
||||
.take_while(|c| *c != '\n') // cannot traverse newline boundaries
|
||||
.enumerate()
|
||||
.peekable();
|
||||
let backward_iter = buffer
|
||||
.chars_rev_at(offset)
|
||||
.ok()?
|
||||
.take_while(|c| *c != '\n')
|
||||
.enumerate()
|
||||
.peekable();
|
||||
|
||||
// This will be None if the line is empty
|
||||
let (_, c) = forward_iter.next()?;
|
||||
|
||||
// First, check if the cursor is currently on top of a quote.
|
||||
if !quote_type.is_char(c) {
|
||||
// If not, see if there are quotes surrounding the cursor, one before one after.
|
||||
let mut quote_behind_found_at = None;
|
||||
for (i, c) in backward_iter {
|
||||
if quote_type.is_char(c) {
|
||||
quote_behind_found_at = Some(offset - i - 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let mut quote_ahead_fount_at = None;
|
||||
for (i, c) in &mut forward_iter {
|
||||
if quote_type.is_char(c) {
|
||||
quote_ahead_fount_at = Some(offset + i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
match (quote_behind_found_at, quote_ahead_fount_at) {
|
||||
// If there are quotes surrounding the cursor, take that range.
|
||||
(Some(start), Some(end)) => Some(start..end + 1),
|
||||
// If there is no quote before, but there _is_ one after, treat the one after as the
|
||||
// opening quote and look for a closing quote after that.
|
||||
(None, Some(start)) => {
|
||||
for (i, c) in forward_iter {
|
||||
if quote_type.is_char(c) {
|
||||
let end = offset + i + 1;
|
||||
return Some(start..end);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
// Otherwise, no valid range. Vim doesn't attempt to look for a pair of quotes behind.
|
||||
(_, None) => None,
|
||||
}
|
||||
} else {
|
||||
// If the cursor is on a quote, we need to count all quotes on this line to figure out if
|
||||
// this should be treated as an opening or closing quote.
|
||||
let quotes_behind = backward_iter
|
||||
.filter(|(_, c)| quote_type.is_char(*c))
|
||||
.collect_vec();
|
||||
// If there is an odd number of quotes before this one, then treat this one as closing.
|
||||
if quotes_behind.len() % 2 == 1 {
|
||||
let (i, _) = quotes_behind[0];
|
||||
let start = offset - i - 1;
|
||||
let end = offset + 1;
|
||||
Some(start..end)
|
||||
} else {
|
||||
// If there is an even number of quotes before this one, treat this one as opening.
|
||||
for (i, c) in forward_iter {
|
||||
if quote_type.is_char(c) {
|
||||
let end = offset + i + 1;
|
||||
return Some(offset..end);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "quote_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,114 @@
|
||||
use super::*;
|
||||
use crate::vim::QuoteType;
|
||||
|
||||
#[test]
|
||||
fn test_a_quote() {
|
||||
assert_eq!(vim_a_quote("", 0, QuoteType::Single), None);
|
||||
assert_eq!(
|
||||
vim_a_quote("'foo'", 1, QuoteType::Single).unwrap(),
|
||||
0.into()..5.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_a_quote("'foo'", 0, QuoteType::Single).unwrap(),
|
||||
0.into()..5.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_a_quote("'foo'", 4, QuoteType::Single).unwrap(),
|
||||
0.into()..5.into()
|
||||
);
|
||||
assert_eq!(vim_a_quote("'foo'", 1, QuoteType::Double), None);
|
||||
assert_eq!(vim_a_quote("'foo' ", 5, QuoteType::Single), None);
|
||||
assert_eq!(
|
||||
vim_a_quote("foo 'foo' ", 0, QuoteType::Single).unwrap(),
|
||||
5.into()..10.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_a_quote(r#"foo "" "#, 0, QuoteType::Double).unwrap(),
|
||||
5.into()..7.into()
|
||||
);
|
||||
|
||||
// 0000000000111111111
|
||||
// 0123456789012345678
|
||||
let line = " 'foo' 'bar' 'baz";
|
||||
for i in 0..=6 {
|
||||
assert_eq!(
|
||||
vim_a_quote(line, i, QuoteType::Single).unwrap(),
|
||||
2.into()..7.into()
|
||||
)
|
||||
}
|
||||
for i in 7..=8 {
|
||||
assert_eq!(
|
||||
vim_a_quote(line, i, QuoteType::Single).unwrap(),
|
||||
6.into()..10.into()
|
||||
)
|
||||
}
|
||||
for i in 9..=13 {
|
||||
assert_eq!(
|
||||
vim_a_quote(line, i, QuoteType::Single).unwrap(),
|
||||
9.into()..14.into()
|
||||
)
|
||||
}
|
||||
assert_eq!(
|
||||
vim_a_quote(line, 14, QuoteType::Single).unwrap(),
|
||||
13.into()..16.into()
|
||||
);
|
||||
for i in 15..=18 {
|
||||
assert_eq!(vim_a_quote(line, i, QuoteType::Single), None)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inner_quote() {
|
||||
assert_eq!(vim_inner_quote("", 0, QuoteType::Single), None);
|
||||
assert_eq!(
|
||||
vim_inner_quote("`foo`", 1, QuoteType::Backtick).unwrap(),
|
||||
1.into()..4.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_inner_quote("`foo`", 0, QuoteType::Backtick).unwrap(),
|
||||
1.into()..4.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_inner_quote("'foo'", 4, QuoteType::Single).unwrap(),
|
||||
1.into()..4.into()
|
||||
);
|
||||
assert_eq!(vim_inner_quote("'foo'", 1, QuoteType::Double), None);
|
||||
assert_eq!(vim_inner_quote("'foo' ", 5, QuoteType::Single), None);
|
||||
assert_eq!(
|
||||
vim_inner_quote("foo 'foo' ", 0, QuoteType::Single).unwrap(),
|
||||
6.into()..9.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_inner_quote(r#"foo "" "#, 0, QuoteType::Double).unwrap(),
|
||||
6.into()..6.into()
|
||||
);
|
||||
|
||||
// 0000000000111111111
|
||||
// 0123456789012345678
|
||||
let line = " 'foo' 'bar' 'baz";
|
||||
for i in 0..=6 {
|
||||
assert_eq!(
|
||||
vim_inner_quote(line, i, QuoteType::Single).unwrap(),
|
||||
3.into()..6.into()
|
||||
)
|
||||
}
|
||||
for i in 7..=8 {
|
||||
assert_eq!(
|
||||
vim_inner_quote(line, i, QuoteType::Single).unwrap(),
|
||||
7.into()..9.into()
|
||||
)
|
||||
}
|
||||
for i in 9..=13 {
|
||||
assert_eq!(
|
||||
vim_inner_quote(line, i, QuoteType::Single).unwrap(),
|
||||
10.into()..13.into()
|
||||
)
|
||||
}
|
||||
assert_eq!(
|
||||
vim_inner_quote(line, 14, QuoteType::Single).unwrap(),
|
||||
14.into()..15.into()
|
||||
);
|
||||
for i in 15..=18 {
|
||||
assert_eq!(vim_inner_quote(line, i, QuoteType::Single), None)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use string_offset::CharOffset;
|
||||
use warpui::text::TextBuffer;
|
||||
|
||||
use crate::{vim::WordType, word_iterator::CharacterKind};
|
||||
|
||||
/// Vim's "inner word" text object, e.g. `diw`. This includes the series of either word chars,
|
||||
/// symbols, or whitespace around the cursor.
|
||||
/// See https://vimdoc.sourceforge.net/htmldoc/motion.html#iw
|
||||
/// or enter ":help iw" in Vim.
|
||||
///
|
||||
/// Returns None when the buffer is empty or the offset is out of bounds.
|
||||
pub fn vim_inner_word<T, C>(buffer: &T, offset: C, word_type: WordType) -> Option<Range<CharOffset>>
|
||||
where
|
||||
T: TextBuffer + ?Sized,
|
||||
C: Into<CharOffset>,
|
||||
{
|
||||
let offset = offset.into();
|
||||
let mut forward_iter = buffer.chars_at(offset).ok()?.peekable();
|
||||
// Empty buffer will be None here.
|
||||
let cursor_context = CharacterKind::from(*forward_iter.peek()?);
|
||||
|
||||
let mut word_start = offset;
|
||||
let backward_iter = buffer.chars_rev_at(offset).ok()?;
|
||||
for c in backward_iter {
|
||||
if !CharacterKind::from(c).equivalent_char_kind(&cursor_context, word_type) {
|
||||
break;
|
||||
}
|
||||
word_start -= 1;
|
||||
}
|
||||
|
||||
let mut word_end = offset;
|
||||
for c in forward_iter {
|
||||
if !CharacterKind::from(c).equivalent_char_kind(&cursor_context, word_type) {
|
||||
break;
|
||||
}
|
||||
word_end += 1;
|
||||
}
|
||||
|
||||
Some(word_start..word_end)
|
||||
}
|
||||
|
||||
/// Vim's "a word" text object, e.g. `daw`. This includes the series of either word chars or
|
||||
/// symbols around the cursor as well as the section of whitespace after it (or if there is no
|
||||
/// whitespace after it, then before it).
|
||||
/// See https://vimdoc.sourceforge.net/htmldoc/motion.html#aw
|
||||
/// or enter ":help aw" in Vim.
|
||||
///
|
||||
/// Returns None when the buffer is empty or the offset is out of bounds.
|
||||
pub fn vim_a_word<T, C>(buffer: &T, offset: C, word_type: WordType) -> Option<Range<CharOffset>>
|
||||
where
|
||||
T: TextBuffer + ?Sized,
|
||||
C: Into<CharOffset>,
|
||||
{
|
||||
let offset = offset.into();
|
||||
let mut forward_iter = buffer.chars_at(offset).ok()?.peekable();
|
||||
// Empty buffer will be None here.
|
||||
let cursor_context = CharacterKind::from(*forward_iter.peek()?);
|
||||
|
||||
let mut word_start = offset;
|
||||
let mut word_end = offset;
|
||||
let mut backward_iter = buffer.chars_rev_at(offset).ok()?.peekable();
|
||||
|
||||
// Go back to the beginning of this context.
|
||||
while let Some(&c) = backward_iter.peek() {
|
||||
if !CharacterKind::from(c).equivalent_char_kind(&cursor_context, word_type) {
|
||||
break;
|
||||
}
|
||||
word_start -= 1;
|
||||
backward_iter.next();
|
||||
}
|
||||
|
||||
// Proceed to the end of this context.
|
||||
while let Some(&c) = forward_iter.peek() {
|
||||
if !CharacterKind::from(c).equivalent_char_kind(&cursor_context, word_type) {
|
||||
break;
|
||||
}
|
||||
word_end += 1;
|
||||
forward_iter.next();
|
||||
}
|
||||
|
||||
// If the cursor is in whitespace it needs to get the non-whitespace word ahead.
|
||||
if cursor_context == CharacterKind::Whitespace {
|
||||
// Check if there as non-whitespace after this.
|
||||
let Some(&c) = forward_iter.peek() else {
|
||||
// If this is the end of the buffer, we're done.
|
||||
return Some(word_start..word_end);
|
||||
};
|
||||
let next_cursor_context = CharacterKind::from(c);
|
||||
|
||||
// Proceed to the end of this non-whitespace word.
|
||||
for c in forward_iter {
|
||||
if !CharacterKind::from(c).equivalent_char_kind(&next_cursor_context, word_type) {
|
||||
break;
|
||||
}
|
||||
word_end += 1;
|
||||
}
|
||||
return Some(word_start..word_end);
|
||||
}
|
||||
|
||||
// If we've made it this far, then the cursor did not start off in whitespace. This means we
|
||||
// need to include some whitespace around this word. If there is whitespace ahead, include
|
||||
// that. Otherwise, if there is whitespace behind, include that.
|
||||
|
||||
// If there is no content in this buffer ahead of this context.
|
||||
let Some(&c) = forward_iter.peek() else {
|
||||
// Check if there is any content behind.
|
||||
let Some(&c) = backward_iter.peek() else {
|
||||
return Some(word_start..word_end);
|
||||
};
|
||||
// Go backwards to include the whitespace.
|
||||
if CharacterKind::from(c) == CharacterKind::Whitespace {
|
||||
for c in backward_iter {
|
||||
if CharacterKind::from(c) != CharacterKind::Whitespace {
|
||||
break;
|
||||
}
|
||||
word_start -= 1;
|
||||
}
|
||||
}
|
||||
return Some(word_start..word_end);
|
||||
};
|
||||
|
||||
// If we made it this far, there is content ahead of this context in the buffer.
|
||||
if CharacterKind::from(c) == CharacterKind::Whitespace {
|
||||
// If the content ahead as whitespace, include that.
|
||||
for c in forward_iter {
|
||||
if CharacterKind::from(c) != CharacterKind::Whitespace {
|
||||
break;
|
||||
}
|
||||
word_end += 1;
|
||||
}
|
||||
} else {
|
||||
// If the content ahead is not whitespace, try to include any whitespace behind.
|
||||
let Some(&c) = backward_iter.peek() else {
|
||||
return Some(word_start..word_end);
|
||||
};
|
||||
if CharacterKind::from(c) == CharacterKind::Whitespace {
|
||||
for c in backward_iter {
|
||||
if CharacterKind::from(c) != CharacterKind::Whitespace {
|
||||
break;
|
||||
}
|
||||
word_start -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(word_start..word_end)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "word_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,451 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_vim_inner_word() {
|
||||
assert_eq!(vim_inner_word("", 0, WordType::Default), None);
|
||||
assert_eq!(
|
||||
vim_inner_word("foo", 0, WordType::Default).unwrap(),
|
||||
0.into()..3.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_inner_word("foo<<?>>", 4, WordType::Default).unwrap(),
|
||||
3.into()..8.into()
|
||||
);
|
||||
|
||||
// 000000000011111111112222222222333333333344444444445555555555666666666677
|
||||
// 012345678901234567890123456789012345678901234567890123456789012345678901
|
||||
let line = "impl<'a, T: TextBuffer + ?Sized + 'a> Iterator for WordBoundariesVim {";
|
||||
for i in 0..=3 {
|
||||
assert_eq!(
|
||||
vim_inner_word(line, i, WordType::Default).unwrap(),
|
||||
0.into()..4.into()
|
||||
);
|
||||
}
|
||||
for i in 4..=5 {
|
||||
assert_eq!(
|
||||
vim_inner_word(line, i, WordType::Default).unwrap(),
|
||||
4.into()..6.into()
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
vim_inner_word(line, 6, WordType::Default).unwrap(),
|
||||
6.into()..7.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_inner_word(line, 7, WordType::Default).unwrap(),
|
||||
7.into()..8.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_inner_word(line, 8, WordType::Default).unwrap(),
|
||||
8.into()..9.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_inner_word(line, 9, WordType::Default).unwrap(),
|
||||
9.into()..10.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_inner_word(line, 10, WordType::Default).unwrap(),
|
||||
10.into()..11.into()
|
||||
);
|
||||
for i in 12..=21 {
|
||||
assert_eq!(
|
||||
vim_inner_word(line, i, WordType::Default).unwrap(),
|
||||
12.into()..22.into()
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
vim_inner_word(line, 22, WordType::Default).unwrap(),
|
||||
22.into()..23.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_inner_word(line, 23, WordType::Default).unwrap(),
|
||||
23.into()..24.into()
|
||||
);
|
||||
for i in 26..=30 {
|
||||
assert_eq!(
|
||||
vim_inner_word(line, i, WordType::Default).unwrap(),
|
||||
26.into()..31.into()
|
||||
);
|
||||
}
|
||||
for i in 37..=39 {
|
||||
assert_eq!(
|
||||
vim_inner_word(line, i, WordType::Default).unwrap(),
|
||||
37.into()..40.into()
|
||||
);
|
||||
}
|
||||
for i in 40..=47 {
|
||||
assert_eq!(
|
||||
vim_inner_word(line, i, WordType::Default).unwrap(),
|
||||
40.into()..48.into()
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
vim_inner_word(line, 48, WordType::Default).unwrap(),
|
||||
48.into()..49.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_inner_word(line, 71, WordType::Default).unwrap(),
|
||||
71.into()..72.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vim_a_word() {
|
||||
assert_eq!(vim_a_word("", 0, WordType::Default), None);
|
||||
assert_eq!(
|
||||
vim_a_word("foo", 0, WordType::Default).unwrap(),
|
||||
0.into()..3.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_a_word(" foo", 0, WordType::Default).unwrap(),
|
||||
0.into()..5.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_a_word(" foo", 1, WordType::Default).unwrap(),
|
||||
0.into()..5.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_a_word(" foo", 3, WordType::Default).unwrap(),
|
||||
0.into()..5.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_a_word("foo ", 1, WordType::Default).unwrap(),
|
||||
0.into()..4.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_a_word("foo ", 1, WordType::Default).unwrap(),
|
||||
0.into()..5.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_a_word("foo ", 3, WordType::Default).unwrap(),
|
||||
3.into()..5.into()
|
||||
);
|
||||
|
||||
// 00000000001111111111222222222233333333334444444444555555
|
||||
// 01234567890123456789012345678901234567890123456789012345
|
||||
let line = "impl<T> Thing<T> { fn foo(&self,foo :&Foo, a:i32) {{{";
|
||||
|
||||
for i in 0..=3 {
|
||||
assert_eq!(
|
||||
vim_a_word(line, i, WordType::Default).unwrap(),
|
||||
0.into()..4.into()
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
vim_a_word(line, 4, WordType::Default).unwrap(),
|
||||
4.into()..5.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_a_word(line, 5, WordType::Default).unwrap(),
|
||||
5.into()..6.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_a_word(line, 6, WordType::Default).unwrap(),
|
||||
6.into()..9.into()
|
||||
);
|
||||
for i in 7..=13 {
|
||||
assert_eq!(
|
||||
vim_a_word(line, i, WordType::Default).unwrap(),
|
||||
7.into()..14.into()
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
vim_a_word(line, 16, WordType::Default).unwrap(),
|
||||
16.into()..18.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_a_word(line, 17, WordType::Default).unwrap(),
|
||||
17.into()..19.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_a_word(line, 18, WordType::Default).unwrap(),
|
||||
18.into()..20.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_a_word(line, 19, WordType::Default).unwrap(),
|
||||
19.into()..22.into()
|
||||
);
|
||||
for i in 20..=21 {
|
||||
assert_eq!(
|
||||
vim_a_word(line, i, WordType::Default).unwrap(),
|
||||
20.into()..23.into()
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
vim_a_word(line, 22, WordType::Default).unwrap(),
|
||||
22.into()..26.into()
|
||||
);
|
||||
for i in 23..=25 {
|
||||
assert_eq!(
|
||||
vim_a_word(line, i, WordType::Default).unwrap(),
|
||||
22.into()..26.into()
|
||||
);
|
||||
}
|
||||
for i in 26..=27 {
|
||||
assert_eq!(
|
||||
vim_a_word(line, i, WordType::Default).unwrap(),
|
||||
26.into()..28.into()
|
||||
);
|
||||
}
|
||||
for i in 28..=31 {
|
||||
assert_eq!(
|
||||
vim_a_word(line, i, WordType::Default).unwrap(),
|
||||
28.into()..32.into()
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
vim_a_word(line, 32, WordType::Default).unwrap(),
|
||||
32.into()..33.into()
|
||||
);
|
||||
for i in 33..=35 {
|
||||
assert_eq!(
|
||||
vim_a_word(line, i, WordType::Default).unwrap(),
|
||||
33.into()..39.into()
|
||||
);
|
||||
}
|
||||
for i in 36..=40 {
|
||||
assert_eq!(
|
||||
vim_a_word(line, i, WordType::Default).unwrap(),
|
||||
36.into()..41.into()
|
||||
);
|
||||
}
|
||||
for i in 41..=43 {
|
||||
assert_eq!(
|
||||
vim_a_word(line, i, WordType::Default).unwrap(),
|
||||
41.into()..44.into()
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
vim_a_word(line, 44, WordType::Default).unwrap(),
|
||||
44.into()..46.into()
|
||||
);
|
||||
for i in 45..=46 {
|
||||
assert_eq!(
|
||||
vim_a_word(line, i, WordType::Default).unwrap(),
|
||||
45.into()..47.into()
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
vim_a_word(line, 47, WordType::Default).unwrap(),
|
||||
47.into()..48.into()
|
||||
);
|
||||
for i in 48..=50 {
|
||||
assert_eq!(
|
||||
vim_a_word(line, i, WordType::Default).unwrap(),
|
||||
48.into()..51.into()
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
vim_a_word(line, 51, WordType::Default).unwrap(),
|
||||
51.into()..53.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_a_word(line, 52, WordType::Default).unwrap(),
|
||||
52.into()..56.into()
|
||||
);
|
||||
for i in 53..=55 {
|
||||
assert_eq!(
|
||||
vim_a_word(line, i, WordType::Default).unwrap(),
|
||||
52.into()..56.into()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vim_inner_bigword() {
|
||||
assert_eq!(vim_inner_word("", 0, WordType::BigWord), None);
|
||||
assert_eq!(
|
||||
vim_inner_word("foo", 0, WordType::BigWord).unwrap(),
|
||||
0.into()..3.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_inner_word("foo<<?>>", 4, WordType::BigWord).unwrap(),
|
||||
0.into()..8.into()
|
||||
);
|
||||
|
||||
// 000000000011111111112222222222333333333344444444445555555555666666666677
|
||||
// 012345678901234567890123456789012345678901234567890123456789012345678901
|
||||
let line = "impl<'a, T: TextBuffer + ?Sized + 'a> Iterator for WordBoundariesVim {";
|
||||
for i in 0..=7 {
|
||||
assert_eq!(
|
||||
vim_inner_word(line, i, WordType::BigWord).unwrap(),
|
||||
0.into()..8.into()
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
vim_inner_word(line, 8, WordType::BigWord).unwrap(),
|
||||
8.into()..9.into()
|
||||
);
|
||||
for i in 9..=10 {
|
||||
assert_eq!(
|
||||
vim_inner_word(line, i, WordType::BigWord).unwrap(),
|
||||
9.into()..11.into()
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
vim_inner_word(line, 11, WordType::BigWord).unwrap(),
|
||||
11.into()..12.into()
|
||||
);
|
||||
for i in 12..=21 {
|
||||
assert_eq!(
|
||||
vim_inner_word(line, i, WordType::BigWord).unwrap(),
|
||||
12.into()..22.into()
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
vim_inner_word(line, 22, WordType::BigWord).unwrap(),
|
||||
22.into()..23.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_inner_word(line, 23, WordType::BigWord).unwrap(),
|
||||
23.into()..24.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_inner_word(line, 24, WordType::BigWord).unwrap(),
|
||||
24.into()..25.into()
|
||||
);
|
||||
for i in 25..=30 {
|
||||
assert_eq!(
|
||||
vim_inner_word(line, i, WordType::BigWord).unwrap(),
|
||||
25.into()..31.into()
|
||||
);
|
||||
}
|
||||
for i in 34..=36 {
|
||||
assert_eq!(
|
||||
vim_inner_word(line, i, WordType::BigWord).unwrap(),
|
||||
34.into()..37.into()
|
||||
);
|
||||
}
|
||||
for i in 37..=39 {
|
||||
assert_eq!(
|
||||
vim_inner_word(line, i, WordType::BigWord).unwrap(),
|
||||
37.into()..40.into()
|
||||
);
|
||||
}
|
||||
for i in 40..=47 {
|
||||
assert_eq!(
|
||||
vim_inner_word(line, i, WordType::BigWord).unwrap(),
|
||||
40.into()..48.into()
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
vim_inner_word(line, 48, WordType::BigWord).unwrap(),
|
||||
48.into()..49.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_inner_word(line, 71, WordType::BigWord).unwrap(),
|
||||
71.into()..72.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vim_a_bigword() {
|
||||
assert_eq!(vim_a_word("", 0, WordType::BigWord), None);
|
||||
assert_eq!(
|
||||
vim_a_word("foo.bar", 0, WordType::BigWord).unwrap(),
|
||||
0.into()..7.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_a_word(" foo.bar", 0, WordType::BigWord).unwrap(),
|
||||
0.into()..9.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_a_word(" foo.bar", 1, WordType::BigWord).unwrap(),
|
||||
0.into()..9.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_a_word(" foo.bar", 3, WordType::BigWord).unwrap(),
|
||||
0.into()..9.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_a_word("foo.bar ", 1, WordType::BigWord).unwrap(),
|
||||
0.into()..8.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_a_word("foo.bar ", 1, WordType::BigWord).unwrap(),
|
||||
0.into()..9.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_a_word("foo.bar ", 7, WordType::BigWord).unwrap(),
|
||||
7.into()..9.into()
|
||||
);
|
||||
|
||||
// 00000000001111111111222222222233333333334444444444555555
|
||||
// 01234567890123456789012345678901234567890123456789012345
|
||||
let line = "impl<T> Thing<T> { fn foo(&self,foo :&Foo, a:i32) {{{";
|
||||
|
||||
for i in 0..=6 {
|
||||
assert_eq!(
|
||||
vim_a_word(line, i, WordType::BigWord).unwrap(),
|
||||
0.into()..9.into()
|
||||
);
|
||||
}
|
||||
for i in 7..=8 {
|
||||
assert_eq!(
|
||||
vim_a_word(line, i, WordType::BigWord).unwrap(),
|
||||
7.into()..17.into()
|
||||
);
|
||||
}
|
||||
for i in 9..=16 {
|
||||
assert_eq!(
|
||||
vim_a_word(line, i, WordType::BigWord).unwrap(),
|
||||
9.into()..18.into()
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
vim_a_word(line, 17, WordType::BigWord).unwrap(),
|
||||
17.into()..19.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_a_word(line, 18, WordType::BigWord).unwrap(),
|
||||
18.into()..20.into()
|
||||
);
|
||||
assert_eq!(
|
||||
vim_a_word(line, 19, WordType::BigWord).unwrap(),
|
||||
19.into()..22.into()
|
||||
);
|
||||
for i in 20..=21 {
|
||||
assert_eq!(
|
||||
vim_a_word(line, i, WordType::BigWord).unwrap(),
|
||||
20.into()..23.into()
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
vim_a_word(line, 22, WordType::BigWord).unwrap(),
|
||||
22.into()..36.into()
|
||||
);
|
||||
for i in 23..=35 {
|
||||
assert_eq!(
|
||||
vim_a_word(line, i, WordType::BigWord).unwrap(),
|
||||
23.into()..39.into()
|
||||
);
|
||||
}
|
||||
for i in 36..=38 {
|
||||
assert_eq!(
|
||||
vim_a_word(line, i, WordType::BigWord).unwrap(),
|
||||
36.into()..45.into()
|
||||
);
|
||||
}
|
||||
for i in 39..=44 {
|
||||
assert_eq!(
|
||||
vim_a_word(line, i, WordType::BigWord).unwrap(),
|
||||
39.into()..46.into()
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
vim_a_word(line, 45, WordType::BigWord).unwrap(),
|
||||
45.into()..52.into()
|
||||
);
|
||||
for i in 46..=51 {
|
||||
assert_eq!(
|
||||
vim_a_word(line, i, WordType::BigWord).unwrap(),
|
||||
46.into()..53.into()
|
||||
);
|
||||
}
|
||||
for i in 52..=55 {
|
||||
assert_eq!(
|
||||
vim_a_word(line, i, WordType::BigWord).unwrap(),
|
||||
52.into()..56.into()
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,250 @@
|
||||
use std::iter::Peekable;
|
||||
|
||||
use anyhow::Result;
|
||||
use itertools::{peek_nth, Either, PeekNth};
|
||||
use string_offset::CharOffset;
|
||||
use warpui::text::{words::is_default_word_boundary, TextBuffer};
|
||||
|
||||
use crate::vim::{Direction, WordBound, WordType};
|
||||
|
||||
/// The "kind" of character that a char is. "Symbols" here refer to non-whitespace characters that
|
||||
/// are traditionally word-breaking characters, e.g. punctuation and brackets. Vim treats contiguous
|
||||
/// symbols as their own "words" and hence they need to be tracked as a distinct context from
|
||||
/// word-characters and whitespace.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(super) enum CharacterKind {
|
||||
WordChars,
|
||||
Symbols,
|
||||
Whitespace,
|
||||
}
|
||||
|
||||
impl CharacterKind {
|
||||
/// This is an alternative to `==` which will treat Self::WordChars and Self::Symbols as equal
|
||||
/// to one another for WordType::BigWord. This is useful for motions or text objects
|
||||
/// involving bigwords.
|
||||
pub(super) fn equivalent_char_kind(&self, other: &Self, word_type: WordType) -> bool {
|
||||
match word_type {
|
||||
WordType::Default => self == other,
|
||||
WordType::BigWord => {
|
||||
self == other
|
||||
|| (*self == Self::WordChars && *other == Self::Symbols
|
||||
|| *self == Self::Symbols && *other == Self::WordChars)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<char> for CharacterKind {
|
||||
fn from(c: char) -> Self {
|
||||
if c.is_whitespace() {
|
||||
CharacterKind::Whitespace
|
||||
} else if is_default_word_boundary(c) {
|
||||
CharacterKind::Symbols
|
||||
} else {
|
||||
CharacterKind::WordChars
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This function abstracts over the different specific types of word iterators. This is meant to
|
||||
/// be used by Views rather than any of these iterator structs.
|
||||
pub fn vim_word_iterator_from_offset<'a, T, C>(
|
||||
offset: C,
|
||||
buffer: &'a T,
|
||||
direction: Direction,
|
||||
bound: WordBound,
|
||||
word_type: WordType,
|
||||
) -> Result<Box<dyn Iterator<Item = CharOffset> + 'a>>
|
||||
where
|
||||
T: TextBuffer + ?Sized + 'a,
|
||||
C: Into<CharOffset>,
|
||||
{
|
||||
let offset = offset.into();
|
||||
match (direction, bound) {
|
||||
(Direction::Forward, WordBound::Start) | (Direction::Backward, WordBound::End) => Ok(
|
||||
Box::new(WordHeadsVim::new(offset, buffer, direction, word_type)?),
|
||||
),
|
||||
(Direction::Forward, WordBound::End) | (Direction::Backward, WordBound::Start) => Ok(
|
||||
Box::new(WordTailsVim::new(offset, buffer, direction, word_type)?),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// This struct represents the logic for pressing either w, W, ge, or gE in Vim.
|
||||
pub struct WordHeadsVim<'a, T: TextBuffer + ?Sized + 'a> {
|
||||
offset: CharOffset,
|
||||
chars: Peekable<Either<T::Chars<'a>, T::CharsReverse<'a>>>,
|
||||
cursor_context: CharacterKind,
|
||||
direction: Direction,
|
||||
word_type: WordType,
|
||||
done: bool,
|
||||
}
|
||||
|
||||
impl<'a, T: TextBuffer + ?Sized + 'a> WordHeadsVim<'a, T> {
|
||||
pub fn new(
|
||||
offset: CharOffset,
|
||||
buffer: &'a T,
|
||||
direction: Direction,
|
||||
word_type: WordType,
|
||||
) -> Result<Self> {
|
||||
let (offset, mut chars) = match direction {
|
||||
// TextBuffer::chars_rev_at does not include the character _at_ the originating offset.
|
||||
// However, Vim's word motion semantics require that we see the character at the
|
||||
// originating offset, so we need to +1 the offset before creating the reverse char
|
||||
// iterator.
|
||||
Direction::Backward => {
|
||||
let offset = offset + 1;
|
||||
(
|
||||
offset,
|
||||
Either::Right(buffer.chars_rev_at(offset)?).peekable(),
|
||||
)
|
||||
}
|
||||
Direction::Forward => (offset, Either::Left(buffer.chars_at(offset)?).peekable()),
|
||||
};
|
||||
Ok(Self {
|
||||
offset,
|
||||
cursor_context: chars
|
||||
.peek()
|
||||
.map_or(CharacterKind::WordChars, |c| (*c).into()),
|
||||
chars,
|
||||
direction,
|
||||
word_type,
|
||||
done: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn step(&mut self) {
|
||||
self.chars.next();
|
||||
match self.direction {
|
||||
Direction::Backward => self.offset -= 1,
|
||||
Direction::Forward => self.offset += 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: TextBuffer + ?Sized + 'a> Iterator for WordHeadsVim<'a, T> {
|
||||
type Item = CharOffset;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.done {
|
||||
return None;
|
||||
}
|
||||
|
||||
loop {
|
||||
self.step();
|
||||
let Some(&c) = self.chars.peek() else {
|
||||
break;
|
||||
};
|
||||
let prev_cursor_context = self.cursor_context;
|
||||
self.cursor_context = CharacterKind::from(c);
|
||||
|
||||
if !self
|
||||
.cursor_context
|
||||
.equivalent_char_kind(&prev_cursor_context, self.word_type)
|
||||
&& self.cursor_context != CharacterKind::Whitespace
|
||||
{
|
||||
return Some(match self.direction {
|
||||
Direction::Backward => self.offset - 1,
|
||||
Direction::Forward => self.offset,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
self.done = true;
|
||||
Some(self.offset)
|
||||
}
|
||||
}
|
||||
|
||||
/// This struct represents the logic for pressing either b, B, e, or E in Vim.
|
||||
pub struct WordTailsVim<'a, T: TextBuffer + ?Sized + 'a> {
|
||||
offset: CharOffset,
|
||||
chars: PeekNth<Either<T::Chars<'a>, T::CharsReverse<'a>>>,
|
||||
direction: Direction,
|
||||
word_type: WordType,
|
||||
done: bool,
|
||||
}
|
||||
|
||||
impl<'a, T: TextBuffer + ?Sized + 'a> WordTailsVim<'a, T> {
|
||||
pub fn new(
|
||||
offset: CharOffset,
|
||||
buffer: &'a T,
|
||||
direction: Direction,
|
||||
word_type: WordType,
|
||||
) -> Result<Self> {
|
||||
let (offset, chars) = match direction {
|
||||
// TextBuffer::chars_rev_at does not include the character _at_ the originating offset.
|
||||
// However, Vim's word motion semantics require that we see the character at the
|
||||
// originating offset, so we need to +1 the offset before creating the reverse char
|
||||
// iterator.
|
||||
Direction::Backward => {
|
||||
let offset = offset + 1;
|
||||
(
|
||||
offset,
|
||||
peek_nth(Either::Right(buffer.chars_rev_at(offset)?)),
|
||||
)
|
||||
}
|
||||
Direction::Forward => (offset, peek_nth(Either::Left(buffer.chars_at(offset)?))),
|
||||
};
|
||||
Ok(Self {
|
||||
offset,
|
||||
chars,
|
||||
direction,
|
||||
word_type,
|
||||
done: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn step(&mut self) {
|
||||
self.chars.next();
|
||||
match self.direction {
|
||||
Direction::Backward => self.offset -= 1,
|
||||
Direction::Forward => self.offset += 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: TextBuffer + ?Sized + 'a> Iterator for WordTailsVim<'a, T> {
|
||||
type Item = CharOffset;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.done {
|
||||
return None;
|
||||
}
|
||||
|
||||
loop {
|
||||
if self.chars.peek_nth(1).is_none() {
|
||||
break;
|
||||
}
|
||||
self.step();
|
||||
|
||||
let Some(&c) = self.chars.peek() else {
|
||||
break;
|
||||
};
|
||||
let Some(&c_next) = self.chars.peek_nth(1) else {
|
||||
break;
|
||||
};
|
||||
|
||||
let cursor_context = CharacterKind::from(c);
|
||||
let cursor_context_next = CharacterKind::from(c_next);
|
||||
|
||||
if !cursor_context.equivalent_char_kind(&cursor_context_next, self.word_type)
|
||||
&& cursor_context != CharacterKind::Whitespace
|
||||
{
|
||||
return Some(match self.direction {
|
||||
Direction::Backward => self.offset - 1,
|
||||
Direction::Forward => self.offset,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
self.done = true;
|
||||
Some(match self.direction {
|
||||
Direction::Backward => self.offset - 1,
|
||||
Direction::Forward => self.offset,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "word_iterator_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,573 @@
|
||||
use super::*;
|
||||
|
||||
// 0000000000111111111122222222223333333333444444444455555555556666666666
|
||||
// 0123456789012345678901234567890123456789012345678901234567890123456789
|
||||
const LINE: &str = "impl<'a, T: TextBuffer + ?Sized + 'a> Iterator for WordBoundariesVim";
|
||||
|
||||
/// The behavior for pressing "w".
|
||||
#[test]
|
||||
fn test_word_forward_heads() {
|
||||
let mut iter1 = vim_word_iterator_from_offset(
|
||||
0,
|
||||
LINE,
|
||||
Direction::Forward,
|
||||
WordBound::Start,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(iter1.next(), Some(4.into()));
|
||||
assert_eq!(iter1.next(), Some(6.into()));
|
||||
assert_eq!(iter1.next(), Some(7.into()));
|
||||
assert_eq!(iter1.next(), Some(9.into()));
|
||||
assert_eq!(iter1.next(), Some(10.into()));
|
||||
assert_eq!(iter1.next(), Some(12.into()));
|
||||
assert_eq!(iter1.next(), Some(23.into()));
|
||||
assert_eq!(iter1.next(), Some(25.into()));
|
||||
assert_eq!(iter1.next(), Some(26.into()));
|
||||
assert_eq!(iter1.next(), Some(32.into()));
|
||||
assert_eq!(iter1.next(), Some(34.into()));
|
||||
assert_eq!(iter1.next(), Some(35.into()));
|
||||
assert_eq!(iter1.next(), Some(36.into()));
|
||||
assert_eq!(iter1.next(), Some(40.into()));
|
||||
assert_eq!(iter1.next(), Some(49.into()));
|
||||
assert_eq!(iter1.next(), Some(53.into()));
|
||||
assert_eq!(iter1.next(), Some(70.into()));
|
||||
assert_eq!(iter1.next(), None);
|
||||
|
||||
let mut iter2 = vim_word_iterator_from_offset(
|
||||
15,
|
||||
LINE,
|
||||
Direction::Forward,
|
||||
WordBound::Start,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(iter2.next(), Some(23.into()));
|
||||
|
||||
let mut iter3 = vim_word_iterator_from_offset(
|
||||
43,
|
||||
LINE,
|
||||
Direction::Forward,
|
||||
WordBound::Start,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(iter3.next(), Some(49.into()));
|
||||
|
||||
let mut iter4 = vim_word_iterator_from_offset(
|
||||
38,
|
||||
LINE,
|
||||
Direction::Forward,
|
||||
WordBound::Start,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(iter4.next(), Some(40.into()));
|
||||
|
||||
let mut iter5 = vim_word_iterator_from_offset(
|
||||
69,
|
||||
LINE,
|
||||
Direction::Forward,
|
||||
WordBound::Start,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(iter5.next(), Some(70.into()));
|
||||
assert_eq!(iter5.next(), None);
|
||||
}
|
||||
|
||||
/// The behavior for pressing "W".
|
||||
#[test]
|
||||
fn test_word_forward_heads_including_symbols() {
|
||||
let mut iter1 = vim_word_iterator_from_offset(
|
||||
0,
|
||||
LINE,
|
||||
Direction::Forward,
|
||||
WordBound::Start,
|
||||
WordType::BigWord,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(iter1.next(), Some(9.into()));
|
||||
assert_eq!(iter1.next(), Some(12.into()));
|
||||
assert_eq!(iter1.next(), Some(23.into()));
|
||||
assert_eq!(iter1.next(), Some(25.into()));
|
||||
assert_eq!(iter1.next(), Some(32.into()));
|
||||
assert_eq!(iter1.next(), Some(34.into()));
|
||||
assert_eq!(iter1.next(), Some(40.into()));
|
||||
assert_eq!(iter1.next(), Some(49.into()));
|
||||
assert_eq!(iter1.next(), Some(53.into()));
|
||||
assert_eq!(iter1.next(), Some(70.into()));
|
||||
assert_eq!(iter1.next(), None);
|
||||
|
||||
let mut iter2 = vim_word_iterator_from_offset(
|
||||
15,
|
||||
LINE,
|
||||
Direction::Forward,
|
||||
WordBound::Start,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(iter2.next(), Some(23.into()));
|
||||
|
||||
let mut iter3 = vim_word_iterator_from_offset(
|
||||
43,
|
||||
LINE,
|
||||
Direction::Forward,
|
||||
WordBound::Start,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(iter3.next(), Some(49.into()));
|
||||
|
||||
let mut iter4 = vim_word_iterator_from_offset(
|
||||
38,
|
||||
LINE,
|
||||
Direction::Forward,
|
||||
WordBound::Start,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(iter4.next(), Some(40.into()));
|
||||
|
||||
let mut iter5 = vim_word_iterator_from_offset(
|
||||
69,
|
||||
LINE,
|
||||
Direction::Forward,
|
||||
WordBound::Start,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(iter5.next(), Some(70.into()));
|
||||
assert_eq!(iter5.next(), None);
|
||||
}
|
||||
|
||||
/// The behavior for pressing "ge"
|
||||
#[test]
|
||||
fn test_word_backward_heads() {
|
||||
let mut iter1 = vim_word_iterator_from_offset(
|
||||
69,
|
||||
LINE,
|
||||
Direction::Backward,
|
||||
WordBound::End,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(iter1.next(), Some(51.into()));
|
||||
assert_eq!(iter1.next(), Some(47.into()));
|
||||
assert_eq!(iter1.next(), Some(36.into()));
|
||||
assert_eq!(iter1.next(), Some(35.into()));
|
||||
assert_eq!(iter1.next(), Some(34.into()));
|
||||
assert_eq!(iter1.next(), Some(32.into()));
|
||||
assert_eq!(iter1.next(), Some(30.into()));
|
||||
assert_eq!(iter1.next(), Some(25.into()));
|
||||
assert_eq!(iter1.next(), Some(23.into()));
|
||||
assert_eq!(iter1.next(), Some(21.into()));
|
||||
assert_eq!(iter1.next(), Some(10.into()));
|
||||
assert_eq!(iter1.next(), Some(9.into()));
|
||||
assert_eq!(iter1.next(), Some(7.into()));
|
||||
assert_eq!(iter1.next(), Some(6.into()));
|
||||
assert_eq!(iter1.next(), Some(5.into()));
|
||||
assert_eq!(iter1.next(), Some(3.into()));
|
||||
assert_eq!(iter1.next(), Some(0.into()));
|
||||
assert_eq!(iter1.next(), None);
|
||||
|
||||
let mut iter2 = vim_word_iterator_from_offset(
|
||||
15,
|
||||
LINE,
|
||||
Direction::Backward,
|
||||
WordBound::End,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(iter2.next(), Some(10.into()));
|
||||
|
||||
let mut iter3 = vim_word_iterator_from_offset(
|
||||
43,
|
||||
LINE,
|
||||
Direction::Backward,
|
||||
WordBound::End,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(iter3.next(), Some(36.into()));
|
||||
|
||||
let mut iter4 = vim_word_iterator_from_offset(
|
||||
38,
|
||||
LINE,
|
||||
Direction::Backward,
|
||||
WordBound::End,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(iter4.next(), Some(36.into()));
|
||||
|
||||
let mut iter5 = vim_word_iterator_from_offset(
|
||||
0,
|
||||
LINE,
|
||||
Direction::Backward,
|
||||
WordBound::End,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(iter5.next(), Some(0.into()));
|
||||
assert_eq!(iter5.next(), None);
|
||||
}
|
||||
|
||||
/// The behavior for pressing "gE"
|
||||
#[test]
|
||||
fn test_word_backward_heads_including_symbols() {
|
||||
let mut iter1 = vim_word_iterator_from_offset(
|
||||
69,
|
||||
LINE,
|
||||
Direction::Backward,
|
||||
WordBound::End,
|
||||
WordType::BigWord,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(iter1.next(), Some(51.into()));
|
||||
assert_eq!(iter1.next(), Some(47.into()));
|
||||
assert_eq!(iter1.next(), Some(36.into()));
|
||||
assert_eq!(iter1.next(), Some(32.into()));
|
||||
assert_eq!(iter1.next(), Some(30.into()));
|
||||
assert_eq!(iter1.next(), Some(23.into()));
|
||||
assert_eq!(iter1.next(), Some(21.into()));
|
||||
assert_eq!(iter1.next(), Some(10.into()));
|
||||
assert_eq!(iter1.next(), Some(7.into()));
|
||||
assert_eq!(iter1.next(), Some(0.into()));
|
||||
assert_eq!(iter1.next(), None);
|
||||
|
||||
let mut iter2 = vim_word_iterator_from_offset(
|
||||
15,
|
||||
LINE,
|
||||
Direction::Backward,
|
||||
WordBound::End,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(iter2.next(), Some(10.into()));
|
||||
|
||||
let mut iter3 = vim_word_iterator_from_offset(
|
||||
43,
|
||||
LINE,
|
||||
Direction::Backward,
|
||||
WordBound::End,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(iter3.next(), Some(36.into()));
|
||||
|
||||
let mut iter4 = vim_word_iterator_from_offset(
|
||||
38,
|
||||
LINE,
|
||||
Direction::Backward,
|
||||
WordBound::End,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(iter4.next(), Some(36.into()));
|
||||
|
||||
let mut iter5 = vim_word_iterator_from_offset(
|
||||
0,
|
||||
LINE,
|
||||
Direction::Backward,
|
||||
WordBound::End,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(iter5.next(), Some(0.into()));
|
||||
assert_eq!(iter5.next(), None);
|
||||
}
|
||||
|
||||
/// The behavior for pressing "e"
|
||||
#[test]
|
||||
fn test_word_forward_tails() {
|
||||
let mut iter1 = vim_word_iterator_from_offset(
|
||||
0,
|
||||
LINE,
|
||||
Direction::Forward,
|
||||
WordBound::End,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(iter1.next(), Some(3.into()));
|
||||
assert_eq!(iter1.next(), Some(5.into()));
|
||||
assert_eq!(iter1.next(), Some(6.into()));
|
||||
assert_eq!(iter1.next(), Some(7.into()));
|
||||
assert_eq!(iter1.next(), Some(9.into()));
|
||||
assert_eq!(iter1.next(), Some(10.into()));
|
||||
assert_eq!(iter1.next(), Some(21.into()));
|
||||
assert_eq!(iter1.next(), Some(23.into()));
|
||||
assert_eq!(iter1.next(), Some(25.into()));
|
||||
assert_eq!(iter1.next(), Some(30.into()));
|
||||
assert_eq!(iter1.next(), Some(32.into()));
|
||||
assert_eq!(iter1.next(), Some(34.into()));
|
||||
assert_eq!(iter1.next(), Some(35.into()));
|
||||
assert_eq!(iter1.next(), Some(36.into()));
|
||||
assert_eq!(iter1.next(), Some(47.into()));
|
||||
assert_eq!(iter1.next(), Some(51.into()));
|
||||
assert_eq!(iter1.next(), Some(69.into()));
|
||||
assert_eq!(iter1.next(), None);
|
||||
|
||||
let mut iter2 = vim_word_iterator_from_offset(
|
||||
15,
|
||||
LINE,
|
||||
Direction::Forward,
|
||||
WordBound::End,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(iter2.next(), Some(21.into()));
|
||||
|
||||
let mut iter3 = vim_word_iterator_from_offset(
|
||||
43,
|
||||
LINE,
|
||||
Direction::Forward,
|
||||
WordBound::End,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(iter3.next(), Some(47.into()));
|
||||
|
||||
let mut iter4 = vim_word_iterator_from_offset(
|
||||
38,
|
||||
LINE,
|
||||
Direction::Forward,
|
||||
WordBound::End,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(iter4.next(), Some(47.into()));
|
||||
|
||||
let mut iter5 = vim_word_iterator_from_offset(
|
||||
69,
|
||||
LINE,
|
||||
Direction::Forward,
|
||||
WordBound::End,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(iter5.next(), Some(69.into()));
|
||||
assert_eq!(iter5.next(), None);
|
||||
}
|
||||
|
||||
/// The behavior for pressing "E"
|
||||
#[test]
|
||||
fn test_word_forward_tails_including_symbols() {
|
||||
let mut iter1 = vim_word_iterator_from_offset(
|
||||
0,
|
||||
LINE,
|
||||
Direction::Forward,
|
||||
WordBound::End,
|
||||
WordType::BigWord,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(iter1.next(), Some(7.into()));
|
||||
assert_eq!(iter1.next(), Some(10.into()));
|
||||
assert_eq!(iter1.next(), Some(21.into()));
|
||||
assert_eq!(iter1.next(), Some(23.into()));
|
||||
assert_eq!(iter1.next(), Some(30.into()));
|
||||
assert_eq!(iter1.next(), Some(32.into()));
|
||||
assert_eq!(iter1.next(), Some(36.into()));
|
||||
assert_eq!(iter1.next(), Some(47.into()));
|
||||
assert_eq!(iter1.next(), Some(51.into()));
|
||||
assert_eq!(iter1.next(), Some(69.into()));
|
||||
assert_eq!(iter1.next(), None);
|
||||
|
||||
let mut iter2 = vim_word_iterator_from_offset(
|
||||
15,
|
||||
LINE,
|
||||
Direction::Forward,
|
||||
WordBound::End,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(iter2.next(), Some(21.into()));
|
||||
|
||||
let mut iter3 = vim_word_iterator_from_offset(
|
||||
43,
|
||||
LINE,
|
||||
Direction::Forward,
|
||||
WordBound::End,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(iter3.next(), Some(47.into()));
|
||||
|
||||
let mut iter4 = vim_word_iterator_from_offset(
|
||||
38,
|
||||
LINE,
|
||||
Direction::Forward,
|
||||
WordBound::End,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(iter4.next(), Some(47.into()));
|
||||
|
||||
let mut iter5 = vim_word_iterator_from_offset(
|
||||
69,
|
||||
LINE,
|
||||
Direction::Forward,
|
||||
WordBound::End,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(iter5.next(), Some(69.into()));
|
||||
assert_eq!(iter5.next(), None);
|
||||
}
|
||||
|
||||
/// The behavior for pressing "b"
|
||||
#[test]
|
||||
fn test_word_backward_tails() {
|
||||
let mut iter1 = vim_word_iterator_from_offset(
|
||||
69,
|
||||
LINE,
|
||||
Direction::Backward,
|
||||
WordBound::Start,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(iter1.next(), Some(53.into()));
|
||||
assert_eq!(iter1.next(), Some(49.into()));
|
||||
assert_eq!(iter1.next(), Some(40.into()));
|
||||
assert_eq!(iter1.next(), Some(36.into()));
|
||||
assert_eq!(iter1.next(), Some(35.into()));
|
||||
assert_eq!(iter1.next(), Some(34.into()));
|
||||
assert_eq!(iter1.next(), Some(32.into()));
|
||||
assert_eq!(iter1.next(), Some(26.into()));
|
||||
assert_eq!(iter1.next(), Some(25.into()));
|
||||
assert_eq!(iter1.next(), Some(23.into()));
|
||||
assert_eq!(iter1.next(), Some(12.into()));
|
||||
assert_eq!(iter1.next(), Some(10.into()));
|
||||
assert_eq!(iter1.next(), Some(9.into()));
|
||||
assert_eq!(iter1.next(), Some(7.into()));
|
||||
assert_eq!(iter1.next(), Some(6.into()));
|
||||
assert_eq!(iter1.next(), Some(4.into()));
|
||||
assert_eq!(iter1.next(), Some(0.into()));
|
||||
assert_eq!(iter1.next(), None);
|
||||
|
||||
let mut iter2 = vim_word_iterator_from_offset(
|
||||
15,
|
||||
LINE,
|
||||
Direction::Backward,
|
||||
WordBound::Start,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(iter2.next(), Some(12.into()));
|
||||
|
||||
let mut iter3 = vim_word_iterator_from_offset(
|
||||
43,
|
||||
LINE,
|
||||
Direction::Backward,
|
||||
WordBound::Start,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(iter3.next(), Some(40.into()));
|
||||
|
||||
let mut iter4 = vim_word_iterator_from_offset(
|
||||
38,
|
||||
LINE,
|
||||
Direction::Backward,
|
||||
WordBound::Start,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(iter4.next(), Some(36.into()));
|
||||
|
||||
let mut iter5 = vim_word_iterator_from_offset(
|
||||
0,
|
||||
LINE,
|
||||
Direction::Backward,
|
||||
WordBound::Start,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(iter5.next(), Some(0.into()));
|
||||
assert_eq!(iter5.next(), None);
|
||||
}
|
||||
|
||||
/// The behavior for pressing "B"
|
||||
#[test]
|
||||
fn test_word_backward_tails_including_symbols() {
|
||||
let mut iter1 = vim_word_iterator_from_offset(
|
||||
69,
|
||||
LINE,
|
||||
Direction::Backward,
|
||||
WordBound::Start,
|
||||
WordType::BigWord,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(iter1.next(), Some(53.into()));
|
||||
assert_eq!(iter1.next(), Some(49.into()));
|
||||
assert_eq!(iter1.next(), Some(40.into()));
|
||||
assert_eq!(iter1.next(), Some(34.into()));
|
||||
assert_eq!(iter1.next(), Some(32.into()));
|
||||
assert_eq!(iter1.next(), Some(25.into()));
|
||||
assert_eq!(iter1.next(), Some(23.into()));
|
||||
assert_eq!(iter1.next(), Some(12.into()));
|
||||
assert_eq!(iter1.next(), Some(9.into()));
|
||||
assert_eq!(iter1.next(), Some(0.into()));
|
||||
assert_eq!(iter1.next(), None);
|
||||
|
||||
let mut iter2 = vim_word_iterator_from_offset(
|
||||
15,
|
||||
LINE,
|
||||
Direction::Backward,
|
||||
WordBound::Start,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(iter2.next(), Some(12.into()));
|
||||
|
||||
let mut iter3 = vim_word_iterator_from_offset(
|
||||
43,
|
||||
LINE,
|
||||
Direction::Backward,
|
||||
WordBound::Start,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(iter3.next(), Some(40.into()));
|
||||
|
||||
let mut iter4 = vim_word_iterator_from_offset(
|
||||
38,
|
||||
LINE,
|
||||
Direction::Backward,
|
||||
WordBound::Start,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(iter4.next(), Some(36.into()));
|
||||
|
||||
let mut iter5 = vim_word_iterator_from_offset(
|
||||
0,
|
||||
LINE,
|
||||
Direction::Backward,
|
||||
WordBound::Start,
|
||||
WordType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(iter5.next(), Some(0.into()));
|
||||
assert_eq!(iter5.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_out_of_bounds_is_error() {
|
||||
assert!(vim_word_iterator_from_offset(
|
||||
70,
|
||||
LINE,
|
||||
Direction::Backward,
|
||||
WordBound::Start,
|
||||
WordType::BigWord
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
Reference in New Issue
Block a user