implemented support for the 'sentence' text object in the line editor

This commit is contained in:
2025-06-12 04:52:51 -04:00
parent dbeeff579d
commit 23fb67aba8
3 changed files with 84 additions and 76 deletions

View File

@@ -235,6 +235,9 @@ impl ClampedUsize {
pub fn get(self) -> usize { pub fn get(self) -> usize {
self.value self.value
} }
pub fn cap(&self) -> usize {
self.max
}
pub fn upper_bound(&self) -> usize { pub fn upper_bound(&self) -> usize {
if self.exclusive { if self.exclusive {
self.max.saturating_sub(1) self.max.saturating_sub(1)
@@ -935,29 +938,15 @@ impl LineBuf {
let mut closer_count: u32 = 0; let mut closer_count: u32 = 0;
while let Some(idx) = backward_indices.next() { while let Some(idx) = backward_indices.next() {
let gr = self.grapheme_at(idx)?.to_string(); let gr = self.grapheme_at(idx)?.to_string();
if gr != closer && gr != opener { continue } if (gr != closer && gr != opener) || self.grapheme_is_escaped(idx) { continue }
let mut escaped = false; if gr == closer {
while let Some(idx) = backward_indices.next() { closer_count += 1;
// Keep consuming indices as long as they refer to a backslash } else if closer_count == 0 {
let Some("\\") = self.grapheme_at(idx) else { start_pos = Some(idx);
break break
}; } else {
// On each backslash, flip this boolean closer_count = closer_count.saturating_sub(1)
escaped = !escaped
}
// If there are an even number of backslashes (or none), we are not escaped
// Therefore, we have found the start position
if !escaped {
if gr == closer {
closer_count += 1;
} else if closer_count == 0 {
start_pos = Some(idx);
break
} else {
closer_count = closer_count.saturating_sub(1)
}
} }
} }
@@ -968,8 +957,8 @@ impl LineBuf {
let mut opener_count: u32 = 0; let mut opener_count: u32 = 0;
while let Some(idx) = forward_indices.next() { while let Some(idx) = forward_indices.next() {
if self.grapheme_is_escaped(idx) { continue }
match self.grapheme_at(idx)? { match self.grapheme_at(idx)? {
"\\" => { forward_indices.next(); }
gr if gr == opener => opener_count += 1, gr if gr == opener => opener_count += 1,
gr if gr == closer => { gr if gr == closer => {
if opener_count == 0 { if opener_count == 0 {
@@ -991,8 +980,8 @@ impl LineBuf {
let mut opener_count: u32 = 0; let mut opener_count: u32 = 0;
while let Some(idx) = forward_indices.next() { while let Some(idx) = forward_indices.next() {
if self.grapheme_is_escaped(idx) { continue }
match self.grapheme_at(idx)? { match self.grapheme_at(idx)? {
"\\" => { forward_indices.next(); }
gr if gr == opener => { gr if gr == opener => {
if opener_count == 0 { if opener_count == 0 {
start = Some(idx); start = Some(idx);
@@ -1022,6 +1011,19 @@ impl LineBuf {
Bound::Around => { Bound::Around => {
// End excludes the quote, so push it forward // End excludes the quote, so push it forward
end += 1; 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
}
}
} }
} }
@@ -1673,41 +1675,53 @@ impl LineBuf {
}; };
match text_obj { match text_obj {
TextObj::Sentence(dir) | TextObj::Sentence(dir) |
TextObj::Paragraph(dir) => { TextObj::Paragraph(dir) => {
match dir { match dir {
Direction::Forward => MotionKind::On(end), Direction::Forward => MotionKind::On(end),
Direction::Backward => { Direction::Backward => {
let cur_sentence_start = start; let cur_sentence_start = start;
let mut start_pos = self.cursor.get(); let mut start_pos = self.cursor.get();
for _ in 0..count { for _ in 0..count {
if self.is_sentence_start(start_pos) { if self.is_sentence_start(start_pos) {
// We know there is some punctuation before us now // We know there is some punctuation before us now
// Let's find it // Let's find it
let mut bkwd_indices = (0..start_pos).rev(); let mut bkwd_indices = (0..start_pos).rev();
let punct_pos = bkwd_indices let punct_pos = bkwd_indices
.find(|idx| self.grapheme_at(*idx).is_some_and(|gr| PUNCTUATION.contains(&gr))) .find(|idx| self.grapheme_at(*idx).is_some_and(|gr| PUNCTUATION.contains(&gr)))
.unwrap(); .unwrap();
if self.grapheme_before(punct_pos).is_some() { if self.grapheme_before(punct_pos).is_some() {
let Some((new_start,_)) = self.text_obj_sentence(punct_pos - 1, count, Bound::Inside) else { 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 return MotionKind::Null
}; }
start_pos = new_start;
continue
} else { } else {
return MotionKind::Null start_pos = cur_sentence_start;
} }
} else {
start_pos = cur_sentence_start;
} }
MotionKind::On(start_pos)
} }
MotionKind::On(start_pos)
} }
} }
TextObj::Word(_, bound) |
TextObj::WholeSentence(bound) |
TextObj::WholeParagraph(bound) => {
match bound {
Bound::Inside => MotionKind::Inclusive((start,end)),
Bound::Around => MotionKind::Exclusive((start,end)),
}
} }
_ => { TextObj::DoubleQuote(_) |
TextObj::SingleQuote(_) |
MotionKind::Inclusive((start,end)) TextObj::BacktickQuote(_) |
} TextObj::Paren(_) |
TextObj::Bracket(_) |
TextObj::Brace(_) |
TextObj::Angle(_) => MotionKind::Exclusive((start,end)),
_ => todo!()
} }
} }
MotionCmd(_,Motion::ToDelimMatch) => { MotionCmd(_,Motion::ToDelimMatch) => {
@@ -1927,7 +1941,7 @@ impl LineBuf {
} }
final_end = final_end.min(self.cursor.max); final_end = final_end.min(self.cursor.max);
MotionKind::Inclusive((start,final_end)) MotionKind::Exclusive((start,final_end))
} }
MotionCmd(count,Motion::RepeatMotion) => todo!(), MotionCmd(count,Motion::RepeatMotion) => todo!(),
MotionCmd(count,Motion::RepeatMotionRev) => todo!(), MotionCmd(count,Motion::RepeatMotionRev) => todo!(),
@@ -2041,7 +2055,15 @@ impl LineBuf {
let end = end.min(col); let end = end.min(col);
self.cursor.set(start + end) self.cursor.set(start + end)
} }
MotionKind::Inclusive((start,end)) | MotionKind::Inclusive((start,end)) => {
if self.select_range().is_none() {
self.cursor.set(start)
} else {
self.cursor.set(end);
self.select_mode = Some(SelectMode::Char(SelectAnchor::End));
self.select_range = Some((start,end));
}
}
MotionKind::Exclusive((start,end)) => { MotionKind::Exclusive((start,end)) => {
if self.select_range().is_none() { if self.select_range().is_none() {
self.cursor.set(start) self.cursor.set(start)
@@ -2081,11 +2103,11 @@ impl LineBuf {
ordered(self.cursor.get(), pos) ordered(self.cursor.get(), pos)
} }
MotionKind::InclusiveWithTargetCol((start,end),_) | MotionKind::InclusiveWithTargetCol((start,end),_) |
MotionKind::Inclusive((start,end)) => ordered(*start, *end), MotionKind::Exclusive((start,end)) => ordered(*start, *end),
MotionKind::ExclusiveWithTargetCol((start,end),_) | MotionKind::ExclusiveWithTargetCol((start,end),_) |
MotionKind::Exclusive((start,end)) => { MotionKind::Inclusive((start,end)) => {
let (start, mut end) = ordered(*start, *end); let (start, mut end) = ordered(*start, *end);
end = end.saturating_sub(1); end = ClampedUsize::new(end,self.cursor.max,false).ret_add(1);
(start,end) (start,end)
} }
MotionKind::Null => return None MotionKind::Null => return None
@@ -2463,7 +2485,7 @@ impl LineBuf {
.map(|m| self.eval_motion(verb_ref.as_ref(), m)) .map(|m| self.eval_motion(verb_ref.as_ref(), m))
.unwrap_or({ .unwrap_or({
self.select_range self.select_range
.map(MotionKind::Inclusive) .map(MotionKind::Exclusive)
.unwrap_or(MotionKind::Null) .unwrap_or(MotionKind::Null)
}) })
}; };

View File

@@ -941,6 +941,8 @@ impl ViNormal {
let obj = match chars_clone.next().unwrap() { let obj = match chars_clone.next().unwrap() {
'w' => TextObj::Word(Word::Normal,bound), 'w' => TextObj::Word(Word::Normal,bound),
'W' => TextObj::Word(Word::Big,bound), 'W' => TextObj::Word(Word::Big,bound),
's' => TextObj::WholeSentence(bound),
'p' => TextObj::WholeParagraph(bound),
'"' => TextObj::DoubleQuote(bound), '"' => TextObj::DoubleQuote(bound),
'\'' => TextObj::SingleQuote(bound), '\'' => TextObj::SingleQuote(bound),
'`' => TextObj::BacktickQuote(bound), '`' => TextObj::BacktickQuote(bound),
@@ -1602,6 +1604,8 @@ impl ViVisual {
let obj = match chars_clone.next().unwrap() { let obj = match chars_clone.next().unwrap() {
'w' => TextObj::Word(Word::Normal,bound), 'w' => TextObj::Word(Word::Normal,bound),
'W' => TextObj::Word(Word::Big,bound), 'W' => TextObj::Word(Word::Big,bound),
's' => TextObj::WholeSentence(bound),
'p' => TextObj::WholeParagraph(bound),
'"' => TextObj::DoubleQuote(bound), '"' => TextObj::DoubleQuote(bound),
'\'' => TextObj::SingleQuote(bound), '\'' => TextObj::SingleQuote(bound),
'`' => TextObj::BacktickQuote(bound), '`' => TextObj::BacktickQuote(bound),

View File

@@ -1,18 +0,0 @@
Welcome to the vicut wiki!
Table of contents:
- [🚀 Advanced Usage](https://github.com/km-clay/vicut/wiki/Advanced-Usage)
- [🌀 Nested Repeats](https://github.com/km-clay/vicut/wiki/Advanced-Usage#-nested-repeats)
- [🏷️ Naming Fields](https://github.com/km-clay/vicut/wiki/Advanced-Usage#%EF%B8%8F-naming-fields)
- [🧹 Editing the Input](https://github.com/km-clay/vicut/wiki/Advanced-Usage#-editing-the-input)
- [🎛️ Switching Modes](https://github.com/km-clay/vicut/wiki/Advanced-Usage#%EF%B8%8F-switching-modes)
- [✍️ Insert Mode](https://github.com/km-clay/vicut/wiki/Advanced-Usage#%EF%B8%8F-insert-mode)
- [👁️ Visual Mode](https://github.com/km-clay/vicut/wiki/Advanced-Usage#%EF%B8%8F-visual-mode)
- [🪄 Control Sequence Aliases](https://github.com/km-clay/vicut/wiki/Control-Sequence-Aliases)
- [🔑 Special Key Aliases](https://github.com/km-clay/vicut/wiki/Control-Sequence-Aliases#-special-key-aliases)
- [🧩 Modifier Keys](https://github.com/km-clay/vicut/wiki/Control-Sequence-Aliases#-modifier-keys)
- [💡 Usage Examples](https://github.com/km-clay/vicut/wiki/Usage-Examples)
- [📡 Exhibit A: `speedtest-cli --list`](https://github.com/km-clay/vicut/wiki/Usage-Examples#-exhibit-a-speedtest-cli---list)
- [📊 Tool Comparison](https://github.com/km-clay/vicut/wiki/Usage-Examples#-tool-comparison)
- [🌐 Exhibit B: `nmcli dev`](https://github.com/km-clay/vicut/wiki/Usage-Examples#-exhibit-b-nmcli-dev)
- [📊 Tool Comparison](https://github.com/km-clay/vicut/wiki/Usage-Examples#-tool-comparison-1)