Work on implementing more text objects

This commit is contained in:
2025-06-12 03:15:52 -04:00
parent 2295669382
commit 4372cf3820
5 changed files with 606 additions and 93 deletions

View File

@@ -6,6 +6,20 @@ use unicode_width::UnicodeWidthStr;
use super::vicmd::{Anchor, Bound, CmdFlags, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb, ViCmd, Word};
use crate::{libsh::{error::ShResult, term::{Style, Styled}}, prelude::*};
const PUNCTUATION: [&str;3] = [
"?",
"!",
"."
];
#[derive(PartialEq,Eq,Debug,Clone,Copy)]
pub enum Delim {
Paren,
Brace,
Bracket,
Angle
}
#[derive(Default,PartialEq,Eq,Debug,Clone,Copy)]
pub enum CharClass {
#[default]
@@ -385,6 +399,36 @@ impl LineBuf {
pub fn grapheme_indices_owned(&self) -> Vec<usize> {
self.grapheme_indices.as_ref().cloned().unwrap_or_default()
}
pub fn grapheme_is_escaped(&mut self, pos: usize) -> bool {
let mut pos = ClampedUsize::new(pos, self.cursor.max, false);
let mut escaped = false;
while pos.dec() {
let Some(gr) = self.grapheme_at(pos.get()) else { return escaped };
if gr == "\\" {
escaped = !escaped;
} else {
return escaped
}
}
escaped
}
/// Does not update graphemes
/// Useful in cases where you have to check many graphemes at once
/// And don't want to trigger any mutable borrowing issues
pub fn read_grapheme_at(&self, pos: usize) -> Option<&str> {
let indices = self.grapheme_indices();
let start = indices.get(pos).copied()?;
let end = indices.get(pos + 1).copied().or_else(|| {
if pos + 1 == self.grapheme_indices().len() {
Some(self.buffer.len())
} else {
None
}
})?;
self.buffer.get(start..end)
}
pub fn grapheme_at(&mut self, pos: usize) -> Option<&str> {
self.update_graphemes_lazy();
let indices = self.grapheme_indices();
@@ -398,6 +442,34 @@ impl LineBuf {
})?;
self.buffer.get(start..end)
}
pub fn read_grapheme_before(&self, pos: usize) -> Option<&str> {
if pos == 0 {
return None
}
let pos = ClampedUsize::new(pos, self.cursor.max, false);
self.read_grapheme_at(pos.ret_sub(1))
}
pub fn grapheme_before(&mut self, pos: usize) -> Option<&str> {
if pos == 0 {
return None
}
let pos = ClampedUsize::new(pos, self.cursor.max, false);
self.grapheme_at(pos.ret_sub(1))
}
pub fn read_grapheme_after(&self, pos: usize) -> Option<&str> {
if pos == self.cursor.max {
return None
}
let pos = ClampedUsize::new(pos, self.cursor.max, false);
self.read_grapheme_at(pos.ret_add(1))
}
pub fn grapheme_after(&mut self, pos: usize) -> Option<&str> {
if pos == self.cursor.max {
return None
}
let pos = ClampedUsize::new(pos, self.cursor.max, false);
self.grapheme_at(pos.ret_add(1))
}
pub fn grapheme_at_cursor(&mut self) -> Option<&str> {
self.grapheme_at(self.cursor.get())
}
@@ -527,6 +599,57 @@ impl LineBuf {
.count()
}).unwrap_or(0)
}
pub fn is_sentence_punctuation(&mut self, pos: usize) -> bool {
if let Some(gr) = self.grapheme_at(pos) {
if PUNCTUATION.contains(&gr) && self.grapheme_after(pos).is_some() {
let mut fwd_indices = (pos + 1..self.cursor.max).peekable();
if self.grapheme_after(pos).is_some_and(|gr| [")","]","\"","'"].contains(&gr)) {
while let Some(idx) = fwd_indices.peek() {
if self.grapheme_after(*idx).is_some_and(|gr| [")","]","\"","'"].contains(&gr)) {
fwd_indices.next();
} else {
break
}
}
}
if let Some(idx) = fwd_indices.next() {
if let Some(gr) = self.grapheme_at(idx) {
if is_whitespace(gr) {
return true
}
}
}
}
}
false
}
pub fn is_sentence_start(&mut self, pos: usize) -> bool {
if self.grapheme_before(pos).is_some_and(is_whitespace) {
let pos = pos.saturating_sub(1);
let mut bkwd_indices = (0..pos).rev().peekable();
while let Some(idx) = bkwd_indices.next() {
let Some(gr) = self.read_grapheme_at(idx) else { break };
if [")","]","\"","'"].contains(&gr) {
while let Some(idx) = bkwd_indices.peek() {
let Some(gr) = self.read_grapheme_at(*idx) else { break };
if [")","]","\"","'"].contains(&gr) {
bkwd_indices.next();
} else {
break
}
}
}
if !is_whitespace(gr) {
if [".","?","!"].contains(&gr) {
return true
} else {
break
}
}
}
}
false
}
pub fn nth_next_line(&mut self, n: usize) -> Option<(usize,usize)> {
let line_no = self.cursor_line_number() + n;
if line_no > self.total_lines() {
@@ -657,50 +780,154 @@ impl LineBuf {
}
}
}
pub fn is_word_bound(&mut self, pos: usize, word: Word, dir: Direction) -> bool {
let clamped_pos = ClampedUsize::new(pos, self.cursor.max, true);
let cur_char = self.grapheme_at(clamped_pos.get()).map(|c| c.to_string()).unwrap();
let other_pos = match dir {
Direction::Forward => clamped_pos.ret_add(1),
Direction::Backward => clamped_pos.ret_sub(1)
};
if other_pos == clamped_pos.get() { return true }
let other_char = self.grapheme_at(other_pos).unwrap();
match word {
Word::Big => is_whitespace(other_char),
Word::Normal => is_other_class_or_is_ws(other_char, &cur_char)
}
}
pub fn dispatch_text_obj(
&mut self,
count: usize,
text_obj: TextObj,
bound: Bound
text_obj: TextObj
) -> Option<(usize,usize)> {
match text_obj {
// Text groups
TextObj::Word(word) => self.text_obj_word(count, bound, word),
TextObj::Sentence(dir) => self.text_obj_sentence(count, dir, bound),
TextObj::Paragraph(dir) => self.text_obj_paragraph(count, dir, bound),
TextObj::Word(word,bound) => self.text_obj_word(count, bound, word),
TextObj::Sentence(dir) => {
let (start,end) = self.text_obj_sentence(self.cursor.get(), count, Bound::Around)?;
let cursor = self.cursor.get();
match dir {
Direction::Forward => Some((cursor,end)),
Direction::Backward => Some((start,cursor)),
}
}
TextObj::Paragraph(dir) => {
let (start,end) = self.text_obj_paragraph(count, Bound::Around)?;
let cursor = self.cursor.get();
match dir {
Direction::Forward => Some((cursor,end)),
Direction::Backward => Some((start,cursor)),
}
}
TextObj::WholeSentence(bound) => self.text_obj_sentence(self.cursor.get(), count, bound),
TextObj::WholeParagraph(bound) => self.text_obj_paragraph(count, bound),
// Quoted blocks
TextObj::DoubleQuote |
TextObj::SingleQuote |
TextObj::BacktickQuote => self.text_obj_quote(count, text_obj, bound),
TextObj::DoubleQuote(bound) |
TextObj::SingleQuote(bound) |
TextObj::BacktickQuote(bound) => self.text_obj_quote(count, text_obj, bound),
// Delimited blocks
TextObj::Paren |
TextObj::Bracket |
TextObj::Brace |
TextObj::Angle => self.text_obj_delim(count, text_obj, bound),
TextObj::Paren(bound) |
TextObj::Bracket(bound) |
TextObj::Brace(bound) |
TextObj::Angle(bound) => self.text_obj_delim(count, text_obj, bound),
// Other stuff
TextObj::Tag => todo!(),
TextObj::Tag(bound) => todo!(),
TextObj::Custom(_) => todo!(),
}
}
pub fn text_obj_word(&mut self, count: usize, bound: Bound, word: Word) -> Option<(usize,usize)> {
match bound {
Bound::Inside => {
let start = if self.is_word_bound(self.cursor.get(), word, Direction::Backward) {
self.cursor.get()
} else {
self.end_of_word_forward_or_start_of_word_backward_from(self.cursor.get(), word, Direction::Backward)
};
let end = self.dispatch_word_motion(count, To::Start, word, Direction::Forward, true);
Some((start,end))
}
Bound::Around => {
let start = if self.is_word_bound(self.cursor.get(), word, Direction::Backward) {
self.cursor.get()
} else {
self.end_of_word_forward_or_start_of_word_backward_from(self.cursor.get(), word, Direction::Backward)
};
let end = self.dispatch_word_motion(count, To::Start, word, Direction::Forward, false);
Some((start,end))
}
}
}
pub fn text_obj_sentence(&mut self, start_pos: usize, count: usize, bound: Bound) -> Option<(usize, usize)> {
let mut start = None;
let mut end = None;
let mut fwd_indices = start_pos..self.cursor.max;
while let Some(idx) = fwd_indices.next() {
let Some(gr) = self.grapheme_at(idx) else {
end = Some(self.cursor.max);
break
};
if PUNCTUATION.contains(&gr) && self.is_sentence_punctuation(idx) {
match bound {
Bound::Inside => {
end = Some(idx);
break
}
Bound::Around => {
let mut end_pos = idx;
while let Some(idx) = fwd_indices.next() {
if !self.grapheme_at(idx).is_some_and(is_whitespace) {
end_pos += 1;
break
} else {
end_pos += 1;
}
}
end = Some(end_pos);
break
}
}
}
}
let mut end = end.unwrap_or(self.cursor.max);
flog!(DEBUG, end);
flog!(DEBUG, self.grapheme_at(end));
flog!(DEBUG, self.grapheme_before(end));
flog!(DEBUG, self.grapheme_after(end));
let mut bkwd_indices = (0..end).rev();
while let Some(idx) = bkwd_indices.next() {
if self.is_sentence_start(idx) {
start = Some(idx);
break
}
}
let start = start.unwrap_or(0);
flog!(DEBUG, start);
flog!(DEBUG, self.grapheme_at(start));
flog!(DEBUG, self.grapheme_before(start));
flog!(DEBUG, self.grapheme_after(start));
if count > 1 {
if let Some((_,new_end)) = self.text_obj_sentence(end, count - 1, bound) {
end = new_end;
}
}
Some((start,end))
}
pub fn text_obj_paragraph(&mut self, count: usize, bound: Bound) -> Option<(usize, usize)> {
todo!()
}
pub fn text_obj_sentence(&mut self, count: usize, dir: Direction, bound: Bound) -> Option<(usize, usize)> {
todo!()
}
pub fn text_obj_paragraph(&mut self, count: usize, dir: Direction, bound: Bound) -> Option<(usize, usize)> {
todo!()
}
pub fn text_obj_delim(&mut self, count: usize, text_obj: TextObj, bound: Bound) -> Option<(usize,usize)> {
pub fn text_obj_delim(&mut self , count: usize, text_obj: TextObj, bound: Bound) -> Option<(usize,usize)> {
let mut backward_indices = (0..self.cursor.get()).rev();
let (opener,closer) = match text_obj {
TextObj::Paren => ("(",")"),
TextObj::Bracket => ("[","]"),
TextObj::Brace => ("{","}"),
TextObj::Angle => ("<",">"),
TextObj::Paren(_) => ("(",")"),
TextObj::Bracket(_) => ("[","]"),
TextObj::Brace(_) => ("{","}"),
TextObj::Angle(_) => ("<",">"),
_ => unreachable!()
};
@@ -795,19 +1022,6 @@ impl LineBuf {
Bound::Around => {
// End excludes the quote, so push it forward
end += 1;
// We also need to include any trailing whitespace
let end_of_line = self.end_of_line();
let remainder = end..end_of_line;
for idx in remainder {
let Some(gr) = self.grapheme_at(idx) else { break };
flog!(DEBUG, gr);
if is_whitespace(gr) {
end += 1;
} else {
break
}
}
}
}
@@ -819,9 +1033,9 @@ impl LineBuf {
// Get the grapheme indices backward from the cursor
let mut backward_indices = (start..self.cursor.get()).rev();
let target = match text_obj {
TextObj::DoubleQuote => "\"",
TextObj::SingleQuote => "'",
TextObj::BacktickQuote => "`",
TextObj::DoubleQuote(_) => "\"",
TextObj::SingleQuote(_) => "'",
TextObj::BacktickQuote(_) => "`",
_ => unreachable!()
};
let mut start_pos = None;
@@ -930,6 +1144,103 @@ impl LineBuf {
Some((start, end))
}
pub fn find_next_matching_delim(&mut self) -> Option<usize> {
let delims = [
"[", "]",
"{", "}",
"(", ")",
"<", ">",
];
let mut fwd_indices = self.cursor.get()..self.cursor.max;
let idx = fwd_indices.find(|idx| self.grapheme_at(*idx).is_some_and(|gr| delims.contains(&gr)))?;
let search_direction = match self.grapheme_at(idx)? {
"[" |
"{" |
"(" |
"<" => Direction::Forward,
"]" |
"}" |
")" |
">" => Direction::Backward,
_ => unreachable!()
};
let target_delim = match self.grapheme_at(idx)? {
"[" => "]",
"]" => "[",
"{" => "}",
"}" => "{",
"(" => ")",
")" => "(",
"<" => ">",
">" => "<",
_ => unreachable!()
};
match search_direction {
Direction::Forward => {
let mut fwd_indices = idx..self.cursor_max();
fwd_indices.find(|idx| self.grapheme_at(*idx).is_some_and(|gr| gr == target_delim) && !self.grapheme_is_escaped(*idx))
}
Direction::Backward => {
let mut bkwd_indices = 0..idx;
bkwd_indices.find(|idx| self.grapheme_at(*idx).is_some_and(|gr| gr == target_delim) && !self.grapheme_is_escaped(*idx))
}
}
}
pub fn find_unmatched_delim(&mut self, delim: Delim, dir: Direction) -> Option<usize> {
let (opener,closer) = match delim {
Delim::Paren => ("(",")"),
Delim::Brace => ("{","}"),
Delim::Bracket => ("[","]"),
Delim::Angle => ("<",">"),
};
match dir {
Direction::Forward => {
let mut fwd_indices = self.cursor.get()..self.cursor.max;
let mut depth = 0;
while let Some(idx) = fwd_indices.next() {
if self.grapheme_is_escaped(idx) { continue }
let gr = self.grapheme_at(idx)?;
match gr {
_ if gr == opener => depth += 1,
_ if gr == closer => {
if depth == 0 {
return Some(idx)
} else {
depth -= 1;
}
}
_ => { /* Continue */ }
}
}
None
}
Direction::Backward => {
let mut bkwd_indices = (0..self.cursor.get()).rev();
let mut depth = 0;
while let Some(idx) = bkwd_indices.next() {
if self.grapheme_is_escaped(idx) { continue }
let gr = self.grapheme_at(idx)?;
match gr {
_ if gr == closer => depth += 1,
_ if gr == opener => {
if depth == 0 {
return Some(idx)
} else {
depth -= 1;
}
}
_ => { /* Continue */ }
}
}
None
}
}
}
pub fn dispatch_word_motion(
&mut self,
count: usize,
@@ -1356,17 +1667,73 @@ impl LineBuf {
MotionKind::On(pos.get())
}
}
MotionCmd(count,Motion::TextObj(text_obj, bound)) => {
let Some((start,end)) = self.dispatch_text_obj(count, text_obj, bound) else {
MotionCmd(count,Motion::TextObj(text_obj)) => {
let Some((start,end)) = self.dispatch_text_obj(count, text_obj.clone()) else {
return MotionKind::Null
};
match text_obj {
TextObj::Sentence(dir) |
TextObj::Paragraph(dir) => {
match dir {
Direction::Forward => MotionKind::On(end),
Direction::Backward => {
let cur_sentence_start = start;
let mut start_pos = self.cursor.get();
for _ in 0..count {
if self.is_sentence_start(start_pos) {
// We know there is some punctuation before us now
// Let's find it
let mut bkwd_indices = (0..start_pos).rev();
let punct_pos = bkwd_indices
.find(|idx| self.grapheme_at(*idx).is_some_and(|gr| PUNCTUATION.contains(&gr)))
.unwrap();
if self.grapheme_before(punct_pos).is_some() {
let Some((new_start,_)) = self.text_obj_sentence(punct_pos - 1, count, Bound::Inside) else {
return MotionKind::Null
};
start_pos = new_start;
continue
} else {
return MotionKind::Null
}
} else {
start_pos = cur_sentence_start;
}
}
MotionKind::On(start_pos)
}
}
}
_ => {
MotionKind::Inclusive((start,end))
}
}
}
MotionCmd(_,Motion::ToDelimMatch) => {
// Just ignoring the count here, it does some really weird stuff in Vim
// try doing something like '5%' in vim, it is really strange
let Some(pos) = self.find_next_matching_delim() else {
return MotionKind::Null
};
MotionKind::On(pos)
}
MotionCmd(_,Motion::ToBrace(direction)) |
MotionCmd(_,Motion::ToBracket(direction)) |
MotionCmd(_,Motion::ToParen(direction)) => {
// Counts don't seem to do anything significant for these either
let delim = match motion.1 {
Motion::ToBrace(_) => Delim::Brace,
Motion::ToBracket(_) => Delim::Bracket,
Motion::ToParen(_) => Delim::Paren,
_ => unreachable!()
};
let Some(pos) = self.find_unmatched_delim(delim, direction) else {
return MotionKind::Null
};
MotionKind::Inclusive((start,end))
MotionKind::On(pos)
}
MotionCmd(count,Motion::ToDelimMatch) => todo!(),
MotionCmd(count,Motion::ToBrace(direction)) => todo!(),
MotionCmd(count,Motion::ToBracket(direction)) => todo!(),
MotionCmd(count,Motion::ToParen(direction)) => todo!(),
MotionCmd(count,Motion::EndOfLastWord) => {
let start = self.start_of_line();
let mut newline_count = 0;
@@ -1473,15 +1840,15 @@ impl LineBuf {
MotionCmd(count,Motion::LineDown) |
MotionCmd(count,Motion::LineUp) => {
let Some((start,end)) = (match motion.1 {
Motion::LineUp => self.nth_prev_line(1),
Motion::LineDown => self.nth_next_line(1),
Motion::LineUp => self.nth_prev_line(count),
Motion::LineDown => self.nth_next_line(count),
_ => unreachable!()
}) else {
return MotionKind::Null
};
flog!(DEBUG, self.slice(start..end));
let mut target_col = if let Some(col) = self.saved_col {
let target_col = if let Some(col) = self.saved_col {
col
} else {
let col = self.cursor_col();
@@ -1511,8 +1878,8 @@ impl LineBuf {
MotionCmd(count,Motion::LineDownCharwise) |
MotionCmd(count,Motion::LineUpCharwise) => {
let Some((start,end)) = (match motion.1 {
Motion::LineUpCharwise => self.nth_prev_line(1),
Motion::LineDownCharwise => self.nth_next_line(1),
Motion::LineUpCharwise => self.nth_prev_line(count),
Motion::LineDownCharwise => self.nth_next_line(count),
_ => unreachable!()
}) else {
return MotionKind::Null
@@ -1625,7 +1992,7 @@ impl LineBuf {
}
pub fn update_select_range(&mut self) {
if let Some(mut mode) = self.select_mode {
let Some((mut start,mut end)) = self.select_range.clone() else {
let Some((mut start,mut end)) = self.select_range else {
return
};
match mode {
@@ -1674,9 +2041,16 @@ impl LineBuf {
let end = end.min(col);
self.cursor.set(start + end)
}
MotionKind::Inclusive((start,_)) |
MotionKind::Exclusive((start,_)) => {
self.cursor.set(start)
MotionKind::Inclusive((start,end)) |
MotionKind::Exclusive((start,end)) => {
if self.select_range().is_none() {
self.cursor.set(start)
} else {
let end = end.saturating_sub(1);
self.cursor.set(end);
self.select_mode = Some(SelectMode::Char(SelectAnchor::End));
self.select_range = Some((start,end));
}
}
MotionKind::Null => { /* Do nothing */ }
}