Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
+132
View File
@@ -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;
+147
View File
@@ -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())
);
}
}
+13
View File
@@ -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::*;
+102
View File
@@ -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());
}
+118
View File
@@ -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;
+114
View File
@@ -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)
}
}
+152
View File
@@ -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;
+451
View File
@@ -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()
);
}
}