Implemented proper variable scoping
Extracted business logic out of signal handler functions Consolidated state variables into a single struct Implemented var types
This commit is contained in:
@@ -26,11 +26,17 @@ fn get_prompt() -> ShResult<String> {
|
||||
expand_prompt(&prompt)
|
||||
}
|
||||
|
||||
pub fn readline(edit_mode: FernEditMode) -> ShResult<String> {
|
||||
pub fn readline(edit_mode: FernEditMode, initial: Option<&str>) -> ShResult<String> {
|
||||
let prompt = get_prompt()?;
|
||||
let mut reader: Box<dyn Readline> = match edit_mode {
|
||||
FernEditMode::Vi => Box::new(FernVi::new(Some(prompt))?),
|
||||
FernEditMode::Emacs => todo!(),
|
||||
FernEditMode::Vi => {
|
||||
let mut fern_vi = FernVi::new(Some(prompt))?;
|
||||
if let Some(input) = initial {
|
||||
fern_vi = fern_vi.with_initial(&input)
|
||||
}
|
||||
Box::new(fern_vi) as Box<dyn Readline>
|
||||
}
|
||||
FernEditMode::Emacs => todo!(), // idk if I'm ever gonna do this one actually, I don't use emacs
|
||||
};
|
||||
reader.readline()
|
||||
}
|
||||
|
||||
@@ -622,17 +622,25 @@ impl LineBuf {
|
||||
.map(|slice| slice.graphemes(true).filter(|g| *g == "\n").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() {
|
||||
pub fn is_sentence_punctuation(&self, pos: usize) -> bool {
|
||||
self.next_sentence_start_from_punctuation(pos).is_some()
|
||||
}
|
||||
|
||||
/// If position is at sentence-ending punctuation, returns the position of the next sentence start.
|
||||
/// Handles closing delimiters (`)`, `]`, `"`, `'`) after punctuation.
|
||||
#[allow(clippy::collapsible_if)]
|
||||
pub fn next_sentence_start_from_punctuation(&self, pos: usize) -> Option<usize> {
|
||||
if let Some(gr) = self.read_grapheme_at(pos) {
|
||||
if PUNCTUATION.contains(&gr) && self.read_grapheme_after(pos).is_some() {
|
||||
let mut fwd_indices = (pos + 1..self.cursor.max).peekable();
|
||||
// Skip any closing delimiters after the punctuation
|
||||
if self
|
||||
.grapheme_after(pos)
|
||||
.read_grapheme_after(pos)
|
||||
.is_some_and(|gr| [")", "]", "\"", "'"].contains(&gr))
|
||||
{
|
||||
while let Some(idx) = fwd_indices.peek() {
|
||||
if self
|
||||
.grapheme_after(*idx)
|
||||
.read_grapheme_at(*idx)
|
||||
.is_some_and(|gr| [")", "]", "\"", "'"].contains(&gr))
|
||||
{
|
||||
fwd_indices.next();
|
||||
@@ -641,16 +649,32 @@ impl LineBuf {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Now we should be at whitespace - skip it to find sentence start
|
||||
if let Some(idx) = fwd_indices.next() {
|
||||
if let Some(gr) = self.grapheme_at(idx) {
|
||||
if let Some(gr) = self.read_grapheme_at(idx) {
|
||||
if is_whitespace(gr) {
|
||||
return true;
|
||||
if gr == "\n" {
|
||||
return Some(idx);
|
||||
}
|
||||
// Skip remaining whitespace to find actual sentence start
|
||||
while let Some(idx) = fwd_indices.next() {
|
||||
if let Some(gr) = self.read_grapheme_at(idx) {
|
||||
if is_whitespace(gr) {
|
||||
if gr == "\n" {
|
||||
return Some(idx);
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
return Some(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
None
|
||||
}
|
||||
pub fn is_sentence_start(&mut self, pos: usize) -> bool {
|
||||
if self.grapheme_before(pos).is_some_and(is_whitespace) {
|
||||
@@ -875,11 +899,7 @@ impl LineBuf {
|
||||
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,
|
||||
)
|
||||
self.start_of_word_backward(self.cursor.get(), word)
|
||||
};
|
||||
let end = self.dispatch_word_motion(count, To::Start, word, Direction::Forward, true);
|
||||
Some((start, end))
|
||||
@@ -888,11 +908,7 @@ impl LineBuf {
|
||||
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,
|
||||
)
|
||||
self.start_of_word_backward(self.cursor.get(), word)
|
||||
};
|
||||
let end = self.dispatch_word_motion(count, To::Start, word, Direction::Forward, false);
|
||||
Some((start, end))
|
||||
@@ -907,39 +923,26 @@ impl LineBuf {
|
||||
) -> Option<(usize, usize)> {
|
||||
let mut start = None;
|
||||
let mut end = None;
|
||||
let mut fwd_indices = start_pos..self.cursor.max;
|
||||
let mut fwd_indices = (start_pos..self.cursor.max).peekable();
|
||||
while let Some(idx) = fwd_indices.next() {
|
||||
let Some(gr) = self.grapheme_at(idx) else {
|
||||
end = Some(self.cursor.max);
|
||||
if self.grapheme_at(idx).is_none() {
|
||||
break;
|
||||
};
|
||||
if PUNCTUATION.contains(&gr) && self.is_sentence_punctuation(idx) {
|
||||
}
|
||||
|
||||
if let Some(next_sentence_start) = self.next_sentence_start_from_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);
|
||||
end = Some(next_sentence_start);
|
||||
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() {
|
||||
@@ -949,10 +952,6 @@ impl LineBuf {
|
||||
}
|
||||
}
|
||||
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) {
|
||||
@@ -1321,7 +1320,6 @@ impl LineBuf {
|
||||
dir: Direction,
|
||||
include_last_char: bool,
|
||||
) -> usize {
|
||||
// Not sorry for these method names btw
|
||||
let mut pos = ClampedUsize::new(self.cursor.get(), self.cursor.max, false);
|
||||
for i in 0..count {
|
||||
// We alter 'include_last_char' to only be true on the last iteration
|
||||
@@ -1331,16 +1329,12 @@ impl LineBuf {
|
||||
pos.set(match to {
|
||||
To::Start => {
|
||||
match dir {
|
||||
Direction::Forward => self.start_of_word_forward_or_end_of_word_backward_from(
|
||||
pos.get(),
|
||||
word,
|
||||
dir,
|
||||
include_last_char_and_is_last_word,
|
||||
),
|
||||
Direction::Forward => {
|
||||
self.start_of_word_forward(pos.get(), word, include_last_char_and_is_last_word)
|
||||
}
|
||||
Direction::Backward => 'backward: {
|
||||
// We also need to handle insert mode's Ctrl+W behaviors here
|
||||
let target =
|
||||
self.end_of_word_forward_or_start_of_word_backward_from(pos.get(), word, dir);
|
||||
let target = self.start_of_word_backward(pos.get(), word);
|
||||
|
||||
// Check to see if we are in insert mode
|
||||
let Some(start_pos) = self.insert_mode_start_pos else {
|
||||
@@ -1361,38 +1355,18 @@ impl LineBuf {
|
||||
}
|
||||
}
|
||||
To::End => match dir {
|
||||
Direction::Forward => {
|
||||
self.end_of_word_forward_or_start_of_word_backward_from(pos.get(), word, dir)
|
||||
}
|
||||
Direction::Backward => {
|
||||
self.start_of_word_forward_or_end_of_word_backward_from(pos.get(), word, dir, false)
|
||||
}
|
||||
Direction::Forward => self.end_of_word_forward(pos.get(), word),
|
||||
Direction::Backward => self.end_of_word_backward(pos.get(), word, false),
|
||||
},
|
||||
});
|
||||
}
|
||||
pos.get()
|
||||
}
|
||||
|
||||
/// Finds the start of a word forward, or the end of a word backward
|
||||
///
|
||||
/// Finding the start of a word in the forward direction, and finding the end
|
||||
/// of a word in the backward direction are logically the same operation, if
|
||||
/// you use a reversed iterator for the backward motion.
|
||||
///
|
||||
/// Tied with 'end_of_word_forward_or_start_of_word_backward_from()' for the
|
||||
/// longest method name I have ever written
|
||||
pub fn start_of_word_forward_or_end_of_word_backward_from(
|
||||
&mut self,
|
||||
mut pos: usize,
|
||||
word: Word,
|
||||
dir: Direction,
|
||||
include_last_char: bool,
|
||||
) -> usize {
|
||||
let default = match dir {
|
||||
Direction::Backward => 0,
|
||||
Direction::Forward => self.grapheme_indices().len(),
|
||||
};
|
||||
let mut indices_iter = self.directional_indices_iter_from(pos, dir).peekable(); // And make it peekable
|
||||
/// Find the start of the next word forward
|
||||
pub fn start_of_word_forward(&mut self, mut pos: usize, word: Word, include_last_char: bool) -> usize {
|
||||
let default = self.grapheme_indices().len();
|
||||
let mut indices_iter = (pos..self.cursor.max).peekable();
|
||||
|
||||
match word {
|
||||
Word::Big => {
|
||||
@@ -1404,7 +1378,6 @@ impl LineBuf {
|
||||
let Some(idx) = indices_iter.next() else {
|
||||
return default;
|
||||
};
|
||||
// We have a 'cw' call, do not include the trailing whitespace
|
||||
if include_last_char {
|
||||
return idx;
|
||||
} else {
|
||||
@@ -1412,15 +1385,14 @@ impl LineBuf {
|
||||
}
|
||||
}
|
||||
|
||||
// Check current grapheme
|
||||
let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else {
|
||||
return default;
|
||||
};
|
||||
let on_whitespace = is_whitespace(&cur_char);
|
||||
|
||||
// Find the next whitespace
|
||||
if !on_whitespace {
|
||||
let Some(ws_pos) = indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace))
|
||||
let Some(ws_pos) =
|
||||
indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace))
|
||||
else {
|
||||
return default;
|
||||
};
|
||||
@@ -1429,11 +1401,9 @@ impl LineBuf {
|
||||
}
|
||||
}
|
||||
|
||||
// Return the next visible grapheme position
|
||||
let non_ws_pos = indices_iter
|
||||
indices_iter
|
||||
.find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c)))
|
||||
.unwrap_or(default);
|
||||
non_ws_pos
|
||||
.unwrap_or(default)
|
||||
}
|
||||
Word::Normal => {
|
||||
let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else {
|
||||
@@ -1462,7 +1432,6 @@ impl LineBuf {
|
||||
}
|
||||
let on_whitespace = is_whitespace(&cur_char);
|
||||
|
||||
// Advance until hitting whitespace or a different character class
|
||||
if !on_whitespace {
|
||||
let other_class_pos = indices_iter.find(|i| {
|
||||
self
|
||||
@@ -1472,7 +1441,6 @@ impl LineBuf {
|
||||
let Some(other_class_pos) = other_class_pos else {
|
||||
return default;
|
||||
};
|
||||
// If we hit a different character class, we return here
|
||||
if self
|
||||
.grapheme_at(other_class_pos)
|
||||
.is_some_and(|c| !is_whitespace(c))
|
||||
@@ -1482,79 +1450,54 @@ impl LineBuf {
|
||||
}
|
||||
}
|
||||
|
||||
// We are now certainly on a whitespace character. Advance until a
|
||||
// non-whitespace character.
|
||||
let non_ws_pos = indices_iter
|
||||
indices_iter
|
||||
.find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c)))
|
||||
.unwrap_or(default);
|
||||
non_ws_pos
|
||||
.unwrap_or(default)
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Finds the end of a word forward, or the start of a word backward
|
||||
///
|
||||
/// Finding the end of a word in the forward direction, and finding the start
|
||||
/// of a word in the backward direction are logically the same operation, if
|
||||
/// you use a reversed iterator for the backward motion.
|
||||
pub fn end_of_word_forward_or_start_of_word_backward_from(
|
||||
&mut self,
|
||||
mut pos: usize,
|
||||
word: Word,
|
||||
dir: Direction,
|
||||
) -> usize {
|
||||
let default = match dir {
|
||||
Direction::Backward => 0,
|
||||
Direction::Forward => self.grapheme_indices().len(),
|
||||
};
|
||||
|
||||
let mut indices_iter = self.directional_indices_iter_from(pos, dir).peekable();
|
||||
/// Find the end of the previous word backward
|
||||
pub fn end_of_word_backward(&mut self, mut pos: usize, word: Word, include_last_char: bool) -> usize {
|
||||
let default = self.grapheme_indices().len();
|
||||
let mut indices_iter = (0..pos).rev().peekable();
|
||||
|
||||
match word {
|
||||
Word::Big => {
|
||||
let Some(next_idx) = indices_iter.peek() else {
|
||||
let Some(next) = indices_iter.peek() else {
|
||||
return default;
|
||||
};
|
||||
let on_boundary = self.grapheme_at(*next_idx).is_none_or(is_whitespace);
|
||||
let on_boundary = self.grapheme_at(*next).is_none_or(is_whitespace);
|
||||
if on_boundary {
|
||||
let Some(idx) = indices_iter.next() else {
|
||||
return default;
|
||||
};
|
||||
pos = idx;
|
||||
if include_last_char {
|
||||
return idx;
|
||||
} else {
|
||||
pos = idx;
|
||||
}
|
||||
}
|
||||
// Check current grapheme
|
||||
|
||||
let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else {
|
||||
return default;
|
||||
};
|
||||
let on_whitespace = is_whitespace(&cur_char);
|
||||
|
||||
// Advance iterator to next visible grapheme
|
||||
if on_whitespace {
|
||||
let Some(_non_ws_pos) =
|
||||
indices_iter.find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c)))
|
||||
if !on_whitespace {
|
||||
let Some(ws_pos) =
|
||||
indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace))
|
||||
else {
|
||||
return default;
|
||||
};
|
||||
}
|
||||
|
||||
// The position of the next whitespace will tell us where the end (or start) of
|
||||
// the word is
|
||||
let Some(next_ws_pos) =
|
||||
indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace))
|
||||
else {
|
||||
return default;
|
||||
};
|
||||
pos = next_ws_pos;
|
||||
|
||||
if pos == self.grapheme_indices().len() {
|
||||
// We reached the end of the buffer
|
||||
pos
|
||||
} else {
|
||||
// We hit some whitespace, so we will go back one
|
||||
match dir {
|
||||
Direction::Forward => pos.saturating_sub(1),
|
||||
Direction::Backward => pos + 1,
|
||||
if include_last_char {
|
||||
return ws_pos;
|
||||
}
|
||||
}
|
||||
|
||||
indices_iter
|
||||
.find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c)))
|
||||
.unwrap_or(default)
|
||||
}
|
||||
Word::Normal => {
|
||||
let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else {
|
||||
@@ -1568,32 +1511,131 @@ impl LineBuf {
|
||||
.grapheme_at(*next_idx)
|
||||
.is_none_or(|c| is_other_class_or_is_ws(c, &cur_char));
|
||||
if on_boundary {
|
||||
let next_idx = indices_iter.next().unwrap();
|
||||
pos = next_idx
|
||||
if include_last_char {
|
||||
return *next_idx;
|
||||
} else {
|
||||
pos = *next_idx;
|
||||
}
|
||||
}
|
||||
|
||||
let Some(next_char) = self.grapheme_at(pos).map(|c| c.to_string()) else {
|
||||
return default;
|
||||
};
|
||||
if is_other_class_not_ws(&cur_char, &next_char) {
|
||||
return pos;
|
||||
}
|
||||
let on_whitespace = is_whitespace(&cur_char);
|
||||
|
||||
if !on_whitespace {
|
||||
let other_class_pos = indices_iter.find(|i| {
|
||||
self
|
||||
.grapheme_at(*i)
|
||||
.is_some_and(|c| is_other_class_or_is_ws(c, &next_char))
|
||||
});
|
||||
let Some(other_class_pos) = other_class_pos else {
|
||||
return default;
|
||||
};
|
||||
if self
|
||||
.grapheme_at(other_class_pos)
|
||||
.is_some_and(|c| !is_whitespace(c))
|
||||
|| include_last_char
|
||||
{
|
||||
return other_class_pos;
|
||||
}
|
||||
}
|
||||
|
||||
indices_iter
|
||||
.find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c)))
|
||||
.unwrap_or(default)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the end of the current/next word forward
|
||||
pub fn end_of_word_forward(&mut self, mut pos: usize, word: Word) -> usize {
|
||||
let default = self.cursor.max;
|
||||
if pos >= default {
|
||||
return default;
|
||||
}
|
||||
let mut fwd_indices = (pos + 1..default).peekable();
|
||||
|
||||
match word {
|
||||
Word::Big => {
|
||||
let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else {
|
||||
return default;
|
||||
};
|
||||
let Some(next_idx) = fwd_indices.peek() else {
|
||||
return default;
|
||||
};
|
||||
let on_boundary =
|
||||
!is_whitespace(&cur_char) && self.grapheme_at(*next_idx).is_none_or(is_whitespace);
|
||||
if on_boundary {
|
||||
let Some(idx) = fwd_indices.next() else {
|
||||
return default;
|
||||
};
|
||||
pos = idx;
|
||||
}
|
||||
|
||||
// Check current grapheme
|
||||
let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else {
|
||||
return default;
|
||||
};
|
||||
let on_whitespace = is_whitespace(&cur_char);
|
||||
|
||||
// Proceed to next visible grapheme
|
||||
if on_whitespace {
|
||||
let Some(non_ws_pos) =
|
||||
indices_iter.find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c)))
|
||||
let Some(_non_ws_pos) =
|
||||
fwd_indices.find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c)))
|
||||
else {
|
||||
return default;
|
||||
};
|
||||
pos = non_ws_pos
|
||||
}
|
||||
|
||||
let Some(next_ws_pos) =
|
||||
fwd_indices.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace))
|
||||
else {
|
||||
return default;
|
||||
};
|
||||
pos = next_ws_pos;
|
||||
|
||||
if pos == self.grapheme_indices().len() {
|
||||
pos
|
||||
} else {
|
||||
pos.saturating_sub(1)
|
||||
}
|
||||
}
|
||||
Word::Normal => {
|
||||
let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else {
|
||||
return default;
|
||||
};
|
||||
let Some(next_idx) = fwd_indices.peek() else {
|
||||
return default;
|
||||
};
|
||||
let on_boundary = !is_whitespace(&cur_char)
|
||||
&& self
|
||||
.grapheme_at(*next_idx)
|
||||
.is_none_or(|c| is_other_class_or_is_ws(c, &cur_char));
|
||||
if on_boundary {
|
||||
let next_idx = fwd_indices.next().unwrap();
|
||||
pos = next_idx;
|
||||
}
|
||||
|
||||
let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else {
|
||||
return default;
|
||||
};
|
||||
let on_whitespace = is_whitespace(&cur_char);
|
||||
|
||||
if on_whitespace {
|
||||
let Some(non_ws_pos) =
|
||||
fwd_indices.find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c)))
|
||||
else {
|
||||
return default;
|
||||
};
|
||||
pos = non_ws_pos;
|
||||
}
|
||||
|
||||
let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else {
|
||||
return self.grapheme_indices().len();
|
||||
};
|
||||
// The position of the next differing character class will tell us where the end
|
||||
// (or start) of the word is
|
||||
let Some(next_ws_pos) = indices_iter.find(|i| {
|
||||
let Some(next_ws_pos) = fwd_indices.find(|i| {
|
||||
self
|
||||
.grapheme_at(*i)
|
||||
.is_some_and(|c| is_other_class_or_is_ws(c, &cur_char))
|
||||
@@ -1603,18 +1645,113 @@ impl LineBuf {
|
||||
pos = next_ws_pos;
|
||||
|
||||
if pos == self.grapheme_indices().len() {
|
||||
// We reached the end of the buffer
|
||||
pos
|
||||
} else {
|
||||
// We hit some other character class, so we go back one
|
||||
match dir {
|
||||
Direction::Forward => pos.saturating_sub(1),
|
||||
Direction::Backward => pos + 1,
|
||||
}
|
||||
pos.saturating_sub(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the start of the current/previous word backward
|
||||
pub fn start_of_word_backward(&mut self, mut pos: usize, word: Word) -> usize {
|
||||
let default = 0;
|
||||
let mut indices_iter = (0..pos).rev().peekable();
|
||||
|
||||
match word {
|
||||
Word::Big => {
|
||||
let on_boundary = 'bound_check: {
|
||||
let Some(next_idx) = indices_iter.peek() else {
|
||||
break 'bound_check false;
|
||||
};
|
||||
self.grapheme_at(*next_idx).is_none_or(is_whitespace)
|
||||
};
|
||||
if on_boundary {
|
||||
let Some(idx) = indices_iter.next() else {
|
||||
return default;
|
||||
};
|
||||
pos = idx;
|
||||
}
|
||||
|
||||
let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else {
|
||||
return default;
|
||||
};
|
||||
let on_whitespace = is_whitespace(&cur_char);
|
||||
|
||||
if on_whitespace {
|
||||
let Some(_non_ws_pos) =
|
||||
indices_iter.find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c)))
|
||||
else {
|
||||
return default;
|
||||
};
|
||||
}
|
||||
|
||||
let Some(next_ws_pos) =
|
||||
indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace))
|
||||
else {
|
||||
return default;
|
||||
};
|
||||
pos = next_ws_pos;
|
||||
|
||||
if pos == self.grapheme_indices().len() {
|
||||
pos
|
||||
} else {
|
||||
pos + 1
|
||||
}
|
||||
}
|
||||
Word::Normal => {
|
||||
let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else {
|
||||
return default;
|
||||
};
|
||||
let on_boundary = 'bound_check: {
|
||||
let Some(next_idx) = indices_iter.peek() else {
|
||||
break 'bound_check false;
|
||||
};
|
||||
!is_whitespace(&cur_char)
|
||||
&& self
|
||||
.grapheme_at(*next_idx)
|
||||
.is_some_and(|c| is_other_class_or_is_ws(c, &cur_char))
|
||||
};
|
||||
if on_boundary {
|
||||
let next_idx = indices_iter.next().unwrap();
|
||||
pos = next_idx;
|
||||
}
|
||||
|
||||
let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else {
|
||||
return default;
|
||||
};
|
||||
let on_whitespace = is_whitespace(&cur_char);
|
||||
|
||||
if on_whitespace {
|
||||
let Some(non_ws_pos) =
|
||||
indices_iter.find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c)))
|
||||
else {
|
||||
return default;
|
||||
};
|
||||
pos = non_ws_pos;
|
||||
}
|
||||
|
||||
let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else {
|
||||
return self.grapheme_indices().len();
|
||||
};
|
||||
let Some(next_ws_pos) = indices_iter.find(|i| {
|
||||
self
|
||||
.grapheme_at(*i)
|
||||
.is_some_and(|c| is_other_class_or_is_ws(c, &cur_char))
|
||||
}) else {
|
||||
return default;
|
||||
};
|
||||
pos = next_ws_pos;
|
||||
|
||||
if pos == 0 {
|
||||
pos
|
||||
} else {
|
||||
pos + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn grapheme_index_for_display_col(&self, line: &str, target_col: usize) -> usize {
|
||||
let mut col = 0;
|
||||
for (grapheme_index, g) in line.graphemes(true).enumerate() {
|
||||
|
||||
@@ -48,7 +48,7 @@ impl Readline for FernVi {
|
||||
loop {
|
||||
raw_mode_guard.disable_for(|| self.print_line())?;
|
||||
|
||||
let Some(key) = self.reader.read_key() else {
|
||||
let Some(key) = self.reader.read_key()? else {
|
||||
raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?;
|
||||
std::mem::drop(raw_mode_guard);
|
||||
return Err(ShErr::simple(ShErrKind::ReadlineErr, "EOF"));
|
||||
@@ -116,11 +116,17 @@ impl FernVi {
|
||||
old_layout: None,
|
||||
repeat_action: None,
|
||||
repeat_motion: None,
|
||||
editor: LineBuf::new().with_initial(LOREM_IPSUM, 0),
|
||||
editor: LineBuf::new(),
|
||||
history: History::new()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_initial(mut self, initial: &str) -> Self {
|
||||
self.editor = LineBuf::new().with_initial(initial, 0);
|
||||
self.history.update_pending_cmd(self.editor.as_str());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_layout(&mut self) -> Layout {
|
||||
let line = self.editor.to_string();
|
||||
flog!(DEBUG, line);
|
||||
@@ -268,8 +274,10 @@ impl FernVi {
|
||||
self.repeat_action = mode.as_replay();
|
||||
}
|
||||
|
||||
self.editor.exec_cmd(cmd)?;
|
||||
// Set cursor clamp BEFORE executing the command so that motions
|
||||
// (like EndOfLine for 'A') can reach positions valid in the new mode
|
||||
self.editor.set_cursor_clamp(self.mode.clamp_cursor());
|
||||
self.editor.exec_cmd(cmd)?;
|
||||
|
||||
if selecting {
|
||||
self
|
||||
|
||||
@@ -167,7 +167,7 @@ pub trait WidthCalculator {
|
||||
}
|
||||
|
||||
pub trait KeyReader {
|
||||
fn read_key(&mut self) -> Option<KeyEvent>;
|
||||
fn read_key(&mut self) -> Result<Option<KeyEvent>, ShErr>;
|
||||
}
|
||||
|
||||
pub trait LineWriter {
|
||||
@@ -232,13 +232,11 @@ impl TermBuffer {
|
||||
impl Read for TermBuffer {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
assert!(isatty(self.tty).is_ok_and(|r| r));
|
||||
loop {
|
||||
match nix::unistd::read(self.tty, buf) {
|
||||
Ok(n) => return Ok(n),
|
||||
Err(Errno::EINTR) => {}
|
||||
Err(e) => return Err(std::io::Error::from_raw_os_error(e as i32)),
|
||||
}
|
||||
}
|
||||
match nix::unistd::read(self.tty, buf) {
|
||||
Ok(n) => Ok(n),
|
||||
Err(Errno::EINTR) => Err(Errno::EINTR.into()),
|
||||
Err(e) => Err(std::io::Error::from_raw_os_error(e as i32)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,24 +418,24 @@ impl TermReader {
|
||||
}
|
||||
|
||||
impl KeyReader for TermReader {
|
||||
fn read_key(&mut self) -> Option<KeyEvent> {
|
||||
fn read_key(&mut self) -> Result<Option<KeyEvent>, ShErr> {
|
||||
use core::str;
|
||||
|
||||
let mut collected = Vec::with_capacity(4);
|
||||
|
||||
loop {
|
||||
let byte = self.next_byte().ok()?;
|
||||
let byte = self.next_byte()?;
|
||||
flog!(DEBUG, "read byte: {:?}", byte as char);
|
||||
collected.push(byte);
|
||||
|
||||
// If it's an escape seq, delegate to ESC sequence handler
|
||||
if collected[0] == 0x1b && collected.len() == 1 && self.poll(PollTimeout::ZERO).ok()? {
|
||||
return self.parse_esc_seq().ok();
|
||||
if collected[0] == 0x1b && collected.len() == 1 && self.poll(PollTimeout::ZERO)? {
|
||||
return self.parse_esc_seq().map(Some);
|
||||
}
|
||||
|
||||
// Try parse as valid UTF-8
|
||||
if let Ok(s) = str::from_utf8(&collected) {
|
||||
return Some(KeyEvent::new(s, ModKeys::empty()));
|
||||
return Ok(Some(KeyEvent::new(s, ModKeys::empty())));
|
||||
}
|
||||
|
||||
// UTF-8 max 4 bytes — if it’s invalid at this point, bail
|
||||
@@ -446,7 +444,7 @@ impl KeyReader for TermReader {
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user