use super::{Buffer, CharClass, Cursor, classify, is_blank_line};
use crate::action::{Object, Scope};
impl Buffer {
pub fn text_object_range(&self, scope: Scope, object: Object) -> Option<(Cursor, Cursor)> {
if let Some(target) = ts_target(object, scope) {
return self.text_object_range_ts(target);
}
if matches!(object, Object::Word) {
return self.word_object_range(scope);
}
if matches!(object, Object::Paragraph) {
return Some(self.paragraph_object_range(scope));
}
let row = self.cursor.row;
let col = self.cursor.col;
let chars: Vec<char> = self.lines[row].chars().collect();
let (open, close) = delim(object);
let (start_col, end_col) = if open == close {
let left = (0..=col).rev().find(|&i| chars.get(i) == Some(&open))?;
let right = ((left + 1)..chars.len()).find(|&i| chars[i] == open)?;
(left, right)
} else {
let left = find_open_left(&chars, col, open, close)?;
let right = find_close_right(&chars, left, open, close)?;
(left, right)
};
let (from_col, to_col) = match scope {
Scope::Inner => (start_col + 1, end_col),
Scope::Around => (start_col, end_col + 1),
};
Some((Cursor { row, col: from_col }, Cursor { row, col: to_col }))
}
fn text_object_range_ts(&self, target: &str) -> Option<(Cursor, Cursor)> {
let h = self.highlighter.as_ref()?;
let (sr, sc, er, ec) = h.find_text_object(target, self.cursor.row, self.cursor.col)?;
Some((Cursor { row: sr, col: sc }, Cursor { row: er, col: ec }))
}
fn word_object_range(&self, scope: Scope) -> Option<(Cursor, Cursor)> {
let row = self.cursor.row;
let chars: Vec<char> = self.lines[row].chars().collect();
if chars.is_empty() {
return None;
}
let col = self.cursor.col.min(chars.len() - 1);
let class = classify(chars[col]);
let mut start = col;
while start > 0 && classify(chars[start - 1]) == class {
start -= 1;
}
let mut end = col;
while end < chars.len() && classify(chars[end]) == class {
end += 1;
}
let (from_col, to_col) = match scope {
Scope::Inner => (start, end),
Scope::Around => {
let mut e = end;
while e < chars.len() && classify(chars[e]) == CharClass::Space {
e += 1;
}
if e != end {
(start, e)
} else {
let mut s = start;
while s > 0 && classify(chars[s - 1]) == CharClass::Space {
s -= 1;
}
(s, end)
}
}
};
Some((Cursor { row, col: from_col }, Cursor { row, col: to_col }))
}
fn paragraph_object_range(&self, scope: Scope) -> (Cursor, Cursor) {
let row = self.cursor.row;
let target_blank = is_blank_line(&self.lines[row]);
let mut start = row;
while start > 0 && is_blank_line(&self.lines[start - 1]) == target_blank {
start -= 1;
}
let mut end = row;
while end + 1 < self.lines.len() && is_blank_line(&self.lines[end + 1]) == target_blank {
end += 1;
}
let (s, e) = match scope {
Scope::Inner => (start, end),
Scope::Around => {
let mut ae = end;
while ae + 1 < self.lines.len()
&& is_blank_line(&self.lines[ae + 1]) != target_blank
{
ae += 1;
}
if ae != end {
(start, ae)
} else {
let mut as_ = start;
while as_ > 0 && is_blank_line(&self.lines[as_ - 1]) != target_blank {
as_ -= 1;
}
(as_, end)
}
}
};
range_for_full_lines(&self.lines, s, e)
}
}
fn delim(object: Object) -> (char, char) {
match object {
Object::DoubleQuote => ('"', '"'),
Object::SingleQuote => ('\'', '\''),
Object::Paren => ('(', ')'),
Object::Brace => ('{', '}'),
Object::Bracket => ('[', ']'),
Object::Word => ('\0', '\0'),
Object::Function | Object::Class | Object::Parameter => ('\0', '\0'),
Object::Paragraph => ('\0', '\0'),
}
}
fn range_for_full_lines(lines: &[String], start_row: usize, end_row: usize) -> (Cursor, Cursor) {
let from = Cursor {
row: start_row,
col: 0,
};
let to = if end_row + 1 < lines.len() {
Cursor {
row: end_row + 1,
col: 0,
}
} else {
Cursor {
row: end_row,
col: lines[end_row].chars().count(),
}
};
(from, to)
}
fn ts_target(object: Object, scope: Scope) -> Option<&'static str> {
Some(match (object, scope) {
(Object::Function, Scope::Inner) => "function.inner",
(Object::Function, Scope::Around) => "function.outer",
(Object::Class, Scope::Inner) => "class.inner",
(Object::Class, Scope::Around) => "class.outer",
(Object::Parameter, Scope::Inner) => "parameter.inner",
(Object::Parameter, Scope::Around) => "parameter.outer",
_ => return None,
})
}
fn find_open_left(chars: &[char], from: usize, open: char, close: char) -> Option<usize> {
let mut depth: i32 = 0;
let mut i = from;
loop {
let c = chars[i];
if c == close {
depth += 1;
} else if c == open {
if depth == 0 {
return Some(i);
}
depth -= 1;
}
if i == 0 {
return None;
}
i -= 1;
}
}
fn find_close_right(chars: &[char], open_pos: usize, open: char, close: char) -> Option<usize> {
let mut depth: i32 = 0;
for (i, &c) in chars.iter().enumerate().skip(open_pos + 1) {
if c == open {
depth += 1;
} else if c == close {
if depth == 0 {
return Some(i);
}
depth -= 1;
}
}
None
}