diff --git a/src/prompt/readline/linebuf.rs b/src/prompt/readline/linebuf.rs index d4ded81..308876d 100644 --- a/src/prompt/readline/linebuf.rs +++ b/src/prompt/readline/linebuf.rs @@ -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> { + 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 { + // 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 bool>(&self, pos: usize, op: F) -> Option { - 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 bool>(&self, pos: usize, op: F) -> Option { - 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); diff --git a/src/prompt/readline/mod.rs b/src/prompt/readline/mod.rs index 982aea2..57133dc 100644 --- a/src/prompt/readline/mod.rs +++ b/src/prompt/readline/mod.rs @@ -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 } diff --git a/src/prompt/readline/mode.rs b/src/prompt/readline/mode.rs index 97d2a38..988cc83 100644 --- a/src/prompt/readline/mode.rs +++ b/src/prompt/readline/mode.rs @@ -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, 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; + fn report_mode(&self) -> ModeReport; } #[derive(Default,Debug)] @@ -149,6 +158,9 @@ impl ViMode for ViInsert { fn hist_scroll_start_pos(&self) -> Option { 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 { 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 { None } + fn report_mode(&self) -> ModeReport { + ModeReport::Normal + } } pub fn common_cmds(key: E) -> Option {