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(crate) type Pos = (usize, usize);
pub(crate) 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(crate) 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 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 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
}
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));
}
}
}
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)
}
pub(crate) 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 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(crate) 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 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)))
}
}
pub(crate) fn is_wordchar(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
pub(crate) use hjkl_buffer::is_keyword_char;
pub(crate) 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(crate) 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(crate) 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(crate) 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(crate) 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(crate) 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(crate) 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(crate) 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(crate) 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(crate) 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(crate) 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)))
}