started work on text objects

pressing l in normal mode now accepts hints
This commit is contained in:
2025-05-30 13:17:58 -04:00
parent 8bdc21c8d5
commit 6d9c876640
3 changed files with 174 additions and 27 deletions

View File

@@ -69,14 +69,21 @@ impl From<&str> for CharClass {
}
}
fn is_whitespace(a: &str) -> bool {
CharClass::from(a) == CharClass::Whitespace
}
fn is_other_class_or_ws(a: &str, b: &str) -> bool {
fn is_other_class(a: &str, b: &str) -> bool {
let a = CharClass::from(a);
let b = CharClass::from(b);
if a == CharClass::Whitespace || b == CharClass::Whitespace {
a != b
}
fn is_other_class_or_ws(a: &str, b: &str) -> bool {
if is_whitespace(a) || is_whitespace(b) {
true
} else {
a != b
is_other_class(a, b)
}
}
@@ -244,6 +251,9 @@ impl LineBuf {
}
}
pub fn into_line(self) -> String {
self.buffer
}
pub fn slice_from_cursor_to_end_of_line(&self) -> &str {
let end = self.end_of_line();
&self.buffer[self.cursor..end]
@@ -780,7 +790,82 @@ impl LineBuf {
}
}
}
pub fn eval_text_object(&self, obj: TextObj, bound: Bound) -> Option<Range<usize>> {
flog!(DEBUG, obj);
flog!(DEBUG, bound);
match obj {
TextObj::Word(word) => {
match word {
Word::Big => match bound {
Bound::Inside => {
let start = self.rfind(is_whitespace)
.map(|pos| pos+1)
.unwrap_or(0);
let end = self.find(is_whitespace)
.map(|pos| pos-1)
.unwrap_or(self.byte_len());
Some(start..end)
}
Bound::Around => {
let start = self.rfind(is_whitespace)
.map(|pos| pos+1)
.unwrap_or(0);
let mut end = self.find(is_whitespace)
.unwrap_or(self.byte_len());
if end != self.byte_len() {
end = self.find_from(end,|c| !is_whitespace(c))
.map(|pos| pos-1)
.unwrap_or(self.byte_len())
}
Some(start..end)
}
}
Word::Normal => match bound {
Bound::Inside => {
let cur_graph = self.grapheme_at_cursor()?;
let start = self.rfind(|c| is_other_class(c, cur_graph))
.map(|pos| pos+1)
.unwrap_or(0);
let end = self.find(|c| is_other_class(c, cur_graph))
.map(|pos| pos-1)
.unwrap_or(self.byte_len());
Some(start..end)
}
Bound::Around => {
let cur_graph = self.grapheme_at_cursor()?;
let start = self.rfind(|c| is_other_class(c, cur_graph))
.map(|pos| pos+1)
.unwrap_or(0);
let mut end = self.find(|c| is_other_class(c, cur_graph))
.unwrap_or(self.byte_len());
if end != self.byte_len() && self.is_whitespace(end) {
end = self.find_from(end,|c| !is_whitespace(c))
.map(|pos| pos-1)
.unwrap_or(self.byte_len())
} else {
end -= 1;
}
Some(start..end)
}
}
}
}
TextObj::Line => todo!(),
TextObj::Sentence => todo!(),
TextObj::Paragraph => todo!(),
TextObj::DoubleQuote => todo!(),
TextObj::SingleQuote => todo!(),
TextObj::BacktickQuote => todo!(),
TextObj::Paren => todo!(),
TextObj::Bracket => todo!(),
TextObj::Brace => todo!(),
TextObj::Angle => todo!(),
TextObj::Tag => todo!(),
TextObj::Custom(_) => todo!(),
}
}
pub fn find_word_pos(&self, word: Word, to: To, dir: Direction) -> Option<usize> {
// FIXME: This uses a lot of hardcoded +1/-1 offsets, but they need to account for grapheme boundaries
let mut pos = self.cursor;
match word {
Word::Big => {
@@ -794,22 +879,27 @@ impl LineBuf {
if self.on_start_of_word(word) {
pos += 1;
if pos >= self.byte_len() {
return None
return Some(self.byte_len())
}
}
let ws_pos = self.find_from(pos, |c| CharClass::from(c) == CharClass::Whitespace)?;
let Some(ws_pos) = self.find_from(pos, |c| CharClass::from(c) == CharClass::Whitespace) else {
return Some(self.byte_len())
};
let word_start = self.find_from(ws_pos, |c| CharClass::from(c) != CharClass::Whitespace)?;
Some(word_start)
}
To::End => {
if self.on_whitespace() {
pos = self.find_from(pos, |c| CharClass::from(c) != CharClass::Whitespace)?;
let Some(non_ws_pos) = self.find_from(pos, |c| CharClass::from(c) != CharClass::Whitespace) else {
return Some(self.byte_len())
};
pos = non_ws_pos
}
match self.on_end_of_word(word) {
true => {
pos += 1;
if pos >= self.byte_len() {
return None
return Some(self.byte_len())
}
let word_start = self.find_from(pos, |c| CharClass::from(c) != CharClass::Whitespace)?;
match self.find_from(word_start, |c| CharClass::from(c) == CharClass::Whitespace) {
@@ -831,12 +921,17 @@ impl LineBuf {
match to {
To::Start => {
if self.on_whitespace() {
pos = self.rfind_from(pos, |c| CharClass::from(c) != CharClass::Whitespace)?;
let Some(non_ws_pos) = self.rfind_from(pos, |c| CharClass::from(c) != CharClass::Whitespace) else {
return Some(0)
};
pos = non_ws_pos
}
match self.on_start_of_word(word) {
true => {
pos = pos.checked_sub(1)?;
let prev_word_end = self.rfind_from(pos, |c| CharClass::from(c) != CharClass::Whitespace)?;
let Some(prev_word_end) = self.rfind_from(pos, |c| CharClass::from(c) != CharClass::Whitespace) else {
return Some(0)
};
match self.rfind_from(prev_word_end, |c| CharClass::from(c) == CharClass::Whitespace) {
Some(n) => Some(n + 1), // Land on char after whitespace
None => Some(0) // Start of buffer
@@ -852,13 +947,17 @@ impl LineBuf {
}
To::End => {
if self.on_whitespace() {
return self.rfind_from(pos, |c| CharClass::from(c) != CharClass::Whitespace)
return Some(self.rfind_from(pos, |c| CharClass::from(c) != CharClass::Whitespace).unwrap_or(0))
}
if self.on_end_of_word(word) {
pos = pos.checked_sub(1)?;
}
let last_ws = self.rfind_from(pos, |c| CharClass::from(c) == CharClass::Whitespace)?;
let prev_word_end = self.rfind_from(last_ws, |c| CharClass::from(c) != CharClass::Whitespace)?;
let Some(last_ws) = self.rfind_from(pos, |c| CharClass::from(c) == CharClass::Whitespace) else {
return Some(0)
};
let Some(prev_word_end) = self.rfind_from(last_ws, |c| CharClass::from(c) != CharClass::Whitespace) else {
return Some(0)
};
Some(prev_word_end)
}
}
@@ -871,13 +970,13 @@ impl LineBuf {
match to {
To::Start => {
if self.on_whitespace() {
return self.find_from(pos, |c| CharClass::from(c) != CharClass::Whitespace)
return Some(self.find_from(pos, |c| CharClass::from(c) != CharClass::Whitespace).unwrap_or(self.byte_len()))
}
if self.on_start_of_word(word) {
let cur_char_class = CharClass::from(self.grapheme_at_cursor()?);
pos += 1;
if pos >= self.byte_len() {
return None
return Some(self.byte_len())
}
let next_char = self.grapheme_at(self.next_pos(1)?)?;
let next_char_class = CharClass::from(next_char);
@@ -886,7 +985,9 @@ impl LineBuf {
}
}
let cur_graph = self.grapheme_at(pos)?;
let diff_class_pos = self.find_from(pos, |c| is_other_class_or_ws(c, cur_graph))?;
let Some(diff_class_pos) = self.find_from(pos, |c| is_other_class_or_ws(c, cur_graph)) else {
return Some(self.byte_len())
};
if let CharClass::Whitespace = CharClass::from(self.grapheme_at(diff_class_pos)?) {
let non_ws_pos = self.find_from(diff_class_pos, |c| CharClass::from(c) != CharClass::Whitespace)?;
Some(non_ws_pos)
@@ -897,7 +998,10 @@ impl LineBuf {
To::End => {
flog!(DEBUG,self.buffer);
if self.on_whitespace() {
pos = self.find_from(pos, |c| CharClass::from(c) != CharClass::Whitespace)?;
let Some(non_ws_pos) = self.find_from(pos, |c| CharClass::from(c) != CharClass::Whitespace) else {
return Some(self.byte_len())
};
pos = non_ws_pos
}
match self.on_end_of_word(word) {
true => {
@@ -905,7 +1009,7 @@ impl LineBuf {
let cur_char_class = CharClass::from(self.grapheme_at_cursor()?);
pos += 1;
if pos >= self.byte_len() {
return None
return Some(self.byte_len())
}
let next_char = self.grapheme_at(self.next_pos(1)?)?;
let next_char_class = CharClass::from(next_char);
@@ -980,7 +1084,7 @@ impl LineBuf {
}
To::End => {
if self.on_whitespace() {
return self.rfind_from(pos, |c| CharClass::from(c) != CharClass::Whitespace)
return Some(self.rfind_from(pos, |c| CharClass::from(c) != CharClass::Whitespace).unwrap_or(0))
}
if self.on_end_of_word(word) {
pos = pos.checked_sub(1)?;
@@ -992,7 +1096,9 @@ impl LineBuf {
}
}
let cur_graph = self.grapheme_at(pos)?;
let diff_class_pos = self.rfind_from(pos, |c|is_other_class_or_ws(c, cur_graph))?;
let Some(diff_class_pos) = self.rfind_from(pos, |c|is_other_class_or_ws(c, cur_graph)) else {
return Some(0)
};
if let CharClass::Whitespace = self.grapheme_at(diff_class_pos)?.into() {
let prev_word_end = self.rfind_from(diff_class_pos, |c| CharClass::from(c) != CharClass::Whitespace).unwrap_or(0);
Some(prev_word_end)
@@ -1016,7 +1122,6 @@ impl LineBuf {
/// Find the first grapheme at or after `pos` for which `op` returns true.
/// Returns the byte index of that grapheme in the buffer.
pub fn find_from<F: Fn(&str) -> bool>(&self, pos: usize, op: F) -> Option<usize> {
assert!(is_grapheme_boundary(&self.buffer, pos));
// Iterate over grapheme indices starting at `pos`
let slice = &self.slice_from(pos);
@@ -1030,7 +1135,6 @@ impl LineBuf {
/// Find the last grapheme at or before `pos` for which `op` returns true.
/// Returns the byte index of that grapheme in the buffer.
pub fn rfind_from<F: Fn(&str) -> bool>(&self, pos: usize, op: F) -> Option<usize> {
assert!(is_grapheme_boundary(&self.buffer, pos));
// Iterate grapheme boundaries backward up to pos
let slice = &self.slice_to(pos);
@@ -1058,7 +1162,12 @@ impl LineBuf {
flog!(DEBUG,motion);
match motion {
Motion::WholeLine => MotionKind::Line(0),
Motion::TextObj(text_obj, bound) => todo!(),
Motion::TextObj(text_obj, bound) => {
let Some(range) = self.eval_text_object(text_obj, bound) else {
return MotionKind::Null
};
MotionKind::range(range)
}
Motion::BeginningOfFirstWord => {
let (start,_) = self.this_line();
let first_graph_pos = self.find_from(start, |c| CharClass::from(c) != CharClass::Whitespace).unwrap_or(start);

View File

@@ -3,7 +3,7 @@ use std::time::Duration;
use history::{History, SearchConstraint, SearchKind};
use keys::{KeyCode, KeyEvent, ModKeys};
use linebuf::{strip_ansi_codes_and_escapes, LineBuf};
use mode::{CmdReplay, ViInsert, ViMode, ViNormal, ViReplace};
use mode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace};
use term::Terminal;
use unicode_width::UnicodeWidthStr;
use vicmd::{Motion, MotionCmd, RegisterName, To, Verb, VerbCmd, ViCmd};
@@ -130,10 +130,30 @@ impl FernVi {
}
pub fn should_accept_hint(&self, event: &KeyEvent) -> bool {
if self.line.at_end_of_buffer() && self.line.has_hint() {
matches!(
event,
KeyEvent(KeyCode::Right, ModKeys::NONE)
)
match self.mode.report_mode() {
ModeReport::Replace |
ModeReport::Insert => {
matches!(
event,
KeyEvent(KeyCode::Right, ModKeys::NONE)
)
}
ModeReport::Visual |
ModeReport::Normal => {
matches!(
event,
KeyEvent(KeyCode::Right, ModKeys::NONE)
) ||
(
self.mode.pending_seq().unwrap(/* always Some on normal mode */).is_empty() &&
matches!(
event,
KeyEvent(KeyCode::Char('l'), ModKeys::NONE)
)
)
}
_ => unimplemented!()
}
} else {
false
}

View File

@@ -7,6 +7,14 @@ use super::keys::{KeyEvent as E, KeyCode as K, ModKeys as M};
use super::vicmd::{Anchor, Bound, Dest, Direction, Motion, MotionBuilder, MotionCmd, RegisterName, TextObj, To, Verb, VerbBuilder, VerbCmd, ViCmd, Word};
use crate::prelude::*;
pub enum ModeReport {
Insert,
Normal,
Visual,
Replace,
Unknown
}
#[derive(Debug,Clone)]
pub enum CmdReplay {
ModeReplay { cmds: Vec<ViCmd>, repeat: u16 },
@@ -41,6 +49,7 @@ pub trait ViMode {
fn move_cursor_on_undo(&self) -> bool;
fn clamp_cursor(&self) -> bool;
fn hist_scroll_start_pos(&self) -> Option<To>;
fn report_mode(&self) -> ModeReport;
}
#[derive(Default,Debug)]
@@ -149,6 +158,9 @@ impl ViMode for ViInsert {
fn hist_scroll_start_pos(&self) -> Option<To> {
Some(To::End)
}
fn report_mode(&self) -> ModeReport {
ModeReport::Insert
}
}
#[derive(Default,Debug)]
@@ -252,6 +264,9 @@ impl ViMode for ViReplace {
fn hist_scroll_start_pos(&self) -> Option<To> {
Some(To::End)
}
fn report_mode(&self) -> ModeReport {
ModeReport::Replace
}
}
#[derive(Default,Debug)]
pub struct ViNormal {
@@ -794,6 +809,9 @@ impl ViMode for ViNormal {
fn hist_scroll_start_pos(&self) -> Option<To> {
None
}
fn report_mode(&self) -> ModeReport {
ModeReport::Normal
}
}
pub fn common_cmds(key: E) -> Option<ViCmd> {