use hjkl_engine::abbrev::{Abbrev, AbbrevKind, AbbrevTrigger};
use hjkl_vim_types::{RangeKind, TextObject};
use hjkl_engine::rope_util::{rope_line_to_str, rope_to_lines_vec};
use crate::vim_state::vim;
use hjkl_engine::Editor;
use hjkl_engine::buf_helpers::{buf_cursor_pos, buf_line, buf_set_cursor_rc};
pub type Pos = (usize, usize);
pub fn text_object_range<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
obj: TextObject,
inner: bool,
count: usize,
) -> Option<(Pos, Pos, RangeKind)> {
match obj {
TextObject::Word { big } => {
word_text_object(ed, inner, big, count).map(|(s, e)| (s, e, RangeKind::Exclusive))
}
TextObject::Quote(q) => {
quote_text_object(ed, q, inner).map(|(s, e)| (s, e, RangeKind::Exclusive))
}
TextObject::Bracket(open) => bracket_text_object(ed, open, inner, count),
TextObject::Paragraph => {
paragraph_text_object(ed, inner, count).map(|(s, e)| (s, e, RangeKind::Linewise))
}
TextObject::XmlTag => tag_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Exclusive)),
TextObject::Sentence => {
sentence_text_object(ed, inner, count).map(|(s, e)| (s, e, RangeKind::Exclusive))
}
}
}
fn is_sentence_terminator(c: char) -> bool {
matches!(c, '.' | '?' | '!')
}
fn is_sentence_closing(c: char) -> bool {
matches!(c, ')' | ']' | '"' | '\'')
}
pub fn sentence_boundary<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
forward: bool,
) -> Option<(usize, usize)> {
let rope = hjkl_engine::types::Query::rope(ed.buffer());
let raw_n_lines = rope.len_lines();
if raw_n_lines == 0 {
return None;
}
let line_of = |r: usize| -> std::borrow::Cow<'_, str> {
let start = rope.line_to_byte(r);
rope.byte_slice(start..start + hjkl_buffer::rope_line_bytes(&rope, r))
.into()
};
let line_chars = |r: usize| -> Vec<char> { line_of(r).chars().collect() };
let n_lines = if raw_n_lines > 1 && line_chars(raw_n_lines - 1).is_empty() {
raw_n_lines - 1
} else {
raw_n_lines
};
if n_lines == 0 {
return None;
}
let cursor = ed.cursor();
let cursor = (cursor.0.min(n_lines - 1), cursor.1);
let (cr, cc) = cursor;
if forward {
if let Some(p) = first_sentence_boundary_forward(&line_chars, cr, cc, n_lines) {
return Some(p);
}
let end_col = line_chars(n_lines - 1).len().saturating_sub(1);
let end = (n_lines - 1, end_col);
(end > cursor).then_some(end)
} else {
let mut origin: Option<(usize, bool)> = if cr == 0 {
None
} else {
closest_stopper_below(&line_chars, cr - 1)
};
for r in (0..=cr).rev() {
let blank = line_chars(r).is_empty();
let first_ns = line_chars(r).iter().position(|&c| !c.is_whitespace());
let (mid, _has_eol) = scan_row_boundaries(r, &line_chars);
let mut cands: Vec<(usize, usize)> = Vec::new();
cands.extend(mid.into_iter().rev());
if origin.is_some_and(|(_, eol)| eol)
&& let Some(c) = first_ns
{
cands.push((r, c));
}
let prev_blank = r > 0 && line_chars(r - 1).is_empty();
if r > 0 && prev_blank != blank {
cands.push((r, 0));
}
if r == 0 {
cands.push((0, 0));
}
for (row, col) in cands {
if r < cr || col < cc {
return Some((row, col));
}
}
origin = if r > 0 {
if row_skippable(&line_chars, r - 1) {
origin
} else if r >= 2 {
closest_stopper_below(&line_chars, r - 2)
} else {
None
}
} else {
None
};
}
None
}
}
fn first_sentence_boundary_forward<F: Fn(usize) -> Vec<char>>(
line_chars: &F,
cr: usize,
cc: usize,
n_lines: usize,
) -> Option<(usize, usize)> {
let mut stopper_eol = if cr == 0 {
false
} else {
closest_stopper_below(line_chars, cr - 1).is_some_and(|(_, eol)| eol)
};
let mut prev_blank = cr > 0 && line_chars(cr - 1).is_empty();
for r in cr..n_lines {
let blank = line_chars(r).is_empty();
let first_ns = line_chars(r).iter().position(|&c| !c.is_whitespace());
let mut cands: Vec<(usize, usize)> = Vec::new();
if r > 0 && prev_blank != blank {
cands.push((r, 0));
}
if stopper_eol && let Some(c) = first_ns {
cands.push((r, c));
}
let (mid, has_eol) = scan_row_boundaries(r, line_chars);
cands.extend(mid);
for (row, col) in cands {
if r > cr || col > cc {
return Some((row, col));
}
}
prev_blank = blank;
stopper_eol = if first_ns.is_some() || blank {
has_eol
} else {
stopper_eol
};
}
None
}
fn row_skippable<F: Fn(usize) -> Vec<char>>(line_chars: &F, r: usize) -> bool {
let lc = line_chars(r);
!lc.is_empty() && lc.iter().all(|&c| c.is_whitespace())
}
fn closest_stopper_below<F: Fn(usize) -> Vec<char>>(
line_chars: &F,
mut p: usize,
) -> Option<(usize, bool)> {
while p > 0 && row_skippable(line_chars, p) {
p -= 1;
}
(!row_skippable(line_chars, p)).then(|| (p, row_has_eol_walk(line_chars, p)))
}
fn row_has_eol_walk<F: Fn(usize) -> Vec<char>>(line_chars: &F, r: usize) -> bool {
scan_row_boundaries(r, line_chars).1
}
fn scan_row_boundaries<F: Fn(usize) -> Vec<char>>(
r: usize,
line_chars: &F,
) -> (Vec<(usize, usize)>, bool) {
let lc = line_chars(r);
let mut mid: Vec<(usize, usize)> = Vec::new();
let mut has_eol = false;
let mut i = 0;
while i < lc.len() {
if is_sentence_terminator(lc[i]) {
let mut j = i;
while j + 1 < lc.len() && is_sentence_terminator(lc[j + 1]) {
j += 1;
}
let mut k = j;
while k + 1 < lc.len() && is_sentence_closing(lc[k + 1]) {
k += 1;
}
if k + 1 < lc.len() {
if lc[k + 1].is_whitespace() {
let mut c = k + 1;
while c < lc.len() && lc[c].is_whitespace() {
c += 1;
}
if c < lc.len() {
mid.push((r, c));
} else {
has_eol = true;
}
}
i = k + 1;
} else {
has_eol = true;
break;
}
} else {
i += 1;
}
}
(mid, has_eol)
}
#[cfg_attr(not(test), allow(dead_code))]
fn sentence_boundaries(lines: &[Vec<char>], n_lines: usize) -> Vec<(usize, usize)> {
let mut out = vec![(0usize, 0usize)];
for (row, line) in lines.iter().enumerate().take(n_lines) {
let mut i = 0;
while i < line.len() {
if is_sentence_terminator(line[i]) {
let mut j = i;
while j + 1 < line.len() && is_sentence_terminator(line[j + 1]) {
j += 1;
}
let mut k = j;
while k + 1 < line.len() && is_sentence_closing(line[k + 1]) {
k += 1;
}
if k + 1 < line.len() {
if line[k + 1].is_whitespace()
&& let Some(p) = skip_sentence_ws(lines, n_lines, row, k + 1)
{
out.push(p);
}
i = k + 1;
continue;
}
if let Some(p) = skip_sentence_ws(lines, n_lines, row, line.len()) {
out.push(p);
}
break;
}
i += 1;
}
if row + 1 < n_lines {
let now_blank = lines[row].is_empty();
let next_blank = lines[row + 1].is_empty();
if now_blank != next_blank {
out.push((row + 1, 0));
}
}
}
out.sort_unstable();
out.dedup();
out
}
#[cfg_attr(not(test), allow(dead_code))]
fn skip_sentence_ws(
lines: &[Vec<char>],
n_lines: usize,
mut row: usize,
mut col: usize,
) -> Option<(usize, usize)> {
loop {
if col < lines[row].len() {
if lines[row][col].is_whitespace() {
col += 1;
continue;
}
return Some((row, col));
}
if row + 1 >= n_lines {
return None;
}
let was_blank = lines[row].is_empty();
row += 1;
col = 0;
let now_blank = lines[row].is_empty();
if now_blank != was_blank {
return Some((row, 0));
}
}
}
#[cfg_attr(not(test), allow(dead_code))]
fn end_of_buffer_pos(lines: &[Vec<char>], n_lines: usize) -> (usize, usize) {
let last = n_lines - 1;
let col = lines[last].len().saturating_sub(1);
(last, col)
}
#[derive(Debug, PartialEq, Eq)]
pub enum SentenceStep {
Boundary((usize, usize)),
EndOfBuffer((usize, usize)),
AtEnd,
}
pub fn sentence_step_forward<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
) -> SentenceStep {
let rope = hjkl_engine::types::Query::rope(ed.buffer());
let raw_n_lines = rope.len_lines();
if raw_n_lines == 0 {
return SentenceStep::AtEnd;
}
let line_of = |r: usize| -> std::borrow::Cow<'_, str> {
let start = rope.line_to_byte(r);
rope.byte_slice(start..start + hjkl_buffer::rope_line_bytes(&rope, r))
.into()
};
let line_chars = |r: usize| -> Vec<char> { line_of(r).chars().collect() };
let n_lines = if raw_n_lines > 1 && line_chars(raw_n_lines - 1).is_empty() {
raw_n_lines - 1
} else {
raw_n_lines
};
if n_lines == 0 {
return SentenceStep::AtEnd;
}
let cursor = ed.cursor();
let cursor = (cursor.0.min(n_lines - 1), cursor.1);
if let Some(p) = first_sentence_boundary_forward(&line_chars, cursor.0, cursor.1, n_lines) {
return SentenceStep::Boundary(p);
}
let last_row = line_chars(n_lines - 1);
let end = (n_lines - 1, last_row.len().saturating_sub(1));
if end <= cursor {
return SentenceStep::AtEnd;
}
let mut tail: &[char] = &last_row;
while tail.last().is_some_and(|c| c.is_whitespace()) {
tail = &tail[..tail.len() - 1];
}
while tail.last().is_some_and(|c| is_sentence_closing(*c)) {
tail = &tail[..tail.len() - 1];
}
if tail.last().is_some_and(|c| is_sentence_terminator(*c)) {
SentenceStep::Boundary(end)
} else {
SentenceStep::EndOfBuffer(end)
}
}
pub fn sentence_text_object<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
inner: bool,
count: usize,
) -> Option<((usize, usize), (usize, usize))> {
let count = count.max(1);
let rope = hjkl_engine::types::Query::rope(ed.buffer());
let raw_n_lines = rope.len_lines();
if raw_n_lines == 0 {
return None;
}
let cursor = ed.cursor();
let win_lo = cursor.0.saturating_sub(SENTENCE_WINDOW_ROWS);
let win_hi = (cursor.0 + SENTENCE_WINDOW_ROWS).min(raw_n_lines - 1);
let line_of = |r: usize| -> std::borrow::Cow<'_, str> {
let start = rope.line_to_byte(r);
rope.byte_slice(start..start + hjkl_buffer::rope_line_bytes(&rope, r))
.into()
};
let line_chars = |r: usize| -> Vec<char> { line_of(r).chars().collect() };
let line_len = |r: usize| -> usize { line_of(r).chars().count() };
let win_off = rope.line_to_char(win_lo);
let (win_lens, chars, last_content) =
window_flat(raw_n_lines, win_lo, win_hi, &line_len, &line_chars);
let flat_len = chars.len();
let whole_buffer = win_lo == 0 && win_hi >= last_content;
if flat_len == 0 {
return if whole_buffer {
None
} else {
sentence_text_object_full(ed, inner, count)
};
}
let idx_to_pos = |mut idx: usize| -> (usize, usize) {
for (i, &len) in win_lens.iter().enumerate() {
if idx <= len {
return (win_lo + i, idx);
}
idx -= len + 1;
}
let last = win_lens.len() - 1;
(win_lo + last, win_lens[last])
};
let cursor_idx = (rope.line_to_char(cursor.0) + cursor.1 - win_off).min(flat_len - 1);
let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
let mut clipped = false;
let mut start = cursor_idx;
while start > 0 {
let prev = chars[start - 1];
if prev.is_whitespace() {
let mut k = start - 1;
while k > 0 && chars[k - 1].is_whitespace() {
k -= 1;
}
if k > 0 && is_terminator(chars[k - 1]) {
break;
}
}
start -= 1;
}
if start == 0 && win_lo > 0 {
clipped = true;
}
while start < flat_len && chars[start].is_whitespace() {
start += 1;
}
if start >= flat_len {
return if whole_buffer {
None
} else {
sentence_text_object_full(ed, inner, count)
};
}
if clipped {
return sentence_text_object_full(ed, inner, count);
}
let mut end = start;
while end < flat_len {
if is_terminator(chars[end]) {
while end + 1 < flat_len && is_terminator(chars[end + 1]) {
end += 1;
}
if end + 1 >= flat_len || chars[end + 1].is_whitespace() {
break;
}
}
end += 1;
}
if end == flat_len && win_hi < last_content {
return sentence_text_object_full(ed, inner, count);
}
let mut rem = count - 1;
while rem > 0 {
let mut s = end + 1;
while s < flat_len && chars[s].is_whitespace() {
s += 1;
}
if s >= flat_len {
if win_hi < last_content {
return sentence_text_object_full(ed, inner, count);
}
break;
}
let mut e = s;
while e < flat_len {
if is_terminator(chars[e]) {
while e + 1 < flat_len && is_terminator(chars[e + 1]) {
e += 1;
}
if e + 1 >= flat_len || chars[e + 1].is_whitespace() {
break;
}
}
e += 1;
}
if e == flat_len && win_hi < last_content {
return sentence_text_object_full(ed, inner, count);
}
end = e;
rem -= 1;
}
let end_idx = (end + 1).min(flat_len);
let final_end = if inner {
end_idx
} else {
let mut e = end_idx;
while e < flat_len && chars[e].is_whitespace() && chars[e] != '\n' {
e += 1;
}
e
};
Some((idx_to_pos(start), idx_to_pos(final_end)))
}
fn sentence_text_object_full<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
inner: bool,
count: usize,
) -> Option<((usize, usize), (usize, usize))> {
let count = count.max(1);
let rope = hjkl_engine::types::Query::rope(ed.buffer());
let n_lines = rope.len_lines();
if n_lines == 0 {
return None;
}
let line_lens: Vec<usize> = (0..n_lines)
.map(|r| rope_line_to_str(&rope, r).chars().count())
.collect();
let pos_to_idx = |pos: (usize, usize)| -> usize {
let idx: usize = line_lens.iter().take(pos.0).map(|&len| len + 1).sum();
idx + pos.1
};
let idx_to_pos = |mut idx: usize| -> (usize, usize) {
for (r, &len) in line_lens.iter().enumerate() {
if idx <= len {
return (r, idx);
}
idx -= len + 1;
}
let last = n_lines.saturating_sub(1);
(last, line_lens[last])
};
let mut chars: Vec<char> = rope.chars().collect();
if chars.last() == Some(&'\n') {
chars.pop();
}
if chars.is_empty() {
return None;
}
let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
let mut start = cursor_idx;
while start > 0 {
let prev = chars[start - 1];
if prev.is_whitespace() {
let mut k = start - 1;
while k > 0 && chars[k - 1].is_whitespace() {
k -= 1;
}
if k > 0 && is_terminator(chars[k - 1]) {
break;
}
}
start -= 1;
}
while start < chars.len() && chars[start].is_whitespace() {
start += 1;
}
if start >= chars.len() {
return None;
}
let mut end = start;
while end < chars.len() {
if is_terminator(chars[end]) {
while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
end += 1;
}
if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
break;
}
}
end += 1;
}
let mut rem = count - 1;
while rem > 0 {
let mut s = end + 1;
while s < chars.len() && chars[s].is_whitespace() {
s += 1;
}
if s >= chars.len() {
break;
}
let mut e = s;
while e < chars.len() {
if is_terminator(chars[e]) {
while e + 1 < chars.len() && is_terminator(chars[e + 1]) {
e += 1;
}
if e + 1 >= chars.len() || chars[e + 1].is_whitespace() {
break;
}
}
e += 1;
}
end = e;
rem -= 1;
}
let end_idx = (end + 1).min(chars.len());
let final_end = if inner {
end_idx
} else {
let mut e = end_idx;
while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
e += 1;
}
e
};
Some((idx_to_pos(start), idx_to_pos(final_end)))
}
pub fn tag_text_object<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
inner: bool,
) -> Option<((usize, usize), (usize, usize))> {
let rope = hjkl_engine::types::Query::rope(ed.buffer());
let raw_n_lines = rope.len_lines();
if raw_n_lines == 0 {
return None;
}
let cursor = ed.cursor();
let win_lo = cursor.0.saturating_sub(TAG_WINDOW_ROWS);
let win_hi = (cursor.0 + TAG_WINDOW_ROWS).min(raw_n_lines - 1);
let line_of = |r: usize| -> std::borrow::Cow<'_, str> {
let start = rope.line_to_byte(r);
rope.byte_slice(start..start + hjkl_buffer::rope_line_bytes(&rope, r))
.into()
};
let line_chars = |r: usize| -> Vec<char> { line_of(r).chars().collect() };
let line_len = |r: usize| -> usize { line_of(r).chars().count() };
let win_off = rope.line_to_char(win_lo);
let (win_lens, chars, last_content) =
window_flat(raw_n_lines, win_lo, win_hi, &line_len, &line_chars);
let flat_len = chars.len();
let whole_buffer = win_lo == 0 && win_hi >= last_content;
if flat_len == 0 {
return if whole_buffer {
None
} else {
tag_text_object_full(ed, inner)
};
}
let idx_to_pos = |mut idx: usize| -> (usize, usize) {
for (i, &len) in win_lens.iter().enumerate() {
if idx <= len {
return (win_lo + i, idx);
}
idx -= len + 1;
}
let last = win_lens.len() - 1;
(win_lo + last, win_lens[last])
};
let cursor_idx = rope.line_to_char(cursor.0) + cursor.1 - win_off;
let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
let mut next_after: Option<(usize, usize, usize, usize)> = None;
let mut i = 0;
while i < flat_len {
if chars[i] != '<' {
i += 1;
continue;
}
let mut j = i + 1;
while j < flat_len && chars[j] != '>' {
j += 1;
}
if j >= flat_len {
break;
}
let inside: String = chars[i + 1..j].iter().collect();
let close_end = j + 1;
let trimmed = inside.trim();
if trimmed.starts_with('!') || trimmed.starts_with('?') {
i = close_end;
continue;
}
if let Some(rest) = trimmed.strip_prefix('/') {
let name = rest.split_whitespace().next().unwrap_or("").to_string();
if !name.is_empty()
&& let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
{
let (open_start, content_start, _) = stack[stack_idx].clone();
stack.truncate(stack_idx);
let content_end = i;
let candidate = (open_start, content_start, content_end, close_end);
if cursor_idx >= open_start && cursor_idx < close_end {
innermost = match innermost {
Some((os, _, _, ce)) if os <= open_start && close_end <= ce => {
Some(candidate)
}
None => Some(candidate),
existing => existing,
};
} else if open_start >= cursor_idx && next_after.is_none() {
next_after = Some(candidate);
}
}
} else if !trimmed.ends_with('/') {
let name: String = trimmed
.split(|c: char| c.is_whitespace() || c == '/')
.next()
.unwrap_or("")
.to_string();
if !name.is_empty() {
stack.push((i, close_end, name));
}
}
i = close_end;
}
let Some((open_start, content_start, content_end, close_end)) = innermost.or(next_after) else {
return tag_text_object_full(ed, inner);
};
let open_row = idx_to_pos(open_start).0;
let close_row = idx_to_pos(close_end).0;
if (open_row == win_lo && win_lo > 0) || (close_row >= win_hi && win_hi < raw_n_lines - 1) {
return tag_text_object_full(ed, inner);
}
if inner {
Some((idx_to_pos(content_start), idx_to_pos(content_end)))
} else {
Some((idx_to_pos(open_start), idx_to_pos(close_end)))
}
}
fn tag_text_object_full<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
inner: bool,
) -> Option<((usize, usize), (usize, usize))> {
let rope = hjkl_engine::types::Query::rope(ed.buffer());
let n_lines = rope.len_lines();
if n_lines == 0 {
return None;
}
let line_lens: Vec<usize> = (0..n_lines)
.map(|r| rope_line_to_str(&rope, r).chars().count())
.collect();
let pos_to_idx = |pos: (usize, usize)| -> usize {
let idx: usize = line_lens.iter().take(pos.0).map(|&len| len + 1).sum();
idx + pos.1
};
let idx_to_pos = |mut idx: usize| -> (usize, usize) {
for (r, &len) in line_lens.iter().enumerate() {
if idx <= len {
return (r, idx);
}
idx -= len + 1;
}
let last = n_lines.saturating_sub(1);
(last, line_lens[last])
};
let mut chars: Vec<char> = rope.chars().collect();
if chars.last() == Some(&'\n') {
chars.pop();
}
let cursor_idx = pos_to_idx(ed.cursor());
let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
let mut next_after: Option<(usize, usize, usize, usize)> = None;
let mut i = 0;
while i < chars.len() {
if chars[i] != '<' {
i += 1;
continue;
}
let mut j = i + 1;
while j < chars.len() && chars[j] != '>' {
j += 1;
}
if j >= chars.len() {
break;
}
let inside: String = chars[i + 1..j].iter().collect();
let close_end = j + 1;
let trimmed = inside.trim();
if trimmed.starts_with('!') || trimmed.starts_with('?') {
i = close_end;
continue;
}
if let Some(rest) = trimmed.strip_prefix('/') {
let name = rest.split_whitespace().next().unwrap_or("").to_string();
if !name.is_empty()
&& let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
{
let (open_start, content_start, _) = stack[stack_idx].clone();
stack.truncate(stack_idx);
let content_end = i;
let candidate = (open_start, content_start, content_end, close_end);
if cursor_idx >= open_start && cursor_idx < close_end {
innermost = match innermost {
Some((os, _, _, ce)) if os <= open_start && close_end <= ce => {
Some(candidate)
}
None => Some(candidate),
existing => existing,
};
} else if open_start >= cursor_idx && next_after.is_none() {
next_after = Some(candidate);
}
}
} else if !trimmed.ends_with('/') {
let name: String = trimmed
.split(|c: char| c.is_whitespace() || c == '/')
.next()
.unwrap_or("")
.to_string();
if !name.is_empty() {
stack.push((i, close_end, name));
}
}
i = close_end;
}
let (open_start, content_start, content_end, close_end) = innermost.or(next_after)?;
if inner {
Some((idx_to_pos(content_start), idx_to_pos(content_end)))
} else {
Some((idx_to_pos(open_start), idx_to_pos(close_end)))
}
}
const SENTENCE_WINDOW_ROWS: usize = 200;
const TAG_WINDOW_ROWS: usize = 50;
fn window_flat<F: Fn(usize) -> usize, G: Fn(usize) -> Vec<char>>(
raw_n_lines: usize,
win_lo: usize,
win_hi: usize,
line_len: &F,
line_chars: &G,
) -> (Vec<usize>, Vec<char>, usize) {
let last_content = if raw_n_lines > 1 && line_len(raw_n_lines - 1) == 0 {
raw_n_lines - 2
} else {
raw_n_lines - 1
};
let hi = win_hi.min(last_content);
let mut win_lens: Vec<usize> = (win_lo..=hi).map(line_len).collect();
let mut flat: Vec<char> = Vec::new();
for r in win_lo..=hi {
flat.extend(line_chars(r));
if r < hi {
flat.push('\n');
}
}
if hi < last_content {
flat.push('\n');
win_lens.push(line_len(hi + 1));
}
(win_lens, flat, last_content)
}
pub fn is_wordchar(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
pub use hjkl_buffer::is_keyword_char;
pub fn abbrev_kind(lhs: &str, iskeyword: &str) -> AbbrevKind {
let chars: Vec<char> = lhs.chars().collect();
if chars.is_empty() {
return AbbrevKind::NonKw;
}
let last = *chars.last().unwrap();
let last_is_kw = is_keyword_char(last, iskeyword);
if !last_is_kw {
return AbbrevKind::NonKw;
}
let all_kw = chars.iter().all(|&c| is_keyword_char(c, iskeyword));
if all_kw {
AbbrevKind::Full
} else {
AbbrevKind::End
}
}
pub fn try_abbrev_expand(
abbrevs: &[Abbrev],
line_before: &str,
mincol: usize,
trigger: AbbrevTrigger,
iskeyword: &str,
) -> Option<(usize, String)> {
let chars: Vec<char> = line_before.chars().collect();
let cursor_col = chars.len();
for abbrev in abbrevs {
if !abbrev.insert {
continue;
}
let lhs_chars: Vec<char> = abbrev.lhs.chars().collect();
if lhs_chars.is_empty() {
continue;
}
let lhs_len = lhs_chars.len();
let kind = abbrev_kind(&abbrev.lhs, iskeyword);
match kind {
AbbrevKind::Full | AbbrevKind::End => {
let trigger_char_is_kw = match trigger {
AbbrevTrigger::NonKeyword(c) => is_keyword_char(c, iskeyword),
AbbrevTrigger::CtrlBracket | AbbrevTrigger::Cr | AbbrevTrigger::Esc => false,
};
if trigger_char_is_kw {
continue;
}
}
AbbrevKind::NonKw => {
match trigger {
AbbrevTrigger::Cr | AbbrevTrigger::Esc | AbbrevTrigger::CtrlBracket => {}
AbbrevTrigger::NonKeyword(_) => continue,
}
}
}
if cursor_col < lhs_len {
continue;
}
let lhs_start_col = cursor_col - lhs_len;
if lhs_start_col < mincol {
continue;
}
let text_slice: &[char] = &chars[lhs_start_col..cursor_col];
if text_slice != lhs_chars.as_slice() {
continue;
}
if lhs_start_col > 0 {
let ch_before = chars[lhs_start_col - 1];
match kind {
AbbrevKind::Full => {
if is_keyword_char(ch_before, iskeyword) {
continue; }
if lhs_len == 1 && ch_before != ' ' && ch_before != '\t' {
continue;
}
}
AbbrevKind::End => {
}
AbbrevKind::NonKw => {
if ch_before != ' ' && ch_before != '\t' {
continue;
}
}
}
}
return Some((lhs_len, abbrev.rhs.clone()));
}
None
}
pub fn check_and_apply_abbrev<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
trigger: AbbrevTrigger,
) -> bool {
use hjkl_buffer::{Edit, Position};
let cursor = buf_cursor_pos(ed.buffer());
let row = cursor.row;
let col = cursor.col;
let line_before: String = {
let line = buf_line(ed.buffer(), row).unwrap_or_default();
line.chars().take(col).collect()
};
let (mincol, on_start_row) = if let Some(ref s) = vim(ed).insert_session {
if row == s.start_row {
(s.start_col, true)
} else {
(0, false)
}
} else {
(0, false)
};
if on_start_row && col <= mincol {
return false;
}
let iskeyword = ed.settings().iskeyword.clone();
let abbrevs = ed.abbrevs();
let Some((lhs_len, rhs)) =
try_abbrev_expand(&abbrevs, &line_before, mincol, trigger, &iskeyword)
else {
return false;
};
let lhs_start = col.saturating_sub(lhs_len);
if lhs_len > 0 {
ed.mutate_edit(Edit::DeleteRange {
start: Position::new(row, lhs_start),
end: Position::new(row, col),
kind: hjkl_buffer::MotionKind::Char,
});
}
let insert_pos = Position::new(row, lhs_start);
if !rhs.is_empty() {
ed.mutate_edit(Edit::InsertStr {
at: insert_pos,
text: rhs.clone(),
});
}
let new_col = lhs_start + rhs.chars().count();
buf_set_cursor_rc(ed.buffer_mut(), row, new_col);
true
}
pub fn word_text_object<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
inner: bool,
big: bool,
count: usize,
) -> Option<((usize, usize), (usize, usize))> {
let count = count.max(1);
let (row, col) = ed.cursor();
let line = buf_line(ed.buffer(), row)?;
let chars: Vec<char> = line.chars().collect();
if chars.is_empty() {
return None;
}
let len = chars.len();
let at = col.min(len.saturating_sub(1));
let classify = |c: char| -> u8 {
if c.is_whitespace() {
0
} else if big || is_wordchar(c) {
1
} else {
2
}
};
let cls = classify(chars[at]);
let mut start = at;
while start > 0 && classify(chars[start - 1]) == cls {
start -= 1;
}
let mut end = at;
while end + 1 < len && classify(chars[end + 1]) == cls {
end += 1;
}
let mut start_col = start;
let end_col;
if inner {
let mut rem = count - 1;
while rem > 0 && end + 1 < len {
let next_kind = classify(chars[end + 1]);
end += 1;
while end + 1 < len && classify(chars[end + 1]) == next_kind {
end += 1;
}
rem -= 1;
}
end_col = end + 1;
} else if cls == 0 {
let mut e = end;
let mut rem = count;
while rem > 0 && e + 1 < len {
while e + 1 < len && chars[e + 1].is_whitespace() {
e += 1;
}
if e + 1 >= len {
break;
}
e += 1;
let k = classify(chars[e]);
while e + 1 < len && classify(chars[e + 1]) == k {
e += 1;
}
rem -= 1;
}
end_col = e + 1;
} else {
let mut e = end;
let mut words_done = 1;
let mut included_trailing = false;
loop {
let mut t = e + 1;
let mut got_ws = false;
while t < len && chars[t].is_whitespace() {
got_ws = true;
t += 1;
}
if words_done == count {
if got_ws {
e = t - 1;
included_trailing = true;
}
break;
}
if t >= len {
break; }
e = t;
let k = classify(chars[e]);
while e + 1 < len && classify(chars[e + 1]) == k {
e += 1;
}
words_done += 1;
}
end_col = e + 1;
if !included_trailing {
let mut s = start;
while s > 0 && chars[s - 1].is_whitespace() {
s -= 1;
}
start_col = s;
}
}
Some(((row, start_col), (row, end_col)))
}
pub fn quote_text_object<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
q: char,
inner: bool,
) -> Option<((usize, usize), (usize, usize))> {
let (row, col) = ed.cursor();
let line = buf_line(ed.buffer(), row)?;
let chars: Vec<char> = line.chars().collect();
let mut positions: Vec<usize> = Vec::new();
for (i, &c) in chars.iter().enumerate() {
if c == q {
positions.push(i);
}
}
if positions.len() < 2 {
return None;
}
let mut open_idx: Option<usize> = None;
let mut close_idx: Option<usize> = None;
for pair in positions.chunks(2) {
if pair.len() < 2 {
break;
}
if col >= pair[0] && col <= pair[1] {
open_idx = Some(pair[0]);
close_idx = Some(pair[1]);
break;
}
if col < pair[0] {
open_idx = Some(pair[0]);
close_idx = Some(pair[1]);
break;
}
}
let open = open_idx?;
let close = close_idx?;
if inner {
if close <= open + 1 {
return None;
}
Some(((row, open + 1), (row, close)))
} else {
let after_close = close + 1; if after_close < chars.len() && chars[after_close].is_ascii_whitespace() {
let mut end = after_close;
while end < chars.len() && chars[end].is_ascii_whitespace() {
end += 1;
}
Some(((row, open), (row, end)))
} else if open > 0 && chars[open - 1].is_ascii_whitespace() {
let mut start = open;
while start > 0 && chars[start - 1].is_ascii_whitespace() {
start -= 1;
}
Some(((row, start), (row, close + 1)))
} else {
Some(((row, open), (row, close + 1)))
}
}
}
pub fn bracket_text_object<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
open: char,
inner: bool,
count: usize,
) -> Option<(Pos, Pos, RangeKind)> {
let close = match open {
'(' => ')',
'[' => ']',
'{' => '}',
'<' => '>',
_ => return None,
};
let (row, col) = ed.cursor();
let lines = rope_to_lines_vec(&hjkl_engine::types::Query::rope(ed.buffer()));
let lines = lines.as_slice();
let cursor_char = lines.get(row).and_then(|l| l.chars().nth(col));
let (open_pos, close_pos) = if cursor_char == Some(close) {
let open_pos = if col > 0 {
find_open_bracket(lines, row, col - 1, open, close)
} else if row > 0 {
let pr = row - 1;
let pc = lines[pr].chars().count().saturating_sub(1);
find_open_bracket(lines, pr, pc, open, close)
} else {
None
}?;
(open_pos, (row, col))
} else {
let open_pos = find_open_bracket(lines, row, col, open, close)
.or_else(|| find_next_open(lines, row, col, open))?;
let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
(open_pos, close_pos)
};
let (open_pos, close_pos) = {
let (mut op, mut cp) = (open_pos, close_pos);
for _ in 1..count.max(1) {
let outer = if op.1 > 0 {
find_open_bracket(lines, op.0, op.1 - 1, open, close)
} else if op.0 > 0 {
let pr = op.0 - 1;
let pc = lines[pr].chars().count().saturating_sub(1);
find_open_bracket(lines, pr, pc, open, close)
} else {
None
};
let Some(oo) = outer else { break };
let Some(oc) = find_close_bracket(lines, oo.0, oo.1 + 1, open, close) else {
break;
};
op = oo;
cp = oc;
}
(op, cp)
};
if inner {
let open_line_len = lines[open_pos.0].chars().count();
let inner_start = if open_pos.1 + 1 >= open_line_len && open_pos.0 + 1 < lines.len() {
(open_pos.0 + 1, 0)
} else {
advance_pos(lines, open_pos)
};
if inner_start.0 > close_pos.0
|| (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
{
return Some((inner_start, inner_start, RangeKind::Exclusive));
}
if close_pos.0 > open_pos.0 {
let mut saw_ws = false;
let mut saw_other = false;
for r in inner_start.0..=close_pos.0 {
let line: Vec<char> = lines
.get(r)
.map(|l| l.chars().collect())
.unwrap_or_default();
let from = if r == inner_start.0 { inner_start.1 } else { 0 };
let to = if r == close_pos.0 {
close_pos.1
} else {
line.len()
};
for &c in line
.iter()
.take(to.min(line.len()))
.skip(from.min(line.len()))
{
if c == ' ' || c == '\t' {
saw_ws = true;
} else {
saw_other = true;
}
}
}
if saw_ws && !saw_other {
return Some((inner_start, inner_start, RangeKind::Exclusive));
}
}
Some((inner_start, close_pos, RangeKind::Exclusive))
} else {
Some((
open_pos,
advance_pos(lines, close_pos),
RangeKind::Exclusive,
))
}
}
pub fn find_open_bracket(
lines: &[String],
row: usize,
col: usize,
open: char,
close: char,
) -> Option<(usize, usize)> {
let mut depth: i32 = 0;
let mut r = row;
let mut c = col as isize;
loop {
let cur = &lines[r];
let chars: Vec<char> = cur.chars().collect();
if (c as usize) >= chars.len() {
c = chars.len() as isize - 1;
}
while c >= 0 {
let ch = chars[c as usize];
if ch == close {
depth += 1;
} else if ch == open {
if depth == 0 {
return Some((r, c as usize));
}
depth -= 1;
}
c -= 1;
}
if r == 0 {
return None;
}
r -= 1;
c = lines[r].chars().count() as isize - 1;
}
}
pub fn find_close_bracket(
lines: &[String],
row: usize,
start_col: usize,
open: char,
close: char,
) -> Option<(usize, usize)> {
let mut depth: i32 = 0;
let mut r = row;
let mut c = start_col;
loop {
let cur = &lines[r];
let chars: Vec<char> = cur.chars().collect();
while c < chars.len() {
let ch = chars[c];
if ch == open {
depth += 1;
} else if ch == close {
if depth == 0 {
return Some((r, c));
}
depth -= 1;
}
c += 1;
}
if r + 1 >= lines.len() {
return None;
}
r += 1;
c = 0;
}
}
pub fn find_next_open(
lines: &[String],
row: usize,
col: usize,
open: char,
) -> Option<(usize, usize)> {
let mut r = row;
let mut c = col;
while r < lines.len() {
let chars: Vec<char> = lines[r].chars().collect();
while c < chars.len() {
if chars[c] == open {
return Some((r, c));
}
c += 1;
}
r += 1;
c = 0;
}
None
}
pub fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
let (r, c) = pos;
let line_len = lines[r].chars().count();
if c < line_len {
(r, c + 1)
} else if r + 1 < lines.len() {
(r + 1, 0)
} else {
pos
}
}
pub fn paragraph_text_object<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
inner: bool,
count: usize,
) -> Option<((usize, usize), (usize, usize))> {
let count = count.max(1);
let (row, _) = ed.cursor();
let rope = hjkl_engine::types::Query::rope(ed.buffer());
let raw_n_lines = rope.len_lines();
if raw_n_lines == 0 {
return None;
}
let n_lines = if raw_n_lines > 1 && rope_line_to_str(&rope, raw_n_lines - 1).is_empty() {
raw_n_lines - 1
} else {
raw_n_lines
};
if n_lines == 0 {
return None;
}
let is_blank = |r: usize| -> bool {
if r >= n_lines {
return true;
}
rope_line_to_str(&rope, r).trim().is_empty()
};
let mut top = row;
let mut bot = row;
if is_blank(row) {
while top > 0 && is_blank(top - 1) {
top -= 1;
}
while bot + 1 < n_lines && is_blank(bot + 1) {
bot += 1;
}
if !inner {
if bot + 1 < n_lines {
bot += 1;
while bot + 1 < n_lines && !is_blank(bot + 1) {
bot += 1;
}
} else {
return None;
}
}
} else {
while top > 0 && !is_blank(top - 1) {
top -= 1;
}
while bot + 1 < n_lines && !is_blank(bot + 1) {
bot += 1;
}
if !inner && bot + 1 < n_lines && is_blank(bot + 1) {
bot += 1;
}
}
let mut rem = count - 1;
while rem > 0 && bot + 1 < n_lines {
if inner {
let blank_next = is_blank(bot + 1);
bot += 1;
while bot + 1 < n_lines && is_blank(bot + 1) == blank_next {
bot += 1;
}
} else {
while bot + 1 < n_lines && !is_blank(bot + 1) {
bot += 1;
}
while bot + 1 < n_lines && is_blank(bot + 1) {
bot += 1;
}
}
rem -= 1;
}
if !inner && bot + 1 >= n_lines && !is_blank(bot) {
while top > 0 && is_blank(top - 1) {
top -= 1;
}
}
let end_col = rope_line_to_str(&rope, bot).chars().count();
Some(((top, 0), (bot, end_col)))
}
#[cfg(test)]
mod tests {
use super::*;
use hjkl_buffer::View;
use hjkl_engine::{DefaultHost, Editor, Options};
fn make_editor(content: &str) -> Editor<View, DefaultHost> {
let buf = View::from_str(content);
let host = DefaultHost::new();
crate::vim::vim_editor(buf, host, Options::default())
}
#[test]
fn sentence_text_object_is_exact_range_mid_paragraph() {
let mut ed = make_editor("First sentence. Second one.\n\nThird.");
ed.set_cursor_quiet(0, 16); assert_eq!(
sentence_text_object(&ed, true, 1),
Some(((0, 16), (0, 27))),
"is must span exactly \"Second one.\" (exclusive end after the '.')"
);
assert_eq!(
sentence_text_object(&ed, false, 1),
Some(((0, 16), (0, 27)))
);
}
#[test]
fn sentence_boundary_matches_full_scan_across_paragraph_break() {
let mut ed = make_editor("First sentence. Second one.\n\nThird.");
ed.set_cursor_quiet(0, 0);
assert_eq!(sentence_boundary(&ed, true), Some((0, 16)));
ed.set_cursor_quiet(0, 16);
assert_eq!(sentence_boundary(&ed, true), Some((1, 0)));
assert_eq!(sentence_boundary(&ed, false), Some((0, 0)));
ed.set_cursor_quiet(0, 0);
assert_eq!(sentence_boundary(&ed, false), None);
}
#[test]
fn tag_text_object_exact_range_nested() {
let mut ed = make_editor("<div>\n <p>Hello</p>\n</div>");
ed.set_cursor_quiet(1, 5); assert_eq!(tag_text_object(&ed, true), Some(((1, 5), (1, 10))));
assert_eq!(tag_text_object(&ed, false), Some(((1, 2), (1, 14))));
}
#[test]
fn tag_text_object_edge_cases() {
let mut ed = make_editor("text <b>x</b>");
ed.set_cursor_quiet(0, 0);
assert_eq!(tag_text_object(&ed, true), Some(((0, 8), (0, 9))));
assert_eq!(tag_text_object(&ed, false), Some(((0, 5), (0, 13))));
let mut ed = make_editor("<a>\n body\n</a>");
ed.set_cursor_quiet(1, 3);
assert_eq!(tag_text_object(&ed, true), Some(((0, 3), (2, 0))));
assert_eq!(tag_text_object(&ed, false), Some(((0, 0), (2, 4))));
let ed = make_editor("</a>");
assert_eq!(tag_text_object(&ed, true), None);
}
fn old_sentence_boundary<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
forward: bool,
) -> Option<(usize, usize)> {
let rope = hjkl_engine::types::Query::rope(ed.buffer());
let raw_n_lines = rope.len_lines();
if raw_n_lines == 0 {
return None;
}
let lines: Vec<Vec<char>> = (0..raw_n_lines)
.map(|r| rope_line_to_str(&rope, r).chars().collect())
.collect();
let n_lines = if raw_n_lines > 1 && lines[raw_n_lines - 1].is_empty() {
raw_n_lines - 1
} else {
raw_n_lines
};
if n_lines == 0 {
return None;
}
let boundaries = sentence_boundaries(&lines, n_lines);
let cursor = ed.cursor();
let cursor = (cursor.0.min(n_lines - 1), cursor.1);
if forward {
if let Some(&p) = boundaries.iter().find(|&&p| p > cursor) {
return Some(p);
}
let end = end_of_buffer_pos(&lines, n_lines);
(end > cursor).then_some(end)
} else {
boundaries.into_iter().rfind(|&p| p < cursor)
}
}
fn old_sentence_step_forward<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
) -> SentenceStep {
let rope = hjkl_engine::types::Query::rope(ed.buffer());
let raw_n_lines = rope.len_lines();
if raw_n_lines == 0 {
return SentenceStep::AtEnd;
}
let lines: Vec<Vec<char>> = (0..raw_n_lines)
.map(|r| rope_line_to_str(&rope, r).chars().collect())
.collect();
let n_lines = if raw_n_lines > 1 && lines[raw_n_lines - 1].is_empty() {
raw_n_lines - 1
} else {
raw_n_lines
};
if n_lines == 0 {
return SentenceStep::AtEnd;
}
let cursor = ed.cursor();
let cursor = (cursor.0.min(n_lines - 1), cursor.1);
if let Some(&p) = sentence_boundaries(&lines, n_lines)
.iter()
.find(|&&p| p > cursor)
{
return SentenceStep::Boundary(p);
}
let end = end_of_buffer_pos(&lines, n_lines);
if end <= cursor {
return SentenceStep::AtEnd;
}
let mut tail = lines[n_lines - 1].as_slice();
while tail.last().is_some_and(|c| c.is_whitespace()) {
tail = &tail[..tail.len() - 1];
}
while tail.last().is_some_and(|c| is_sentence_closing(*c)) {
tail = &tail[..tail.len() - 1];
}
if tail.last().is_some_and(|c| is_sentence_terminator(*c)) {
SentenceStep::Boundary(end)
} else {
SentenceStep::EndOfBuffer(end)
}
}
fn old_sentence_text_object<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
inner: bool,
count: usize,
) -> Option<((usize, usize), (usize, usize))> {
let count = count.max(1);
let rope = hjkl_engine::types::Query::rope(ed.buffer());
let n_lines = rope.len_lines();
if n_lines == 0 {
return None;
}
let line_lens: Vec<usize> = (0..n_lines)
.map(|r| rope_line_to_str(&rope, r).chars().count())
.collect();
let pos_to_idx = |pos: (usize, usize)| -> usize {
let idx: usize = line_lens.iter().take(pos.0).map(|&len| len + 1).sum();
idx + pos.1
};
let idx_to_pos = |mut idx: usize| -> (usize, usize) {
for (r, &len) in line_lens.iter().enumerate() {
if idx <= len {
return (r, idx);
}
idx -= len + 1;
}
let last = n_lines.saturating_sub(1);
(last, line_lens[last])
};
let mut chars: Vec<char> = rope.chars().collect();
if chars.last() == Some(&'\n') {
chars.pop();
}
if chars.is_empty() {
return None;
}
let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
let mut start = cursor_idx;
while start > 0 {
let prev = chars[start - 1];
if prev.is_whitespace() {
let mut k = start - 1;
while k > 0 && chars[k - 1].is_whitespace() {
k -= 1;
}
if k > 0 && is_terminator(chars[k - 1]) {
break;
}
}
start -= 1;
}
while start < chars.len() && chars[start].is_whitespace() {
start += 1;
}
if start >= chars.len() {
return None;
}
let mut end = start;
while end < chars.len() {
if is_terminator(chars[end]) {
while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
end += 1;
}
if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
break;
}
}
end += 1;
}
let mut rem = count - 1;
while rem > 0 {
let mut s = end + 1;
while s < chars.len() && chars[s].is_whitespace() {
s += 1;
}
if s >= chars.len() {
break;
}
let mut e = s;
while e < chars.len() {
if is_terminator(chars[e]) {
while e + 1 < chars.len() && is_terminator(chars[e + 1]) {
e += 1;
}
if e + 1 >= chars.len() || chars[e + 1].is_whitespace() {
break;
}
}
e += 1;
}
end = e;
rem -= 1;
}
let end_idx = (end + 1).min(chars.len());
let final_end = if inner {
end_idx
} else {
let mut e = end_idx;
while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
e += 1;
}
e
};
Some((idx_to_pos(start), idx_to_pos(final_end)))
}
fn cursor_samples(content: &str) -> Vec<(usize, usize)> {
let mut out = Vec::new();
for (row, line) in content.split('\n').enumerate() {
let len = line.chars().count();
for col in 0..=len + 2 {
out.push((row, col));
}
}
out
}
const CORPUS: &[&str] = &[
"",
"\n",
"One.",
"One.\n",
"One. Two.",
"One. Two.\n",
"One. Two. Three!",
"One? Two!",
"One. Two.",
"One. \nTwo.",
"One.\nTwo.",
"One.\n\nTwo.",
"One.\n\n\nTwo.",
"One. \n Two.",
"One. \n \n Two.",
"One.) Two.",
"One.\" Two.",
"One.'] Two.",
"Hello world",
"Hello world\n",
" One. Two. ",
"One. Two",
"Really?! Yes.",
"A.\nB.\nC.",
"First sentence. Second one.\n\nThird.",
"\n\n",
"One.\n\n",
];
fn corpus_buffers() -> Vec<String> {
let mut bufs: Vec<String> = CORPUS.iter().map(|s| (*s).to_string()).collect();
bufs.push("x\n".repeat(300) + "One. Two.\n" + &"y\n".repeat(300));
bufs
}
#[test]
fn sentence_boundary_matches_full_scan_on_corpus() {
for buf in corpus_buffers() {
let mut ed = make_editor(&buf);
for (row, col) in cursor_samples(&buf) {
ed.set_cursor_quiet(row, col);
for forward in [true, false] {
assert_eq!(
sentence_boundary(&ed, forward),
old_sentence_boundary(&ed, forward),
"buffer {:?} cursor ({row},{col}) forward={forward}",
buf
);
}
}
}
}
#[test]
fn sentence_text_object_matches_full_scan_on_corpus() {
for buf in corpus_buffers() {
let mut ed = make_editor(&buf);
for (row, col) in cursor_samples(&buf) {
ed.set_cursor_quiet(row, col);
for inner in [true, false] {
assert_eq!(
sentence_text_object(&ed, inner, 1),
old_sentence_text_object(&ed, inner, 1),
"buffer {:?} cursor ({row},{col}) inner={inner}",
buf
);
}
}
}
}
#[test]
fn sentence_step_forward_matches_full_scan_on_corpus() {
for buf in corpus_buffers() {
let mut ed = make_editor(&buf);
for (row, col) in cursor_samples(&buf) {
ed.set_cursor_quiet(row, col);
assert_eq!(
sentence_step_forward(&ed),
old_sentence_step_forward(&ed),
"buffer {:?} cursor ({row},{col})",
buf
);
}
}
}
}