more work on vi stuff
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
use std::{fmt::Display, ops::{Deref, DerefMut, Range}, sync::Arc};
|
use std::{fmt::Display, ops::{Deref, DerefMut, Range, RangeBounds, RangeInclusive}, sync::Arc};
|
||||||
|
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
use crate::libsh::{error::ShResult, sys::sh_quit, term::{Style, Styled}};
|
use crate::libsh::{error::ShResult, sys::sh_quit, term::{Style, Styled}};
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
use super::vicmd::{Anchor, Bound, Dest, Direction, Motion, RegisterName, TextObj, To, Verb, ViCmd, Word};
|
use super::vicmd::{Anchor, Bound, Dest, Direction, Motion, RegisterName, TextObj, To, Verb, ViCmd, Word};
|
||||||
|
|
||||||
@@ -12,15 +13,35 @@ pub enum CharClass {
|
|||||||
Symbol
|
Symbol
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum MotionKind {
|
pub enum MotionKind {
|
||||||
Forward(usize),
|
Forward(usize),
|
||||||
To(usize),
|
To(usize),
|
||||||
Backward(usize),
|
Backward(usize),
|
||||||
Range(usize,usize),
|
Range(Range<usize>),
|
||||||
Null
|
Null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl MotionKind {
|
||||||
|
pub fn range<R: RangeBounds<usize>>(range: R) -> Self {
|
||||||
|
let start = match range.start_bound() {
|
||||||
|
std::ops::Bound::Included(&start) => start,
|
||||||
|
std::ops::Bound::Excluded(&start) => start + 1,
|
||||||
|
std::ops::Bound::Unbounded => 0
|
||||||
|
};
|
||||||
|
let end = match range.end_bound() {
|
||||||
|
std::ops::Bound::Included(&end) => end,
|
||||||
|
std::ops::Bound::Excluded(&end) => end + 1,
|
||||||
|
std::ops::Bound::Unbounded => panic!("called range constructor with no upper bound")
|
||||||
|
};
|
||||||
|
if end > start {
|
||||||
|
Self::Range(start..end)
|
||||||
|
} else {
|
||||||
|
Self::Range(end..start)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone,Default,Debug)]
|
#[derive(Clone,Default,Debug)]
|
||||||
pub struct TermCharBuf(pub Vec<TermChar>);
|
pub struct TermCharBuf(pub Vec<TermChar>);
|
||||||
|
|
||||||
@@ -162,10 +183,71 @@ fn is_other_class_or_ws(a: &TermChar, b: &TermChar) -> bool {
|
|||||||
CharClass::from(a) != CharClass::from(b)
|
CharClass::from(a) != CharClass::from(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct UndoPayload {
|
||||||
|
buffer: TermCharBuf,
|
||||||
|
cursor: usize
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default,Debug)]
|
||||||
|
pub struct Edit {
|
||||||
|
pub pos: usize,
|
||||||
|
pub cursor_pos: usize,
|
||||||
|
pub old: TermCharBuf,
|
||||||
|
pub new: TermCharBuf
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Edit {
|
||||||
|
pub fn diff(a: TermCharBuf, b: TermCharBuf, old_cursor_pos: usize) -> Self {
|
||||||
|
use std::cmp::min;
|
||||||
|
|
||||||
|
let mut start = 0;
|
||||||
|
let max_start = min(a.len(), b.len());
|
||||||
|
|
||||||
|
// Calculate the prefix of the edit
|
||||||
|
while start < max_start && a[start] == b[start] {
|
||||||
|
start += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if start == a.len() && start == b.len() {
|
||||||
|
return Edit {
|
||||||
|
pos: start,
|
||||||
|
cursor_pos: old_cursor_pos,
|
||||||
|
old: TermCharBuf(vec![]),
|
||||||
|
new: TermCharBuf(vec![]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut end_a = a.len();
|
||||||
|
let mut end_b = b.len();
|
||||||
|
|
||||||
|
// Calculate the suffix of the edit
|
||||||
|
while end_a > start && end_b > start && a[end_a - 1] == b[end_b - 1] {
|
||||||
|
end_a -= 1;
|
||||||
|
end_b -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slice off the prefix and suffix for both
|
||||||
|
let old = TermCharBuf(a[start..end_a].to_vec());
|
||||||
|
let new = TermCharBuf(b[start..end_b].to_vec());
|
||||||
|
|
||||||
|
Edit {
|
||||||
|
pos: start,
|
||||||
|
cursor_pos: old_cursor_pos,
|
||||||
|
old,
|
||||||
|
new
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default,Debug)]
|
#[derive(Default,Debug)]
|
||||||
pub struct LineBuf {
|
pub struct LineBuf {
|
||||||
buffer: TermCharBuf,
|
buffer: TermCharBuf,
|
||||||
cursor: usize,
|
cursor: usize,
|
||||||
|
clamp_cursor: bool,
|
||||||
|
merge_edit: bool,
|
||||||
|
undo_stack: Vec<Edit>,
|
||||||
|
redo_stack: Vec<Edit>,
|
||||||
|
term_dims: (usize,usize)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LineBuf {
|
impl LineBuf {
|
||||||
@@ -179,6 +261,9 @@ impl LineBuf {
|
|||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
pub fn set_cursor_clamp(&mut self, yn: bool) {
|
||||||
|
self.clamp_cursor = yn
|
||||||
|
}
|
||||||
pub fn buffer(&self) -> &TermCharBuf {
|
pub fn buffer(&self) -> &TermCharBuf {
|
||||||
&self.buffer
|
&self.buffer
|
||||||
}
|
}
|
||||||
@@ -197,8 +282,26 @@ impl LineBuf {
|
|||||||
let cursor = self.cursor();
|
let cursor = self.cursor();
|
||||||
self.buffer.insert(cursor,tc)
|
self.buffer.insert(cursor,tc)
|
||||||
}
|
}
|
||||||
pub fn count_lines(&self) -> usize {
|
pub fn count_lines(&self, first_line_offset: usize) -> usize {
|
||||||
self.buffer.iter().filter(|&c| c == &TermChar::Newline).count()
|
let mut cur_line_len = 0;
|
||||||
|
let mut lines = 1;
|
||||||
|
let first_line_max_len = self.term_dims.1.saturating_sub(first_line_offset);
|
||||||
|
for char in self.buffer.iter() {
|
||||||
|
match char {
|
||||||
|
TermChar::Newline => {
|
||||||
|
lines += 1;
|
||||||
|
cur_line_len = 0;
|
||||||
|
}
|
||||||
|
TermChar::Grapheme(str) => {
|
||||||
|
cur_line_len += str.width().max(1);
|
||||||
|
if (lines == 1 && first_line_max_len > 0 && cur_line_len >= first_line_max_len) || cur_line_len > self.term_dims.1 {
|
||||||
|
lines += 1;
|
||||||
|
cur_line_len = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines
|
||||||
}
|
}
|
||||||
pub fn cursor_back(&mut self, count: usize) {
|
pub fn cursor_back(&mut self, count: usize) {
|
||||||
self.cursor = self.cursor.saturating_sub(count)
|
self.cursor = self.cursor.saturating_sub(count)
|
||||||
@@ -217,13 +320,23 @@ impl LineBuf {
|
|||||||
self.cursor = self.cursor.saturating_sub(1)
|
self.cursor = self.cursor.saturating_sub(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn cursor_display_coords(&self) -> (usize, usize) {
|
pub fn update_term_dims(&mut self, x: usize, y: usize) {
|
||||||
|
self.term_dims = (x,y)
|
||||||
|
}
|
||||||
|
pub fn cursor_display_coords(&self, first_line_offset: Option<usize>) -> (usize, usize) {
|
||||||
let mut x = 0;
|
let mut x = 0;
|
||||||
let mut y = 0;
|
let mut y = 0;
|
||||||
|
let first_line_max_len = first_line_offset.map(|fl| self.term_dims.1.saturating_sub(fl)).unwrap_or_default();
|
||||||
for i in 0..self.cursor() {
|
for i in 0..self.cursor() {
|
||||||
let ch = self.get_char(i).unwrap();
|
let ch = self.get_char(i).unwrap();
|
||||||
match ch {
|
match ch {
|
||||||
TermChar::Grapheme(str) => x += str.width().max(1),
|
TermChar::Grapheme(str) => {
|
||||||
|
x += str.width().max(1);
|
||||||
|
if (y == 0 && first_line_max_len > 0 && x >= first_line_max_len) || x > self.term_dims.1 {
|
||||||
|
y += 1;
|
||||||
|
x = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
TermChar::Newline => {
|
TermChar::Newline => {
|
||||||
y += 1;
|
y += 1;
|
||||||
x = 0;
|
x = 0;
|
||||||
@@ -246,20 +359,6 @@ impl LineBuf {
|
|||||||
lines.push(cur_line);
|
lines.push(cur_line);
|
||||||
lines
|
lines
|
||||||
}
|
}
|
||||||
pub fn display_lines(&self) -> Vec<String> {
|
|
||||||
let line_bullet = "∙ ".styled(Style::Dim);
|
|
||||||
self.split_lines()
|
|
||||||
.into_iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, line)| {
|
|
||||||
if i == 0 {
|
|
||||||
line.to_string()
|
|
||||||
} else {
|
|
||||||
format!("{line_bullet}{line}")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
pub fn on_word_bound(&self, word: Word, pos: usize, dir: Direction) -> bool {
|
pub fn on_word_bound(&self, word: Word, pos: usize, dir: Direction) -> bool {
|
||||||
let check_pos = match dir {
|
let check_pos = match dir {
|
||||||
Direction::Forward => self.num_or_len(pos + 1),
|
Direction::Forward => self.num_or_len(pos + 1),
|
||||||
@@ -276,6 +375,7 @@ impl LineBuf {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
fn backward_until<F: Fn(&TermChar) -> bool>(&self, mut start: usize, cond: F, inclusive: bool) -> usize {
|
fn backward_until<F: Fn(&TermChar) -> bool>(&self, mut start: usize, cond: F, inclusive: bool) -> usize {
|
||||||
|
start = self.num_or_len_minus_one(start);
|
||||||
while start > 0 && !cond(&self.buffer[start]) {
|
while start > 0 && !cond(&self.buffer[start]) {
|
||||||
start -= 1;
|
start -= 1;
|
||||||
}
|
}
|
||||||
@@ -401,7 +501,18 @@ impl LineBuf {
|
|||||||
pos = self.backward_until(pos, |c| c.is_whitespace(), false);
|
pos = self.backward_until(pos, |c| c.is_whitespace(), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
To::End => unreachable!()
|
To::End => {
|
||||||
|
if self.on_word_bound(word, pos, dir) {
|
||||||
|
pos = pos.saturating_sub(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.get_char(pos).is_some_and(|c| c.is_whitespace()) {
|
||||||
|
pos = self.backward_until(pos, |c| !c.is_whitespace(), true);
|
||||||
|
} else {
|
||||||
|
pos = self.backward_until(pos, |c| c.is_whitespace(), true);
|
||||||
|
pos = self.backward_until(pos, |c| !c.is_whitespace(), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Word::Normal => {
|
Word::Normal => {
|
||||||
@@ -419,7 +530,24 @@ impl LineBuf {
|
|||||||
pos = self.backward_until(pos, |c| is_other_class_or_ws(this_char, c), false);
|
pos = self.backward_until(pos, |c| is_other_class_or_ws(this_char, c), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
To::End => unreachable!()
|
To::End => {
|
||||||
|
if self.on_word_bound(word, pos, dir) {
|
||||||
|
// Nudge
|
||||||
|
pos = pos.saturating_sub(1);
|
||||||
|
}
|
||||||
|
// If we are on whitespace, proceed until we are not, inclusively
|
||||||
|
if self.get_char(pos).is_some_and(|c| c.is_whitespace()) {
|
||||||
|
pos = self.backward_until(pos, |c| !c.is_whitespace(), true)
|
||||||
|
} else {
|
||||||
|
// If we are not on whitespace, proceed until we hit something different, inclusively
|
||||||
|
let this_char = self.get_char(pos).unwrap();
|
||||||
|
pos = self.backward_until(pos, |c| is_other_class_or_ws(this_char, c), true);
|
||||||
|
// If we landed on whitespace, proceed until we are not on whitespace
|
||||||
|
if self.get_char(pos).is_some_and(|c| c.is_whitespace()) {
|
||||||
|
pos = self.backward_until(pos, |c| !c.is_whitespace(), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -427,9 +555,196 @@ impl LineBuf {
|
|||||||
}
|
}
|
||||||
pos
|
pos
|
||||||
}
|
}
|
||||||
|
pub fn eval_quote_obj(&self, target: &str, bound: Bound) -> Range<usize> {
|
||||||
|
let mut end;
|
||||||
|
let start;
|
||||||
|
let cursor = self.cursor();
|
||||||
|
let ln_start = self.backward_until(cursor, |c| c == &TermChar::Newline, false);
|
||||||
|
let mut line_chars = self.buffer[ln_start..cursor].iter();
|
||||||
|
let mut in_quote = false;
|
||||||
|
while let Some(ch) = line_chars.next() {
|
||||||
|
let TermChar::Grapheme(ch) = ch else { unreachable!() };
|
||||||
|
match ch.as_ref() {
|
||||||
|
"\\" => {
|
||||||
|
line_chars.next();
|
||||||
|
}
|
||||||
|
"\"" => in_quote = !in_quote,
|
||||||
|
_ => { /* continue */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut start_pos = cursor;
|
||||||
|
let end_pos;
|
||||||
|
if !in_quote {
|
||||||
|
start_pos = self.forward_until(start_pos, |c| c.matches("\n") || c.matches(target), true);
|
||||||
|
if !self.get_char(start_pos).is_some_and(|c| c.matches(target)) {
|
||||||
|
return cursor..cursor
|
||||||
|
}
|
||||||
|
end_pos = self.forward_until(start_pos, |c| c.matches("\n") || c.matches(target), true);
|
||||||
|
if !self.get_char(end_pos).is_some_and(|c| c.matches(target)) {
|
||||||
|
return cursor..cursor
|
||||||
|
}
|
||||||
|
start = start_pos;
|
||||||
|
end = end_pos;
|
||||||
|
} else {
|
||||||
|
start_pos = self.backward_until(start_pos, |c| c.matches("\n") || c.matches(target), true);
|
||||||
|
if !self.get_char(start_pos).is_some_and(|c| c.matches(target)) {
|
||||||
|
return cursor..cursor
|
||||||
|
}
|
||||||
|
end_pos = self.forward_until(self.num_or_len(start_pos + 1), |c| c.matches("\n") || c.matches(target), true);
|
||||||
|
if !self.get_char(end_pos).is_some_and(|c| c.matches(target)) {
|
||||||
|
return cursor..cursor
|
||||||
|
}
|
||||||
|
start = start_pos;
|
||||||
|
end = self.num_or_len(end_pos + 1);
|
||||||
|
|
||||||
|
if bound == Bound::Around && self.get_char(end).is_some_and(|c| c.is_whitespace()) {
|
||||||
|
end += 1;
|
||||||
|
end = self.forward_until(end, |c| !c.is_whitespace(), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mk_range(start,end)
|
||||||
|
}
|
||||||
|
pub fn eval_delim_obj(&self, obj: &TextObj, bound: Bound) -> Range<usize> {
|
||||||
|
// FIXME: logic isn't completely robust i think
|
||||||
|
let opener = match obj {
|
||||||
|
TextObj::Brace => "{",
|
||||||
|
TextObj::Bracket => "[",
|
||||||
|
TextObj::Paren => "(",
|
||||||
|
TextObj::Angle => "<",
|
||||||
|
_ => unreachable!()
|
||||||
|
};
|
||||||
|
let closer = match obj {
|
||||||
|
TextObj::Brace => "}",
|
||||||
|
TextObj::Bracket => "]",
|
||||||
|
TextObj::Paren => ")",
|
||||||
|
TextObj::Angle => ">",
|
||||||
|
_ => unreachable!()
|
||||||
|
};
|
||||||
|
let mut end = None;
|
||||||
|
let mut start = None;
|
||||||
|
let mut delim_count: usize = 0;
|
||||||
|
let ln_range = self.cur_line_range();
|
||||||
|
let cursor = self.cursor();
|
||||||
|
let mut ln_chars = self.buffer[*ln_range.start()..cursor].iter().enumerate();
|
||||||
|
while let Some((i,ch)) = ln_chars.next() {
|
||||||
|
let &TermChar::Grapheme(ch) = &ch else { unreachable!() };
|
||||||
|
match ch.as_ref() {
|
||||||
|
"\\" => {
|
||||||
|
ln_chars.next();
|
||||||
|
}
|
||||||
|
ch if ch == opener => {
|
||||||
|
start = Some(ln_range.start() + i);
|
||||||
|
delim_count += 1;
|
||||||
|
}
|
||||||
|
ch if ch == closer => delim_count -= 1,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut start_pos = None;
|
||||||
|
let mut end_pos = None;
|
||||||
|
if delim_count == 0 {
|
||||||
|
let mut ln_chars = self.buffer[cursor..*ln_range.end()].iter().enumerate();
|
||||||
|
while let Some((i,ch)) = ln_chars.next() {
|
||||||
|
let &TermChar::Grapheme(ch) = &ch else { unreachable!() };
|
||||||
|
match ch.as_ref() {
|
||||||
|
"\\" => {
|
||||||
|
ln_chars.next();
|
||||||
|
}
|
||||||
|
ch if ch == opener => {
|
||||||
|
if delim_count == 0 {
|
||||||
|
start_pos = Some(cursor + i);
|
||||||
|
}
|
||||||
|
delim_count += 1;
|
||||||
|
}
|
||||||
|
ch if ch == closer => {
|
||||||
|
delim_count -= 1;
|
||||||
|
if delim_count == 0 {
|
||||||
|
end_pos = Some(cursor + i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if start_pos.is_none() || end_pos.is_none() {
|
||||||
|
return cursor..cursor
|
||||||
|
} else {
|
||||||
|
start = start_pos;
|
||||||
|
end = end_pos;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let Some(strt) = start else {
|
||||||
|
dbg!("no start");
|
||||||
|
dbg!("no start");
|
||||||
|
dbg!("no start");
|
||||||
|
dbg!("no start");
|
||||||
|
dbg!("no start");
|
||||||
|
dbg!("no start");
|
||||||
|
return cursor..cursor
|
||||||
|
};
|
||||||
|
let strt = self.num_or_len(strt + 1); // skip the paren
|
||||||
|
let target = delim_count.saturating_sub(1);
|
||||||
|
let mut ln_chars = self.buffer[strt..*ln_range.end()].iter().enumerate();
|
||||||
|
dbg!(&ln_chars);
|
||||||
|
dbg!(&ln_chars);
|
||||||
|
dbg!(&ln_chars);
|
||||||
|
dbg!(&ln_chars);
|
||||||
|
|
||||||
|
while let Some((i,ch)) = ln_chars.next() {
|
||||||
|
let &TermChar::Grapheme(ch) = &ch else { unreachable!() };
|
||||||
|
match ch.as_ref() {
|
||||||
|
"\\" => {
|
||||||
|
ln_chars.next();
|
||||||
|
}
|
||||||
|
ch if ch == opener => {
|
||||||
|
delim_count += 1;
|
||||||
|
}
|
||||||
|
ch if ch == closer => {
|
||||||
|
delim_count -= 1;
|
||||||
|
if delim_count == target {
|
||||||
|
end_pos = Some(strt + i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dbg!(end_pos);
|
||||||
|
dbg!(end_pos);
|
||||||
|
dbg!(end_pos);
|
||||||
|
dbg!(start_pos);
|
||||||
|
dbg!(start_pos);
|
||||||
|
dbg!(start_pos);
|
||||||
|
dbg!(start_pos);
|
||||||
|
dbg!(start_pos);
|
||||||
|
dbg!(start_pos);
|
||||||
|
dbg!(start_pos);
|
||||||
|
if end_pos.is_none() {
|
||||||
|
return cursor..cursor
|
||||||
|
} else {
|
||||||
|
end = end_pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(mut start) = start else {
|
||||||
|
return cursor..cursor
|
||||||
|
};
|
||||||
|
let Some(mut end) = end else {
|
||||||
|
return cursor..cursor
|
||||||
|
};
|
||||||
|
match bound {
|
||||||
|
Bound::Inside => {
|
||||||
|
end = end.saturating_sub(1);
|
||||||
|
start = self.num_or_len(start + 1);
|
||||||
|
mk_range(start,end)
|
||||||
|
}
|
||||||
|
Bound::Around => mk_range(start,end)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
pub fn eval_text_obj(&self, obj: TextObj, bound: Bound) -> Range<usize> {
|
pub fn eval_text_obj(&self, obj: TextObj, bound: Bound) -> Range<usize> {
|
||||||
let mut start = self.cursor();
|
let mut start;
|
||||||
let mut end = self.cursor();
|
let mut end;
|
||||||
|
|
||||||
match obj {
|
match obj {
|
||||||
TextObj::Word(word) => {
|
TextObj::Word(word) => {
|
||||||
@@ -455,84 +770,59 @@ impl LineBuf {
|
|||||||
}
|
}
|
||||||
TextObj::Sentence => todo!(),
|
TextObj::Sentence => todo!(),
|
||||||
TextObj::Paragraph => todo!(),
|
TextObj::Paragraph => todo!(),
|
||||||
TextObj::DoubleQuote => {
|
TextObj::DoubleQuote => return self.eval_quote_obj("\"", bound),
|
||||||
let cursor = self.cursor();
|
TextObj::SingleQuote => return self.eval_quote_obj("'", bound),
|
||||||
let ln_start = self.backward_until(cursor, |c| c == &TermChar::Newline, false);
|
TextObj::BacktickQuote => return self.eval_quote_obj("`", bound),
|
||||||
let mut line_chars = self.buffer[ln_start..cursor].iter();
|
TextObj::Paren |
|
||||||
let mut in_quote = false;
|
TextObj::Bracket |
|
||||||
while let Some(ch) = line_chars.next() {
|
TextObj::Brace |
|
||||||
let TermChar::Grapheme(ch) = ch else { unreachable!() };
|
TextObj::Angle => return self.eval_delim_obj(&obj, bound),
|
||||||
match ch.as_ref() {
|
|
||||||
"\\" => {
|
|
||||||
line_chars.next();
|
|
||||||
}
|
|
||||||
"\"" => in_quote = !in_quote,
|
|
||||||
_ => { /* continue */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut start_pos = cursor;
|
|
||||||
let end_pos;
|
|
||||||
if !in_quote {
|
|
||||||
start_pos = self.forward_until(start_pos, |c| c.matches("\n") || c.matches("\""), true);
|
|
||||||
if !self.get_char(start_pos).is_some_and(|c| c.matches("\"")) {
|
|
||||||
return cursor..cursor
|
|
||||||
}
|
|
||||||
end_pos = self.forward_until(start_pos, |c| c.matches("\n") || c.matches("\""), true);
|
|
||||||
if !self.get_char(end_pos).is_some_and(|c| c.matches("\"")) {
|
|
||||||
return cursor..cursor
|
|
||||||
}
|
|
||||||
start = start_pos;
|
|
||||||
end = end_pos;
|
|
||||||
} else {
|
|
||||||
start_pos = self.backward_until(start_pos, |c| c.matches("\n") || c.matches("\""), true);
|
|
||||||
if !self.get_char(start_pos).is_some_and(|c| c.matches("\"")) {
|
|
||||||
return cursor..cursor
|
|
||||||
}
|
|
||||||
end_pos = self.forward_until(self.num_or_len(start_pos + 1), |c| c.matches("\n") || c.matches("\""), true);
|
|
||||||
if !self.get_char(end_pos).is_some_and(|c| c.matches("\"")) {
|
|
||||||
return cursor..cursor
|
|
||||||
}
|
|
||||||
start = start_pos;
|
|
||||||
end = self.num_or_len(end_pos + 1);
|
|
||||||
|
|
||||||
if bound == Bound::Around && self.get_char(end).is_some_and(|c| c.is_whitespace()) {
|
|
||||||
end += 1;
|
|
||||||
end = self.forward_until(end, |c| !c.is_whitespace(), true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
TextObj::SingleQuote => todo!(),
|
|
||||||
TextObj::BacktickQuote => todo!(),
|
|
||||||
TextObj::Paren => todo!(),
|
|
||||||
TextObj::Bracket => todo!(),
|
|
||||||
TextObj::Brace => todo!(),
|
|
||||||
TextObj::Angle => todo!(),
|
|
||||||
TextObj::Tag => todo!(),
|
TextObj::Tag => todo!(),
|
||||||
TextObj::Custom(_) => todo!(),
|
TextObj::Custom(_) => todo!(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if bound == Bound::Inside {
|
if bound == Bound::Inside {
|
||||||
start = self.num_or_len(start + 1);
|
start = self.num_or_len_minus_one(start + 1);
|
||||||
end = end.saturating_sub(1);
|
end = end.saturating_sub(1);
|
||||||
}
|
}
|
||||||
start..end
|
start..end
|
||||||
}
|
}
|
||||||
|
pub fn validate_range(&self, range: &Range<usize>) -> bool {
|
||||||
|
range.end < self.buffer.len()
|
||||||
|
}
|
||||||
|
pub fn cur_line_range(&self) -> RangeInclusive<usize> {
|
||||||
|
let cursor = self.cursor();
|
||||||
|
let mut line_start = self.backward_until(cursor, |c| c == &TermChar::Newline, false);
|
||||||
|
let mut line_end = self.forward_until(cursor, |c| c == &TermChar::Newline, true);
|
||||||
|
if self.get_char(line_start.saturating_sub(1)).is_none_or(|c| c != &TermChar::Newline) {
|
||||||
|
line_start = 0;
|
||||||
|
}
|
||||||
|
if self.get_char(line_end).is_none_or(|c| c != &TermChar::Newline) {
|
||||||
|
line_end = self.buffer.len().saturating_sub(1);
|
||||||
|
line_start = self.backward_until(line_start, |c| c == &TermChar::Newline, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
line_start..=self.num_or_len(line_end + 1)
|
||||||
|
}
|
||||||
/// Clamp a number to the length of the buffer
|
/// Clamp a number to the length of the buffer
|
||||||
pub fn num_or_len(&self, num: usize) -> usize {
|
pub fn num_or_len_minus_one(&self, num: usize) -> usize {
|
||||||
num.min(self.buffer.len().saturating_sub(1))
|
num.min(self.buffer.len().saturating_sub(1))
|
||||||
}
|
}
|
||||||
|
pub fn num_or_len(&self, num: usize) -> usize {
|
||||||
|
num.min(self.buffer.len())
|
||||||
|
}
|
||||||
pub fn eval_motion(&self, motion: Motion) -> MotionKind {
|
pub fn eval_motion(&self, motion: Motion) -> MotionKind {
|
||||||
match motion {
|
match motion {
|
||||||
Motion::WholeLine => {
|
Motion::WholeLine => MotionKind::range(self.cur_line_range()),
|
||||||
let cursor = self.cursor();
|
|
||||||
let start = self.backward_until(cursor, |c| c == &TermChar::Newline, false);
|
|
||||||
let end = self.forward_until(cursor, |c| c == &TermChar::Newline, true);
|
|
||||||
MotionKind::Range(start,end)
|
|
||||||
}
|
|
||||||
Motion::TextObj(text_obj, bound) => {
|
Motion::TextObj(text_obj, bound) => {
|
||||||
let range = self.eval_text_obj(text_obj, bound);
|
let range = self.eval_text_obj(text_obj, bound);
|
||||||
let range = mk_range(range.start, range.end);
|
let range = mk_range(range.start, range.end);
|
||||||
MotionKind::Range(range.start,range.end)
|
let cursor = self.cursor();
|
||||||
|
if range.start == cursor && range.end == cursor {
|
||||||
|
MotionKind::Null
|
||||||
|
} else {
|
||||||
|
MotionKind::range(range)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Motion::BeginningOfFirstWord => {
|
Motion::BeginningOfFirstWord => {
|
||||||
let cursor = self.cursor();
|
let cursor = self.cursor();
|
||||||
@@ -540,17 +830,29 @@ impl LineBuf {
|
|||||||
let first_print = self.forward_until(line_start, |c| !c.is_whitespace(), true);
|
let first_print = self.forward_until(line_start, |c| !c.is_whitespace(), true);
|
||||||
MotionKind::To(first_print)
|
MotionKind::To(first_print)
|
||||||
}
|
}
|
||||||
|
Motion::ToColumn(col) => {
|
||||||
|
let rng = self.cur_line_range();
|
||||||
|
let column = (*rng.start() + (col.saturating_sub(1))).min(*rng.end());
|
||||||
|
MotionKind::To(column)
|
||||||
|
}
|
||||||
Motion::BeginningOfLine => {
|
Motion::BeginningOfLine => {
|
||||||
let cursor = self.cursor();
|
let cursor = self.cursor();
|
||||||
let line_start = self.backward_until(cursor, |c| c == &TermChar::Newline, false);
|
let mut line_start = self.backward_until(cursor, |c| c == &TermChar::Newline, false);
|
||||||
|
if self.get_char(line_start.saturating_sub(1)).is_some_and(|c| c != &TermChar::Newline) {
|
||||||
|
line_start = 0; // FIXME: not sure if this logic is correct
|
||||||
|
}
|
||||||
MotionKind::To(line_start)
|
MotionKind::To(line_start)
|
||||||
}
|
}
|
||||||
Motion::EndOfLine => {
|
Motion::EndOfLine => {
|
||||||
let cursor = self.cursor();
|
let cursor = self.cursor();
|
||||||
let line_end = self.forward_until(cursor, |c| c == &TermChar::Newline, false);
|
let mut line_end = self.forward_until(cursor, |c| c == &TermChar::Newline, false);
|
||||||
|
// If we didn't actually find a newline, we need to go to the end of the buffer
|
||||||
|
if self.get_char(line_end + 1).is_some_and(|c| c != &TermChar::Newline) {
|
||||||
|
line_end = self.buffer.len(); // FIXME: not sure if this logic is correct
|
||||||
|
}
|
||||||
MotionKind::To(line_end)
|
MotionKind::To(line_end)
|
||||||
}
|
}
|
||||||
Motion::BackwardWord(word) => MotionKind::To(self.find_word_pos(word, To::Start, Direction::Backward)),
|
Motion::BackwardWord(dest, word) => MotionKind::To(self.find_word_pos(word, dest, Direction::Backward)),
|
||||||
Motion::ForwardWord(dest, word) => MotionKind::To(self.find_word_pos(word, dest, Direction::Forward)),
|
Motion::ForwardWord(dest, word) => MotionKind::To(self.find_word_pos(word, dest, Direction::Forward)),
|
||||||
Motion::CharSearch(direction, dest, ch) => {
|
Motion::CharSearch(direction, dest, ch) => {
|
||||||
let mut cursor = self.cursor();
|
let mut cursor = self.cursor();
|
||||||
@@ -590,11 +892,19 @@ impl LineBuf {
|
|||||||
MotionKind::Null
|
MotionKind::Null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Motion::Range(s, e) => {
|
||||||
|
if self.validate_range(&(s..e)) {
|
||||||
|
let range = mk_range(s, e);
|
||||||
|
MotionKind::range(range)
|
||||||
|
} else {
|
||||||
|
MotionKind::Null
|
||||||
|
}
|
||||||
|
}
|
||||||
Motion::BackwardChar => MotionKind::Backward(1),
|
Motion::BackwardChar => MotionKind::Backward(1),
|
||||||
Motion::ForwardChar => MotionKind::Forward(1),
|
Motion::ForwardChar => MotionKind::Forward(1),
|
||||||
Motion::LineUp => todo!(),
|
Motion::LineUp => todo!(),
|
||||||
Motion::LineDown => todo!(),
|
Motion::LineDown => todo!(),
|
||||||
Motion::WholeBuffer => MotionKind::Range(0,self.buffer.len().saturating_sub(1)),
|
Motion::WholeBuffer => MotionKind::Range(0..self.buffer.len().saturating_sub(1)),
|
||||||
Motion::BeginningOfBuffer => MotionKind::To(0),
|
Motion::BeginningOfBuffer => MotionKind::To(0),
|
||||||
Motion::EndOfBuffer => MotionKind::To(self.buffer.len().saturating_sub(1)),
|
Motion::EndOfBuffer => MotionKind::To(self.buffer.len().saturating_sub(1)),
|
||||||
Motion::Null => MotionKind::Null,
|
Motion::Null => MotionKind::Null,
|
||||||
@@ -623,9 +933,9 @@ impl LineBuf {
|
|||||||
deleted = self.buffer.drain(back..cursor).collect::<TermCharBuf>();
|
deleted = self.buffer.drain(back..cursor).collect::<TermCharBuf>();
|
||||||
self.apply_motion(MotionKind::To(back));
|
self.apply_motion(MotionKind::To(back));
|
||||||
}
|
}
|
||||||
MotionKind::Range(s, e) => {
|
MotionKind::Range(r) => {
|
||||||
deleted = self.buffer.drain(s..e).collect::<TermCharBuf>();
|
deleted = self.buffer.drain(r.clone()).collect::<TermCharBuf>();
|
||||||
self.apply_motion(MotionKind::To(s));
|
self.apply_motion(MotionKind::To(r.start));
|
||||||
}
|
}
|
||||||
MotionKind::Null => return Ok(())
|
MotionKind::Null => return Ok(())
|
||||||
}
|
}
|
||||||
@@ -672,23 +982,55 @@ impl LineBuf {
|
|||||||
.collect::<TermCharBuf>();
|
.collect::<TermCharBuf>();
|
||||||
self.apply_motion(MotionKind::To(back));
|
self.apply_motion(MotionKind::To(back));
|
||||||
}
|
}
|
||||||
MotionKind::Range(s, e) => {
|
MotionKind::Range(r) => {
|
||||||
yanked = self.buffer[s..e]
|
yanked = self.buffer[r.start..r.end]
|
||||||
.iter()
|
.iter()
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect::<TermCharBuf>();
|
.collect::<TermCharBuf>();
|
||||||
self.apply_motion(MotionKind::To(s));
|
self.apply_motion(MotionKind::To(r.start));
|
||||||
}
|
}
|
||||||
MotionKind::Null => return Ok(())
|
MotionKind::Null => return Ok(())
|
||||||
}
|
}
|
||||||
register.write_to_register(yanked);
|
register.write_to_register(yanked);
|
||||||
}
|
}
|
||||||
Verb::ReplaceChar(_) => todo!(),
|
Verb::ReplaceChar(ch) => {
|
||||||
|
let cursor = self.cursor();
|
||||||
|
if let Some(c) = self.buffer.get_mut(cursor) {
|
||||||
|
let mut tc = TermChar::from(ch);
|
||||||
|
std::mem::swap(c, &mut tc)
|
||||||
|
}
|
||||||
|
self.apply_motion(motion);
|
||||||
|
}
|
||||||
Verb::Substitute => todo!(),
|
Verb::Substitute => todo!(),
|
||||||
Verb::ToggleCase => todo!(),
|
Verb::ToggleCase => todo!(),
|
||||||
Verb::Complete => todo!(),
|
Verb::Complete => todo!(),
|
||||||
Verb::CompleteBackward => todo!(),
|
Verb::CompleteBackward => todo!(),
|
||||||
Verb::Undo => todo!(),
|
Verb::Undo => {
|
||||||
|
let Some(undo) = self.undo_stack.pop() else {
|
||||||
|
return Ok(())
|
||||||
|
};
|
||||||
|
flog!(DEBUG, undo);
|
||||||
|
let Edit { pos, cursor_pos, old, new } = undo;
|
||||||
|
let start = pos;
|
||||||
|
let end = pos + new.len();
|
||||||
|
self.buffer.0.splice(start..end, old.0.clone());
|
||||||
|
let cur_pos = self.cursor();
|
||||||
|
self.cursor = cursor_pos;
|
||||||
|
let redo = Edit { pos, cursor_pos: cur_pos, old: new, new: old };
|
||||||
|
flog!(DEBUG, redo);
|
||||||
|
self.redo_stack.push(redo);
|
||||||
|
}
|
||||||
|
Verb::Redo => {
|
||||||
|
let Some(Edit { pos, cursor_pos, old, new }) = self.redo_stack.pop() else {
|
||||||
|
return Ok(())
|
||||||
|
};
|
||||||
|
let start = pos;
|
||||||
|
let end = pos + new.len();
|
||||||
|
self.buffer.0.splice(start..end, old.0.clone());
|
||||||
|
let cur_pos = self.cursor();
|
||||||
|
self.cursor = cursor_pos;
|
||||||
|
self.undo_stack.push(Edit { pos, cursor_pos: cur_pos, old: new, new: old });
|
||||||
|
}
|
||||||
Verb::RepeatLast => todo!(),
|
Verb::RepeatLast => todo!(),
|
||||||
Verb::Put(anchor) => {
|
Verb::Put(anchor) => {
|
||||||
if let Some(charbuf) = register.read_from_register() {
|
if let Some(charbuf) = register.read_from_register() {
|
||||||
@@ -697,8 +1039,8 @@ impl LineBuf {
|
|||||||
self.cursor_back(1);
|
self.cursor_back(1);
|
||||||
}
|
}
|
||||||
for char in chars {
|
for char in chars {
|
||||||
self.insert_at_cursor(char);
|
|
||||||
self.cursor_fwd(1);
|
self.cursor_fwd(1);
|
||||||
|
self.insert_at_cursor(char);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -720,6 +1062,19 @@ impl LineBuf {
|
|||||||
self.cursor = 0;
|
self.cursor = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Verb::InsertModeLineBreak(anchor) => {
|
||||||
|
match anchor {
|
||||||
|
Anchor::After => {
|
||||||
|
let rng = self.cur_line_range();
|
||||||
|
self.apply_motion(MotionKind::To(self.num_or_len(rng.end() + 1)));
|
||||||
|
self.insert_at_cursor('\n'.into());
|
||||||
|
self.apply_motion(MotionKind::Forward(1));
|
||||||
|
}
|
||||||
|
Anchor::Before => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Verb::Equalize => {
|
||||||
|
}
|
||||||
Verb::InsertMode |
|
Verb::InsertMode |
|
||||||
Verb::NormalMode |
|
Verb::NormalMode |
|
||||||
Verb::VisualMode |
|
Verb::VisualMode |
|
||||||
@@ -734,29 +1089,80 @@ impl LineBuf {
|
|||||||
MotionKind::Forward(n) => self.cursor_fwd(n),
|
MotionKind::Forward(n) => self.cursor_fwd(n),
|
||||||
MotionKind::To(pos) => self.cursor_to(pos),
|
MotionKind::To(pos) => self.cursor_to(pos),
|
||||||
MotionKind::Backward(n) => self.cursor_back(n),
|
MotionKind::Backward(n) => self.cursor_back(n),
|
||||||
MotionKind::Range(s, _) => self.cursor_to(s), // TODO: not sure if this is correct in every case
|
MotionKind::Range(r) => self.cursor_to(r.start), // TODO: not sure if this is correct in every case
|
||||||
MotionKind::Null => { /* Pass */ }
|
MotionKind::Null => { /* Pass */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn handle_edit(&mut self, old: TermCharBuf, new: TermCharBuf, curs_pos: usize) {
|
||||||
|
if self.merge_edit {
|
||||||
|
let mut diff = Edit::diff(old, new, curs_pos);
|
||||||
|
let Some(mut edit) = self.undo_stack.pop() else {
|
||||||
|
self.undo_stack.push(diff);
|
||||||
|
return
|
||||||
|
};
|
||||||
|
dbg!("old");
|
||||||
|
dbg!(&edit);
|
||||||
|
|
||||||
|
edit.new.append(&mut diff.new);
|
||||||
|
dbg!("new");
|
||||||
|
dbg!(&edit);
|
||||||
|
|
||||||
|
self.undo_stack.push(edit);
|
||||||
|
} else {
|
||||||
|
let diff = Edit::diff(old, new, curs_pos);
|
||||||
|
self.undo_stack.push(diff);
|
||||||
|
}
|
||||||
|
}
|
||||||
pub fn exec_cmd(&mut self, cmd: ViCmd) -> ShResult<()> {
|
pub fn exec_cmd(&mut self, cmd: ViCmd) -> ShResult<()> {
|
||||||
let ViCmd { register, verb_count, verb, motion_count, motion, .. } = cmd;
|
flog!(DEBUG, cmd);
|
||||||
|
let clear_redos = !cmd.is_undo_op() || cmd.verb.as_ref().is_some_and(|v| v.1.is_edit());
|
||||||
|
let is_char_insert = cmd.verb.as_ref().is_some_and(|v| v.1.is_char_insert());
|
||||||
|
let is_undo_op = cmd.is_undo_op();
|
||||||
|
|
||||||
|
// Merge character inserts into one edit
|
||||||
|
if self.merge_edit && cmd.verb.as_ref().is_none_or(|v| !v.1.is_char_insert()) {
|
||||||
|
self.merge_edit = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ViCmd { register, verb, motion, .. } = cmd;
|
||||||
|
|
||||||
|
let verb_count = verb.as_ref().map(|v| v.0);
|
||||||
|
let motion_count = motion.as_ref().map(|m| m.0);
|
||||||
|
|
||||||
|
let before = self.buffer.clone();
|
||||||
|
let cursor_pos = self.cursor();
|
||||||
|
|
||||||
for _ in 0..verb_count.unwrap_or(1) {
|
for _ in 0..verb_count.unwrap_or(1) {
|
||||||
for _ in 0..motion_count.unwrap_or(1) {
|
for _ in 0..motion_count.unwrap_or(1) {
|
||||||
let motion = motion
|
let motion = motion
|
||||||
.clone()
|
.clone()
|
||||||
.map(|m| self.eval_motion(m))
|
.map(|m| self.eval_motion(m.1))
|
||||||
.unwrap_or(MotionKind::Null);
|
.unwrap_or(MotionKind::Null);
|
||||||
|
|
||||||
if let Some(verb) = verb.clone() {
|
if let Some(verb) = verb.clone() {
|
||||||
self.exec_verb(verb, motion, register)?;
|
self.exec_verb(verb.1, motion, register)?;
|
||||||
} else {
|
} else {
|
||||||
self.apply_motion(motion);
|
self.apply_motion(motion);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.clamp_cursor();
|
let after = self.buffer.clone();
|
||||||
|
if clear_redos {
|
||||||
|
self.redo_stack.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if before.0 != after.0 && !is_undo_op {
|
||||||
|
self.handle_edit(before, after, cursor_pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_char_insert {
|
||||||
|
self.merge_edit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.clamp_cursor {
|
||||||
|
self.clamp_cursor();
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ use linebuf::{strip_ansi_codes_and_escapes, LineBuf, TermCharBuf};
|
|||||||
use mode::{CmdReplay, ViInsert, ViMode, ViNormal};
|
use mode::{CmdReplay, ViInsert, ViMode, ViNormal};
|
||||||
use term::Terminal;
|
use term::Terminal;
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
use vicmd::{Verb, ViCmd};
|
use vicmd::{MotionCmd, RegisterName, Verb, VerbCmd, ViCmd};
|
||||||
|
|
||||||
use crate::libsh::{error::ShResult, term::{Style, Styled}};
|
use crate::libsh::{error::{ShErr, ShErrKind, ShResult}, term::{Style, Styled}};
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
pub mod keys;
|
pub mod keys;
|
||||||
pub mod term;
|
pub mod term;
|
||||||
@@ -20,7 +21,8 @@ pub struct FernVi {
|
|||||||
line: LineBuf,
|
line: LineBuf,
|
||||||
prompt: String,
|
prompt: String,
|
||||||
mode: Box<dyn ViMode>,
|
mode: Box<dyn ViMode>,
|
||||||
repeat_action: Option<CmdReplay>,
|
last_action: Option<CmdReplay>,
|
||||||
|
last_movement: Option<MotionCmd>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FernVi {
|
impl FernVi {
|
||||||
@@ -33,19 +35,22 @@ impl FernVi {
|
|||||||
line,
|
line,
|
||||||
prompt,
|
prompt,
|
||||||
mode: Box::new(ViInsert::new()),
|
mode: Box::new(ViInsert::new()),
|
||||||
repeat_action: None,
|
last_action: None,
|
||||||
|
last_movement: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn clear_line(&self) {
|
pub fn clear_line(&self) {
|
||||||
let prompt_lines = self.prompt.lines().count();
|
let prompt_lines = self.prompt.lines().count();
|
||||||
|
let last_line_len = strip_ansi_codes_and_escapes(self.prompt.lines().last().unwrap_or_default()).width();
|
||||||
let buf_lines = if self.prompt.ends_with('\n') {
|
let buf_lines = if self.prompt.ends_with('\n') {
|
||||||
self.line.count_lines()
|
self.line.count_lines(last_line_len)
|
||||||
} else {
|
} else {
|
||||||
// The prompt does not end with a newline, so one of the buffer's lines overlaps with it
|
// The prompt does not end with a newline, so one of the buffer's lines overlaps with it
|
||||||
self.line.count_lines().saturating_sub(1)
|
self.line.count_lines(last_line_len).saturating_sub(1)
|
||||||
};
|
};
|
||||||
let total = prompt_lines + buf_lines;
|
let total = prompt_lines + buf_lines;
|
||||||
self.term.write_bytes(b"\r\n");
|
self.term.write_bytes(b"\r\n");
|
||||||
|
self.term.write_bytes(format!("\r\x1b[{total}B").as_bytes());
|
||||||
for _ in 0..total {
|
for _ in 0..total {
|
||||||
self.term.write_bytes(b"\r\x1b[2K\x1b[1A");
|
self.term.write_bytes(b"\r\x1b[2K\x1b[1A");
|
||||||
}
|
}
|
||||||
@@ -57,7 +62,7 @@ impl FernVi {
|
|||||||
}
|
}
|
||||||
let mut prompt_lines = self.prompt.lines().peekable();
|
let mut prompt_lines = self.prompt.lines().peekable();
|
||||||
let mut last_line_len = 0;
|
let mut last_line_len = 0;
|
||||||
let lines = self.line.display_lines();
|
let lines = self.line.split_lines();
|
||||||
while let Some(line) = prompt_lines.next() {
|
while let Some(line) = prompt_lines.next() {
|
||||||
if prompt_lines.peek().is_none() {
|
if prompt_lines.peek().is_none() {
|
||||||
last_line_len = strip_ansi_codes_and_escapes(line).width();
|
last_line_len = strip_ansi_codes_and_escapes(line).width();
|
||||||
@@ -66,9 +71,9 @@ impl FernVi {
|
|||||||
self.term.writeln(line);
|
self.term.writeln(line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let num_lines = lines.len();
|
|
||||||
let mut lines_iter = lines.into_iter().peekable();
|
let mut lines_iter = lines.into_iter().peekable();
|
||||||
|
|
||||||
|
let pos = self.term.cursor_pos();
|
||||||
while let Some(line) = lines_iter.next() {
|
while let Some(line) = lines_iter.next() {
|
||||||
if lines_iter.peek().is_some() {
|
if lines_iter.peek().is_some() {
|
||||||
self.term.writeln(&line);
|
self.term.writeln(&line);
|
||||||
@@ -76,23 +81,30 @@ impl FernVi {
|
|||||||
self.term.write(&line);
|
self.term.write(&line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.term.move_cursor_to(pos);
|
||||||
|
|
||||||
let (x, y) = self.line.cursor_display_coords();
|
let (x, y) = self.line.cursor_display_coords(Some(last_line_len));
|
||||||
let y = num_lines.saturating_sub(y + 1);
|
|
||||||
|
|
||||||
if y > 0 {
|
if y > 0 {
|
||||||
self.term.write(&format!("\r\x1b[{}A", y));
|
self.term.write(&format!("\r\x1b[{}B", y));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add prompt offset to X only if cursor is on the last line (y == 0)
|
|
||||||
let cursor_x = if y == 0 { x + last_line_len } else { x };
|
let cursor_x = if y == 0 { x + last_line_len } else { x };
|
||||||
|
|
||||||
self.term.write(&format!("\r\x1b[{}C", cursor_x));
|
if cursor_x > 0 {
|
||||||
|
self.term.write(&format!("\r\x1b[{}C", cursor_x));
|
||||||
|
}
|
||||||
self.term.write(&self.mode.cursor_style());
|
self.term.write(&self.mode.cursor_style());
|
||||||
}
|
}
|
||||||
pub fn readline(&mut self) -> ShResult<String> {
|
pub fn readline(&mut self) -> ShResult<String> {
|
||||||
|
let dims = self.term.get_dimensions()?;
|
||||||
|
self.line.update_term_dims(dims.0, dims.1);
|
||||||
self.print_buf(false);
|
self.print_buf(false);
|
||||||
loop {
|
loop {
|
||||||
|
let dims = self.term.get_dimensions()?;
|
||||||
|
self.line.update_term_dims(dims.0, dims.1);
|
||||||
|
|
||||||
let key = self.term.read_key();
|
let key = self.term.read_key();
|
||||||
let Some(cmd) = self.mode.handle_key(key) else {
|
let Some(cmd) = self.mode.handle_key(key) else {
|
||||||
continue
|
continue
|
||||||
@@ -109,9 +121,16 @@ impl FernVi {
|
|||||||
pub fn exec_cmd(&mut self, cmd: ViCmd) -> ShResult<()> {
|
pub fn exec_cmd(&mut self, cmd: ViCmd) -> ShResult<()> {
|
||||||
if cmd.is_mode_transition() {
|
if cmd.is_mode_transition() {
|
||||||
let count = cmd.verb_count();
|
let count = cmd.verb_count();
|
||||||
let mut mode: Box<dyn ViMode> = match cmd.verb().unwrap() {
|
let mut mode: Box<dyn ViMode> = match cmd.verb().unwrap().1 {
|
||||||
Verb::InsertMode => Box::new(ViInsert::new().with_count(count)),
|
Verb::InsertModeLineBreak(_) |
|
||||||
Verb::NormalMode => Box::new(ViNormal::new()),
|
Verb::InsertMode => {
|
||||||
|
self.line.set_cursor_clamp(false);
|
||||||
|
Box::new(ViInsert::new().with_count(count as u16))
|
||||||
|
}
|
||||||
|
Verb::NormalMode => {
|
||||||
|
self.line.set_cursor_clamp(true);
|
||||||
|
Box::new(ViNormal::new())
|
||||||
|
}
|
||||||
Verb::VisualMode => todo!(),
|
Verb::VisualMode => todo!(),
|
||||||
Verb::OverwriteMode => todo!(),
|
Verb::OverwriteMode => todo!(),
|
||||||
_ => unreachable!()
|
_ => unreachable!()
|
||||||
@@ -121,10 +140,60 @@ impl FernVi {
|
|||||||
self.term.write(&mode.cursor_style());
|
self.term.write(&mode.cursor_style());
|
||||||
|
|
||||||
if mode.is_repeatable() {
|
if mode.is_repeatable() {
|
||||||
self.repeat_action = mode.as_replay();
|
self.last_action = mode.as_replay();
|
||||||
|
}
|
||||||
|
} else if cmd.is_cmd_repeat() {
|
||||||
|
let Some(replay) = self.last_action.clone() else {
|
||||||
|
return Ok(())
|
||||||
|
};
|
||||||
|
let ViCmd { register, verb, motion, raw_seq } = cmd;
|
||||||
|
let VerbCmd(count,_) = verb.unwrap();
|
||||||
|
match replay {
|
||||||
|
CmdReplay::ModeReplay { cmds, mut repeat } => {
|
||||||
|
if count > 1 {
|
||||||
|
repeat = count as u16;
|
||||||
|
}
|
||||||
|
for _ in 0..repeat {
|
||||||
|
let cmds = cmds.clone();
|
||||||
|
for cmd in cmds {
|
||||||
|
self.line.exec_cmd(cmd)?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CmdReplay::Single(mut cmd) => {
|
||||||
|
if count > 1 {
|
||||||
|
// Override the counts with the one passed to the '.' command
|
||||||
|
if cmd.verb.is_some() {
|
||||||
|
cmd.verb.as_mut().map(|v| v.0 = count);
|
||||||
|
cmd.motion.as_mut().map(|m| m.0 = 0);
|
||||||
|
} else {
|
||||||
|
return Ok(()) // it has to have a verb to be repeatable, something weird happened
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.line.exec_cmd(cmd)?;
|
||||||
|
}
|
||||||
|
_ => unreachable!("motions should be handled in the other branch")
|
||||||
|
}
|
||||||
|
return Ok(())
|
||||||
|
} else if cmd.is_motion_repeat() {
|
||||||
|
match cmd.verb.as_ref().unwrap().1 {
|
||||||
|
Verb::RepeatMotion => {
|
||||||
|
let Some(motion) = self.last_movement.clone() else {
|
||||||
|
return Ok(())
|
||||||
|
};
|
||||||
|
let repeat_cmd = ViCmd {
|
||||||
|
register: RegisterName::default(),
|
||||||
|
verb: None,
|
||||||
|
motion: Some(motion),
|
||||||
|
raw_seq: ";".into()
|
||||||
|
};
|
||||||
|
self.line.exec_cmd(repeat_cmd)?;
|
||||||
|
}
|
||||||
|
Verb::RepeatMotionRev => {}
|
||||||
|
_ => unreachable!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.line.exec_cmd(cmd)?;
|
self.line.exec_cmd(cmd.clone())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,36 @@
|
|||||||
|
use std::iter::Peekable;
|
||||||
|
use std::str::Chars;
|
||||||
|
|
||||||
|
use nix::NixPath;
|
||||||
|
|
||||||
use super::keys::{KeyEvent as E, KeyCode as K, ModKeys as M};
|
use super::keys::{KeyEvent as E, KeyCode as K, ModKeys as M};
|
||||||
use super::linebuf::TermChar;
|
use super::linebuf::TermChar;
|
||||||
use super::vicmd::{Anchor, Bound, Dest, Direction, Motion, MotionBuilder, TextObj, To, Verb, VerbBuilder, ViCmd, Word};
|
use super::vicmd::{Anchor, Bound, Dest, Direction, Motion, MotionBuilder, MotionCmd, RegisterName, TextObj, To, Verb, VerbBuilder, VerbCmd, ViCmd, Word};
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
pub struct CmdReplay {
|
#[derive(Debug,Clone)]
|
||||||
cmds: Vec<ViCmd>,
|
pub enum CmdReplay {
|
||||||
repeat: u16
|
ModeReplay { cmds: Vec<ViCmd>, repeat: u16 },
|
||||||
|
Single(ViCmd),
|
||||||
|
Motion(Motion)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CmdReplay {
|
impl CmdReplay {
|
||||||
pub fn new(cmds: Vec<ViCmd>, repeat: u16) -> Self {
|
pub fn mode(cmds: Vec<ViCmd>, repeat: u16) -> Self {
|
||||||
Self { cmds, repeat }
|
Self::ModeReplay { cmds, repeat }
|
||||||
}
|
}
|
||||||
|
pub fn single(cmd: ViCmd) -> Self {
|
||||||
|
Self::Single(cmd)
|
||||||
|
}
|
||||||
|
pub fn motion(motion: Motion) -> Self {
|
||||||
|
Self::Motion(motion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum CmdState {
|
||||||
|
Pending,
|
||||||
|
Complete,
|
||||||
|
Invalid
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ViMode {
|
pub trait ViMode {
|
||||||
@@ -18,6 +38,7 @@ pub trait ViMode {
|
|||||||
fn is_repeatable(&self) -> bool;
|
fn is_repeatable(&self) -> bool;
|
||||||
fn as_replay(&self) -> Option<CmdReplay>;
|
fn as_replay(&self) -> Option<CmdReplay>;
|
||||||
fn cursor_style(&self) -> String;
|
fn cursor_style(&self) -> String;
|
||||||
|
fn pending_seq(&self) -> Option<String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default,Debug)]
|
#[derive(Default,Debug)]
|
||||||
@@ -53,36 +74,36 @@ impl ViMode for ViInsert {
|
|||||||
match key {
|
match key {
|
||||||
E(K::Grapheme(ch), M::NONE) => {
|
E(K::Grapheme(ch), M::NONE) => {
|
||||||
let ch = TermChar::from(ch);
|
let ch = TermChar::from(ch);
|
||||||
self.pending_cmd.set_verb(Verb::InsertChar(ch));
|
self.pending_cmd.set_verb(VerbCmd(1,Verb::InsertChar(ch)));
|
||||||
self.pending_cmd.set_motion(Motion::ForwardChar);
|
self.pending_cmd.set_motion(MotionCmd(1,Motion::ForwardChar));
|
||||||
self.register_and_return()
|
self.register_and_return()
|
||||||
}
|
}
|
||||||
E(K::Char(ch), M::NONE) => {
|
E(K::Char(ch), M::NONE) => {
|
||||||
self.pending_cmd.set_verb(Verb::InsertChar(TermChar::from(ch)));
|
self.pending_cmd.set_verb(VerbCmd(1,Verb::InsertChar(TermChar::from(ch))));
|
||||||
self.pending_cmd.set_motion(Motion::ForwardChar);
|
self.pending_cmd.set_motion(MotionCmd(1,Motion::ForwardChar));
|
||||||
self.register_and_return()
|
self.register_and_return()
|
||||||
}
|
}
|
||||||
E(K::Char('H'), M::CTRL) |
|
E(K::Char('H'), M::CTRL) |
|
||||||
E(K::Backspace, M::NONE) => {
|
E(K::Backspace, M::NONE) => {
|
||||||
self.pending_cmd.set_verb(Verb::Delete);
|
self.pending_cmd.set_verb(VerbCmd(1,Verb::Delete));
|
||||||
self.pending_cmd.set_motion(Motion::BackwardChar);
|
self.pending_cmd.set_motion(MotionCmd(1,Motion::BackwardChar));
|
||||||
self.register_and_return()
|
self.register_and_return()
|
||||||
}
|
}
|
||||||
|
|
||||||
E(K::BackTab, M::NONE) => {
|
E(K::BackTab, M::NONE) => {
|
||||||
self.pending_cmd.set_verb(Verb::CompleteBackward);
|
self.pending_cmd.set_verb(VerbCmd(1,Verb::CompleteBackward));
|
||||||
self.register_and_return()
|
self.register_and_return()
|
||||||
}
|
}
|
||||||
|
|
||||||
E(K::Char('I'), M::CTRL) |
|
E(K::Char('I'), M::CTRL) |
|
||||||
E(K::Tab, M::NONE) => {
|
E(K::Tab, M::NONE) => {
|
||||||
self.pending_cmd.set_verb(Verb::Complete);
|
self.pending_cmd.set_verb(VerbCmd(1,Verb::Complete));
|
||||||
self.register_and_return()
|
self.register_and_return()
|
||||||
}
|
}
|
||||||
|
|
||||||
E(K::Esc, M::NONE) => {
|
E(K::Esc, M::NONE) => {
|
||||||
self.pending_cmd.set_verb(Verb::NormalMode);
|
self.pending_cmd.set_verb(VerbCmd(1,Verb::NormalMode));
|
||||||
self.pending_cmd.set_motion(Motion::BackwardChar);
|
self.pending_cmd.set_motion(MotionCmd(1,Motion::BackwardChar));
|
||||||
self.register_and_return()
|
self.register_and_return()
|
||||||
}
|
}
|
||||||
_ => common_cmds(key)
|
_ => common_cmds(key)
|
||||||
@@ -94,230 +115,416 @@ impl ViMode for ViInsert {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn as_replay(&self) -> Option<CmdReplay> {
|
fn as_replay(&self) -> Option<CmdReplay> {
|
||||||
Some(CmdReplay::new(self.cmds.clone(), self.repeat_count))
|
Some(CmdReplay::mode(self.cmds.clone(), self.repeat_count))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cursor_style(&self) -> String {
|
fn cursor_style(&self) -> String {
|
||||||
"\x1b[6 q".to_string()
|
"\x1b[6 q".to_string()
|
||||||
}
|
}
|
||||||
|
fn pending_seq(&self) -> Option<String> {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default,Debug)]
|
#[derive(Default,Debug)]
|
||||||
pub struct ViNormal {
|
pub struct ViNormal {
|
||||||
pending_cmd: ViCmd,
|
pending_seq: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ViNormal {
|
impl ViNormal {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
pub fn take_cmd(&mut self) -> ViCmd {
|
|
||||||
std::mem::take(&mut self.pending_cmd)
|
|
||||||
}
|
|
||||||
pub fn clear_cmd(&mut self) {
|
pub fn clear_cmd(&mut self) {
|
||||||
self.pending_cmd = ViCmd::new();
|
self.pending_seq = String::new();
|
||||||
}
|
}
|
||||||
fn handle_pending_builder(&mut self, key: E) -> Option<ViCmd> {
|
pub fn take_cmd(&mut self) -> String {
|
||||||
if self.pending_cmd.wants_register {
|
std::mem::take(&mut self.pending_seq)
|
||||||
if let E(K::Char(ch @ ('a'..='z' | 'A'..='Z')), M::NONE) = key {
|
}
|
||||||
self.pending_cmd.set_register(ch);
|
fn validate_combination(&self, verb: Option<&Verb>, motion: Option<&Motion>) -> CmdState {
|
||||||
return None
|
if verb.is_none() {
|
||||||
} else {
|
match motion {
|
||||||
self.clear_cmd();
|
Some(Motion::TextObj(_,_)) => return CmdState::Invalid,
|
||||||
return None
|
Some(_) => return CmdState::Complete,
|
||||||
}
|
None => return CmdState::Pending
|
||||||
} else if let Some(Verb::Builder(_)) = &self.pending_cmd.verb {
|
|
||||||
todo!() // Don't have any verb builders yet, but might later
|
|
||||||
} else if let Some(Motion::Builder(builder)) = self.pending_cmd.motion.clone() {
|
|
||||||
match builder {
|
|
||||||
MotionBuilder::CharSearch(direction, dest, _) => {
|
|
||||||
if let E(K::Char(ch), M::NONE) = key {
|
|
||||||
self.pending_cmd.set_motion(Motion::CharSearch(
|
|
||||||
direction.unwrap(),
|
|
||||||
dest.unwrap(),
|
|
||||||
ch.into(),
|
|
||||||
));
|
|
||||||
return Some(self.take_cmd());
|
|
||||||
} else {
|
|
||||||
self.clear_cmd();
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MotionBuilder::TextObj(_, bound) => {
|
|
||||||
if let Some(bound) = bound {
|
|
||||||
if let E(K::Char(ch), M::NONE) = key {
|
|
||||||
let obj = match ch {
|
|
||||||
'w' => TextObj::Word(Word::Normal),
|
|
||||||
'W' => TextObj::Word(Word::Big),
|
|
||||||
'(' | ')' => TextObj::Paren,
|
|
||||||
'[' | ']' => TextObj::Bracket,
|
|
||||||
'{' | '}' => TextObj::Brace,
|
|
||||||
'<' | '>' => TextObj::Angle,
|
|
||||||
'"' => TextObj::DoubleQuote,
|
|
||||||
'\'' => TextObj::SingleQuote,
|
|
||||||
'`' => TextObj::BacktickQuote,
|
|
||||||
_ => TextObj::Custom(ch),
|
|
||||||
};
|
|
||||||
self.pending_cmd.set_motion(Motion::TextObj(obj, bound));
|
|
||||||
return Some(self.take_cmd());
|
|
||||||
} else {
|
|
||||||
self.clear_cmd();
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
} else if let E(K::Char(ch), M::NONE) = key {
|
|
||||||
let bound = match ch {
|
|
||||||
'i' => Bound::Inside,
|
|
||||||
'a' => Bound::Around,
|
|
||||||
_ => {
|
|
||||||
self.clear_cmd();
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
self.pending_cmd.set_motion(Motion::Builder(MotionBuilder::TextObj(None, Some(bound))));
|
|
||||||
return None;
|
|
||||||
} else {
|
|
||||||
self.clear_cmd();
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if verb.is_some() && motion.is_none() {
|
||||||
|
match verb.unwrap() {
|
||||||
|
Verb::Put(_) |
|
||||||
|
Verb::DeleteChar(_) => CmdState::Complete,
|
||||||
|
_ => CmdState::Pending
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
CmdState::Complete
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn parse_count(&self, chars: &mut Peekable<Chars<'_>>) -> Option<usize> {
|
||||||
|
let mut count = String::new();
|
||||||
|
let Some(_digit @ '1'..='9') = chars.peek() else {
|
||||||
|
return None
|
||||||
|
};
|
||||||
|
count.push(chars.next().unwrap());
|
||||||
|
while let Some(_digit @ '0'..='9') = chars.peek() {
|
||||||
|
count.push(chars.next().unwrap());
|
||||||
|
}
|
||||||
|
if !count.is_empty() {
|
||||||
|
count.parse::<usize>().ok()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// End the parse and clear the pending sequence
|
||||||
|
pub fn quit_parse(&mut self) -> Option<ViCmd> {
|
||||||
|
flog!(WARN, "exiting parse early with sequence: {}",self.pending_seq);
|
||||||
|
self.clear_cmd();
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
pub fn try_parse(&mut self, ch: char) -> Option<ViCmd> {
|
||||||
|
self.pending_seq.push(ch);
|
||||||
|
flog!(DEBUG, self.pending_seq);
|
||||||
|
let mut chars = self.pending_seq.chars().peekable();
|
||||||
|
|
||||||
|
let register = 'reg_parse: {
|
||||||
|
let mut chars_clone = chars.clone();
|
||||||
|
let count = self.parse_count(&mut chars_clone);
|
||||||
|
|
||||||
|
let Some('"') = chars_clone.next() else {
|
||||||
|
break 'reg_parse RegisterName::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(reg_name) = chars_clone.next() else {
|
||||||
|
return None // Pending register name
|
||||||
|
};
|
||||||
|
match reg_name {
|
||||||
|
'a'..='z' |
|
||||||
|
'A'..='Z' => { /* proceed */ }
|
||||||
|
_ => return self.quit_parse()
|
||||||
|
}
|
||||||
|
|
||||||
|
chars = chars_clone;
|
||||||
|
RegisterName::new(Some(reg_name), count)
|
||||||
|
};
|
||||||
|
|
||||||
|
let verb = 'verb_parse: {
|
||||||
|
let mut chars_clone = chars.clone();
|
||||||
|
let count = self.parse_count(&mut chars_clone).unwrap_or(1);
|
||||||
|
|
||||||
|
let Some(ch) = chars_clone.next() else {
|
||||||
|
break 'verb_parse None
|
||||||
|
};
|
||||||
|
flog!(DEBUG, "parsing verb char '{}'",ch);
|
||||||
|
match ch {
|
||||||
|
'.' => {
|
||||||
|
return Some(
|
||||||
|
ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::RepeatLast)),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
'x' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'verb_parse Some(VerbCmd(count, Verb::DeleteChar(Anchor::After)));
|
||||||
|
}
|
||||||
|
'X' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'verb_parse Some(VerbCmd(count, Verb::DeleteChar(Anchor::Before)));
|
||||||
|
}
|
||||||
|
'p' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'verb_parse Some(VerbCmd(count, Verb::Put(Anchor::After)));
|
||||||
|
}
|
||||||
|
'P' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'verb_parse Some(VerbCmd(count, Verb::Put(Anchor::Before)));
|
||||||
|
}
|
||||||
|
'r' => {
|
||||||
|
let Some(ch) = chars_clone.next() else {
|
||||||
|
return None
|
||||||
|
};
|
||||||
|
return Some(
|
||||||
|
ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(1, Verb::ReplaceChar(ch))),
|
||||||
|
motion: Some(MotionCmd(count, Motion::ForwardChar)),
|
||||||
|
raw_seq: self.take_cmd()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
'u' => {
|
||||||
|
return Some(
|
||||||
|
ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::Undo)),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
'o' => {
|
||||||
|
return Some(
|
||||||
|
ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::InsertModeLineBreak(Anchor::After))),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
'O' => {
|
||||||
|
return Some(
|
||||||
|
ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::InsertModeLineBreak(Anchor::Before))),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
'a' => {
|
||||||
|
return Some(
|
||||||
|
ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::InsertMode)),
|
||||||
|
motion: Some(MotionCmd(1, Motion::ForwardChar)),
|
||||||
|
raw_seq: self.take_cmd()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
'A' => {
|
||||||
|
return Some(
|
||||||
|
ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::InsertMode)),
|
||||||
|
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
||||||
|
raw_seq: self.take_cmd()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
'i' => {
|
||||||
|
return Some(
|
||||||
|
ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::InsertMode)),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
'I' => {
|
||||||
|
return Some(
|
||||||
|
ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::InsertMode)),
|
||||||
|
motion: Some(MotionCmd(1, Motion::BeginningOfFirstWord)),
|
||||||
|
raw_seq: self.take_cmd()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
'y' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'verb_parse Some(VerbCmd(count, Verb::Yank))
|
||||||
|
}
|
||||||
|
'd' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'verb_parse Some(VerbCmd(count, Verb::Delete))
|
||||||
|
}
|
||||||
|
'c' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'verb_parse Some(VerbCmd(count, Verb::Change))
|
||||||
|
}
|
||||||
|
'=' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'verb_parse Some(VerbCmd(count, Verb::Equalize))
|
||||||
|
}
|
||||||
|
_ => break 'verb_parse None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let motion = 'motion_parse: {
|
||||||
|
let mut chars_clone = chars.clone();
|
||||||
|
let count = self.parse_count(&mut chars_clone).unwrap_or(1);
|
||||||
|
|
||||||
|
let Some(ch) = chars_clone.next() else {
|
||||||
|
break 'motion_parse None
|
||||||
|
};
|
||||||
|
flog!(DEBUG, "parsing motion char '{}'",ch);
|
||||||
|
match (ch, &verb) {
|
||||||
|
('d', Some(VerbCmd(_,Verb::Delete))) |
|
||||||
|
('c', Some(VerbCmd(_,Verb::Change))) |
|
||||||
|
('y', Some(VerbCmd(_,Verb::Yank))) |
|
||||||
|
('>', Some(VerbCmd(_,Verb::Indent))) |
|
||||||
|
('<', Some(VerbCmd(_,Verb::Dedent))) => break 'motion_parse Some(MotionCmd(count, Motion::WholeLine)),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
match ch {
|
||||||
|
'g' => {
|
||||||
|
if let Some(ch) = chars_clone.peek() {
|
||||||
|
match ch {
|
||||||
|
'g' => {
|
||||||
|
chars_clone.next();
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfBuffer))
|
||||||
|
}
|
||||||
|
'e' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::BackwardWord(To::End, Word::Normal)));
|
||||||
|
}
|
||||||
|
'E' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::BackwardWord(To::End, Word::Big)));
|
||||||
|
}
|
||||||
|
_ => return self.quit_parse()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break 'motion_parse None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'|' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(1, Motion::ToColumn(count)));
|
||||||
|
}
|
||||||
|
'0' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfLine));
|
||||||
|
}
|
||||||
|
'$' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::EndOfLine));
|
||||||
|
}
|
||||||
|
'k' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::LineUp));
|
||||||
|
}
|
||||||
|
'j' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::LineDown));
|
||||||
|
}
|
||||||
|
'h' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::BackwardChar));
|
||||||
|
}
|
||||||
|
'l' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::ForwardChar));
|
||||||
|
}
|
||||||
|
'w' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::Start, Word::Normal)));
|
||||||
|
}
|
||||||
|
'W' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::Start, Word::Big)));
|
||||||
|
}
|
||||||
|
'e' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::End, Word::Normal)));
|
||||||
|
}
|
||||||
|
'E' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::End, Word::Big)));
|
||||||
|
}
|
||||||
|
'b' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::BackwardWord(To::Start, Word::Normal)));
|
||||||
|
}
|
||||||
|
'B' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::BackwardWord(To::Start, Word::Big)));
|
||||||
|
}
|
||||||
|
ch if ch == 'i' || ch == 'a' => {
|
||||||
|
let bound = match ch {
|
||||||
|
'i' => Bound::Inside,
|
||||||
|
'a' => Bound::Around,
|
||||||
|
_ => unreachable!()
|
||||||
|
};
|
||||||
|
if chars_clone.peek().is_none() {
|
||||||
|
break 'motion_parse None
|
||||||
|
}
|
||||||
|
let obj = match chars_clone.next().unwrap() {
|
||||||
|
'w' => TextObj::Word(Word::Normal),
|
||||||
|
'W' => TextObj::Word(Word::Big),
|
||||||
|
'"' => TextObj::DoubleQuote,
|
||||||
|
'\'' => TextObj::SingleQuote,
|
||||||
|
'(' | ')' | 'b' => TextObj::Paren,
|
||||||
|
'{' | '}' | 'B' => TextObj::Brace,
|
||||||
|
'[' | ']' => TextObj::Bracket,
|
||||||
|
'<' | '>' => TextObj::Angle,
|
||||||
|
_ => return self.quit_parse()
|
||||||
|
};
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::TextObj(obj, bound)))
|
||||||
|
}
|
||||||
|
_ => return self.quit_parse(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if chars.peek().is_some() {
|
||||||
|
flog!(WARN, "Unused characters in Vi command parse!");
|
||||||
|
flog!(WARN, "{:?}",chars)
|
||||||
|
}
|
||||||
|
|
||||||
|
let verb_ref = verb.as_ref().map(|v| &v.1);
|
||||||
|
let motion_ref = motion.as_ref().map(|m| &m.1);
|
||||||
|
|
||||||
|
match self.validate_combination(verb_ref, motion_ref) {
|
||||||
|
CmdState::Complete => {
|
||||||
|
let cmd = Some(
|
||||||
|
ViCmd {
|
||||||
|
register,
|
||||||
|
verb,
|
||||||
|
motion,
|
||||||
|
raw_seq: std::mem::take(&mut self.pending_seq)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
flog!(DEBUG, cmd);
|
||||||
|
cmd
|
||||||
|
}
|
||||||
|
CmdState::Pending => {
|
||||||
|
flog!(DEBUG, "pending sequence: {}", self.pending_seq);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
CmdState::Invalid => {
|
||||||
|
flog!(DEBUG, "invalid sequence: {}",self.pending_seq);
|
||||||
|
self.pending_seq.clear();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ViMode for ViNormal {
|
impl ViMode for ViNormal {
|
||||||
fn handle_key(&mut self, key: E) -> Option<ViCmd> {
|
fn handle_key(&mut self, key: E) -> Option<ViCmd> {
|
||||||
if let E(K::Char(ch),M::NONE) = key {
|
flog!(DEBUG, key);
|
||||||
self.pending_cmd.append_seq_char(ch);
|
|
||||||
}
|
|
||||||
if self.pending_cmd.is_building() {
|
|
||||||
return self.handle_pending_builder(key)
|
|
||||||
}
|
|
||||||
match key {
|
match key {
|
||||||
E(K::Char(digit @ '0'..='9'), M::NONE) => self.pending_cmd.append_digit(digit),
|
E(K::Char(ch), M::NONE) => self.try_parse(ch),
|
||||||
E(K::Char('"'),M::NONE) => {
|
E(K::Char('R'), M::CTRL) => {
|
||||||
if self.pending_cmd.is_empty() {
|
let mut chars = self.pending_seq.chars().peekable();
|
||||||
if self.pending_cmd.register().name().is_none() {
|
let count = self.parse_count(&mut chars).unwrap_or(1);
|
||||||
self.pending_cmd.wants_register = true;
|
Some(
|
||||||
} else {
|
ViCmd {
|
||||||
self.clear_cmd();
|
register: RegisterName::default(),
|
||||||
|
verb: Some(VerbCmd(count,Verb::Redo)),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd()
|
||||||
}
|
}
|
||||||
} else {
|
)
|
||||||
|
}
|
||||||
|
E(K::Esc, M::NONE) => {
|
||||||
|
self.clear_cmd();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if let Some(cmd) = common_cmds(key) {
|
||||||
self.clear_cmd();
|
self.clear_cmd();
|
||||||
}
|
Some(cmd)
|
||||||
return None
|
} else {
|
||||||
}
|
None
|
||||||
E(K::Char('i'),M::NONE) if self.pending_cmd.verb().is_some() => {
|
|
||||||
self.pending_cmd.set_motion(Motion::Builder(MotionBuilder::TextObj(None, Some(Bound::Inside))));
|
|
||||||
}
|
|
||||||
E(K::Char('a'),M::NONE) if self.pending_cmd.verb().is_some() => {
|
|
||||||
self.pending_cmd.set_motion(Motion::Builder(MotionBuilder::TextObj(None, Some(Bound::Around))));
|
|
||||||
}
|
|
||||||
E(K::Char('h'),M::NONE) => self.pending_cmd.set_motion(Motion::BackwardChar),
|
|
||||||
E(K::Char('j'),M::NONE) => self.pending_cmd.set_motion(Motion::LineDown),
|
|
||||||
E(K::Char('k'),M::NONE) => self.pending_cmd.set_motion(Motion::LineUp),
|
|
||||||
E(K::Char('l'),M::NONE) => self.pending_cmd.set_motion(Motion::ForwardChar),
|
|
||||||
E(K::Char('w'),M::NONE) => self.pending_cmd.set_motion(Motion::ForwardWord(To::Start, Word::Normal)),
|
|
||||||
E(K::Char('W'),M::NONE) => self.pending_cmd.set_motion(Motion::ForwardWord(To::Start, Word::Big)),
|
|
||||||
E(K::Char('e'),M::NONE) => self.pending_cmd.set_motion(Motion::ForwardWord(To::End, Word::Normal)),
|
|
||||||
E(K::Char('E'),M::NONE) => self.pending_cmd.set_motion(Motion::ForwardWord(To::End, Word::Big)),
|
|
||||||
E(K::Char('b'),M::NONE) => self.pending_cmd.set_motion(Motion::BackwardWord(Word::Normal)),
|
|
||||||
E(K::Char('B'),M::NONE) => self.pending_cmd.set_motion(Motion::BackwardWord(Word::Big)),
|
|
||||||
E(K::Char('x'),M::NONE) => self.pending_cmd.set_verb(Verb::DeleteChar(Anchor::After)),
|
|
||||||
E(K::Char('X'),M::NONE) => self.pending_cmd.set_verb(Verb::DeleteChar(Anchor::Before)),
|
|
||||||
E(K::Char('d'),M::NONE) => {
|
|
||||||
if self.pending_cmd.verb().is_none() {
|
|
||||||
self.pending_cmd.set_verb(Verb::Delete)
|
|
||||||
} else if let Some(verb) = self.pending_cmd.verb() {
|
|
||||||
if verb == &Verb::Delete {
|
|
||||||
self.pending_cmd.set_motion(Motion::WholeLine);
|
|
||||||
} else {
|
|
||||||
self.clear_cmd();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
E(K::Char('c'),M::NONE) => {
|
|
||||||
if self.pending_cmd.verb().is_none() {
|
|
||||||
self.pending_cmd.set_verb(Verb::Change)
|
|
||||||
} else if let Some(verb) = self.pending_cmd.verb() {
|
|
||||||
if verb == &Verb::Change {
|
|
||||||
self.pending_cmd.set_motion(Motion::WholeLine);
|
|
||||||
} else {
|
|
||||||
self.clear_cmd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
E(K::Char('y'),M::NONE) => {
|
|
||||||
if self.pending_cmd.verb().is_none() {
|
|
||||||
self.pending_cmd.set_verb(Verb::Yank)
|
|
||||||
} else if let Some(verb) = self.pending_cmd.verb() {
|
|
||||||
if verb == &Verb::Yank {
|
|
||||||
self.pending_cmd.set_motion(Motion::WholeLine);
|
|
||||||
} else {
|
|
||||||
self.clear_cmd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
E(K::Char('p'),M::NONE) => self.pending_cmd.set_verb(Verb::Put(Anchor::After)),
|
|
||||||
E(K::Char('P'),M::NONE) => self.pending_cmd.set_verb(Verb::Put(Anchor::Before)),
|
|
||||||
E(K::Char('D'),M::NONE) => {
|
|
||||||
self.pending_cmd.set_verb(Verb::Delete);
|
|
||||||
self.pending_cmd.set_motion(Motion::EndOfLine);
|
|
||||||
}
|
|
||||||
E(K::Char('f'),M::NONE) => {
|
|
||||||
let builder = MotionBuilder::CharSearch(
|
|
||||||
Some(Direction::Forward),
|
|
||||||
Some(Dest::On),
|
|
||||||
None
|
|
||||||
);
|
|
||||||
self.pending_cmd.set_motion(Motion::Builder(builder));
|
|
||||||
}
|
|
||||||
E(K::Char('F'),M::NONE) => {
|
|
||||||
let builder = MotionBuilder::CharSearch(
|
|
||||||
Some(Direction::Backward),
|
|
||||||
Some(Dest::On),
|
|
||||||
None
|
|
||||||
);
|
|
||||||
self.pending_cmd.set_motion(Motion::Builder(builder));
|
|
||||||
}
|
|
||||||
E(K::Char('t'),M::NONE) => {
|
|
||||||
let builder = MotionBuilder::CharSearch(
|
|
||||||
Some(Direction::Forward),
|
|
||||||
Some(Dest::Before),
|
|
||||||
None
|
|
||||||
);
|
|
||||||
self.pending_cmd.set_motion(Motion::Builder(builder));
|
|
||||||
}
|
|
||||||
E(K::Char('T'),M::NONE) => {
|
|
||||||
let builder = MotionBuilder::CharSearch(
|
|
||||||
Some(Direction::Backward),
|
|
||||||
Some(Dest::Before),
|
|
||||||
None
|
|
||||||
);
|
|
||||||
self.pending_cmd.set_motion(Motion::Builder(builder));
|
|
||||||
}
|
|
||||||
E(K::Char('i'),M::NONE) => {
|
|
||||||
self.pending_cmd.set_verb(Verb::InsertMode);
|
|
||||||
}
|
|
||||||
E(K::Char('I'),M::NONE) => {
|
|
||||||
self.pending_cmd.set_verb(Verb::InsertMode);
|
|
||||||
self.pending_cmd.set_motion(Motion::BeginningOfFirstWord);
|
|
||||||
}
|
|
||||||
E(K::Char('a'),M::NONE) => {
|
|
||||||
self.pending_cmd.set_verb(Verb::InsertMode);
|
|
||||||
self.pending_cmd.set_motion(Motion::ForwardChar);
|
|
||||||
}
|
|
||||||
E(K::Char('A'),M::NONE) => {
|
|
||||||
self.pending_cmd.set_verb(Verb::InsertMode);
|
|
||||||
self.pending_cmd.set_motion(Motion::EndOfLine);
|
|
||||||
}
|
|
||||||
_ => return common_cmds(key)
|
|
||||||
}
|
|
||||||
if self.pending_cmd.is_complete() {
|
|
||||||
Some(self.take_cmd())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,28 +539,26 @@ impl ViMode for ViNormal {
|
|||||||
fn cursor_style(&self) -> String {
|
fn cursor_style(&self) -> String {
|
||||||
"\x1b[2 q".to_string()
|
"\x1b[2 q".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn pending_seq(&self) -> Option<String> {
|
||||||
|
Some(self.pending_seq.clone())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn common_cmds(key: E) -> Option<ViCmd> {
|
pub fn common_cmds(key: E) -> Option<ViCmd> {
|
||||||
let mut pending_cmd = ViCmd::new();
|
let mut pending_cmd = ViCmd::new();
|
||||||
match key {
|
match key {
|
||||||
E(K::Home, M::NONE) => pending_cmd.set_motion(Motion::BeginningOfLine),
|
E(K::Home, M::NONE) => pending_cmd.set_motion(MotionCmd(1,Motion::BeginningOfLine)),
|
||||||
E(K::End, M::NONE) => pending_cmd.set_motion(Motion::EndOfLine),
|
E(K::End, M::NONE) => pending_cmd.set_motion(MotionCmd(1,Motion::EndOfLine)),
|
||||||
E(K::Left, M::NONE) => pending_cmd.set_motion(Motion::BackwardChar),
|
E(K::Left, M::NONE) => pending_cmd.set_motion(MotionCmd(1,Motion::BackwardChar)),
|
||||||
E(K::Right, M::NONE) => pending_cmd.set_motion(Motion::ForwardChar),
|
E(K::Right, M::NONE) => pending_cmd.set_motion(MotionCmd(1,Motion::ForwardChar)),
|
||||||
E(K::Up, M::NONE) => pending_cmd.set_motion(Motion::LineUp),
|
E(K::Up, M::NONE) => pending_cmd.set_motion(MotionCmd(1,Motion::LineUp)),
|
||||||
E(K::Down, M::NONE) => pending_cmd.set_motion(Motion::LineDown),
|
E(K::Down, M::NONE) => pending_cmd.set_motion(MotionCmd(1,Motion::LineDown)),
|
||||||
E(K::Enter, M::NONE) => pending_cmd.set_verb(Verb::AcceptLine),
|
E(K::Enter, M::NONE) => pending_cmd.set_verb(VerbCmd(1,Verb::AcceptLine)),
|
||||||
E(K::Char('D'), M::CTRL) => pending_cmd.set_verb(Verb::EndOfFile),
|
E(K::Char('D'), M::CTRL) => pending_cmd.set_verb(VerbCmd(1,Verb::EndOfFile)),
|
||||||
|
E(K::Delete, M::NONE) => pending_cmd.set_verb(VerbCmd(1,Verb::DeleteChar(Anchor::After))),
|
||||||
E(K::Backspace, M::NONE) |
|
E(K::Backspace, M::NONE) |
|
||||||
E(K::Char('H'), M::CTRL) => {
|
E(K::Char('H'), M::CTRL) => pending_cmd.set_verb(VerbCmd(1,Verb::DeleteChar(Anchor::Before))),
|
||||||
pending_cmd.set_verb(Verb::Delete);
|
|
||||||
pending_cmd.set_motion(Motion::BackwardChar);
|
|
||||||
}
|
|
||||||
E(K::Delete, M::NONE) => {
|
|
||||||
pending_cmd.set_verb(Verb::Delete);
|
|
||||||
pending_cmd.set_motion(Motion::ForwardChar);
|
|
||||||
}
|
|
||||||
_ => return None
|
_ => return None
|
||||||
}
|
}
|
||||||
Some(pending_cmd)
|
Some(pending_cmd)
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
use std::{os::fd::{BorrowedFd, RawFd}, thread::sleep, time::{Duration, Instant}};
|
use std::{os::fd::{BorrowedFd, RawFd}, thread::sleep, time::{Duration, Instant}};
|
||||||
use nix::{errno::Errno, fcntl::{fcntl, FcntlArg, OFlag}, libc::STDIN_FILENO, sys::termios, unistd::{isatty, read, write}};
|
use nix::{errno::Errno, fcntl::{fcntl, FcntlArg, OFlag}, libc::{self, STDIN_FILENO}, sys::termios, unistd::{isatty, read, write}};
|
||||||
|
use nix::libc::{winsize, TIOCGWINSZ};
|
||||||
|
use std::mem::zeroed;
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
use crate::libsh::error::ShResult;
|
||||||
|
|
||||||
use super::keys::{KeyCode, KeyEvent, ModKeys};
|
use super::keys::{KeyCode, KeyEvent, ModKeys};
|
||||||
|
|
||||||
@@ -32,6 +37,34 @@ impl Terminal {
|
|||||||
.expect("Failed to restore terminal settings");
|
.expect("Failed to restore terminal settings");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn get_dimensions(&self) -> ShResult<(usize, usize)> {
|
||||||
|
if !isatty(self.stdin).unwrap_or(false) {
|
||||||
|
return Err(io::Error::new(io::ErrorKind::Other, "Not a TTY"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ws: winsize = unsafe { zeroed() };
|
||||||
|
|
||||||
|
let res = unsafe { libc::ioctl(self.stdin, TIOCGWINSZ, &mut ws) };
|
||||||
|
if res == -1 {
|
||||||
|
return Err(io::Error::last_os_error())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((ws.ws_row as usize, ws.ws_col as usize))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_cursor_pos(&self) {
|
||||||
|
self.write("\x1b[s")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn restore_cursor_pos(&self) {
|
||||||
|
self.write("\x1b[u")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_cursor_to(&self, (row,col): (usize,usize)) {
|
||||||
|
self.write(&format!("\x1b[{row};{col}H",))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_raw_mode<F: FnOnce() -> R, R>(func: F) -> R {
|
pub fn with_raw_mode<F: FnOnce() -> R, R>(func: F) -> R {
|
||||||
let saved = Self::raw_mode();
|
let saved = Self::raw_mode();
|
||||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(func));
|
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(func));
|
||||||
@@ -48,6 +81,29 @@ impl Terminal {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn read_blocks_then_read(&self, buf: &mut [u8], timeout: Duration) -> Option<usize> {
|
||||||
|
Self::with_raw_mode(|| {
|
||||||
|
self.read_blocks(false);
|
||||||
|
let start = Instant::now();
|
||||||
|
loop {
|
||||||
|
match read(self.stdin, buf) {
|
||||||
|
Ok(n) if n > 0 => {
|
||||||
|
self.read_blocks(true);
|
||||||
|
return Some(n);
|
||||||
|
}
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) if e == Errno::EAGAIN => {}
|
||||||
|
Err(_) => return None,
|
||||||
|
}
|
||||||
|
if start.elapsed() > timeout {
|
||||||
|
self.read_blocks(true);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
sleep(Duration::from_millis(1));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Same as read_byte(), only non-blocking with a very short timeout
|
/// Same as read_byte(), only non-blocking with a very short timeout
|
||||||
pub fn peek_byte(&self, buf: &mut [u8]) -> usize {
|
pub fn peek_byte(&self, buf: &mut [u8]) -> usize {
|
||||||
const TIMEOUT_DUR: Duration = Duration::from_millis(50);
|
const TIMEOUT_DUR: Duration = Duration::from_millis(50);
|
||||||
|
|||||||
@@ -1,18 +1,35 @@
|
|||||||
use super::{linebuf::{TermChar, TermCharBuf}, register::{append_register, read_register, write_register}};
|
use super::{linebuf::{TermChar, TermCharBuf}, register::{append_register, read_register, write_register}};
|
||||||
|
|
||||||
#[derive(Clone,Copy,Default,Debug)]
|
#[derive(Clone,Copy,Debug)]
|
||||||
pub struct RegisterName {
|
pub struct RegisterName {
|
||||||
name: Option<char>,
|
name: Option<char>,
|
||||||
|
count: usize,
|
||||||
append: bool
|
append: bool
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RegisterName {
|
impl RegisterName {
|
||||||
|
pub fn new(name: Option<char>, count: Option<usize>) -> Self {
|
||||||
|
let Some(ch) = name else {
|
||||||
|
return Self::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let append = ch.is_uppercase();
|
||||||
|
let name = ch.to_ascii_lowercase();
|
||||||
|
Self {
|
||||||
|
name: Some(name),
|
||||||
|
count: count.unwrap_or(1),
|
||||||
|
append
|
||||||
|
}
|
||||||
|
}
|
||||||
pub fn name(&self) -> Option<char> {
|
pub fn name(&self) -> Option<char> {
|
||||||
self.name
|
self.name
|
||||||
}
|
}
|
||||||
pub fn is_append(&self) -> bool {
|
pub fn is_append(&self) -> bool {
|
||||||
self.append
|
self.append
|
||||||
}
|
}
|
||||||
|
pub fn count(&self) -> usize {
|
||||||
|
self.count
|
||||||
|
}
|
||||||
pub fn write_to_register(&self, buf: TermCharBuf) {
|
pub fn write_to_register(&self, buf: TermCharBuf) {
|
||||||
if self.append {
|
if self.append {
|
||||||
append_register(self.name, buf);
|
append_register(self.name, buf);
|
||||||
@@ -25,29 +42,21 @@ impl RegisterName {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for RegisterName {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
name: None,
|
||||||
|
count: 1,
|
||||||
|
append: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone,Default,Debug)]
|
#[derive(Clone,Default,Debug)]
|
||||||
pub struct ViCmd {
|
pub struct ViCmd {
|
||||||
pub wants_register: bool, // Waiting for register character
|
|
||||||
|
|
||||||
/// Register to read from/write to
|
|
||||||
pub register_count: Option<u16>,
|
|
||||||
pub register: RegisterName,
|
pub register: RegisterName,
|
||||||
|
pub verb: Option<VerbCmd>,
|
||||||
/// Verb to perform
|
pub motion: Option<MotionCmd>,
|
||||||
pub verb_count: Option<u16>,
|
|
||||||
pub verb: Option<Verb>,
|
|
||||||
|
|
||||||
/// Motion to perform
|
|
||||||
pub motion_count: Option<u16>,
|
|
||||||
pub motion: Option<Motion>,
|
|
||||||
|
|
||||||
/// Count digits are held here until we know what we are counting
|
|
||||||
/// Once a register/verb/motion is set, the count is taken from here
|
|
||||||
pub pending_count: Option<u16>,
|
|
||||||
|
|
||||||
/// The actual keys the user typed for this command
|
|
||||||
/// Maybe display this somewhere around the prompt later?
|
|
||||||
/// Prompt escape sequence maybe?
|
|
||||||
pub raw_seq: String,
|
pub raw_seq: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,79 +64,54 @@ impl ViCmd {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
pub fn set_register(&mut self, register: char) {
|
pub fn set_motion(&mut self, motion: MotionCmd) {
|
||||||
let append = register.is_uppercase();
|
self.motion = Some(motion)
|
||||||
let name = Some(register.to_ascii_lowercase());
|
|
||||||
let reg_name = RegisterName { name, append };
|
|
||||||
self.register = reg_name;
|
|
||||||
self.register_count = self.pending_count.take();
|
|
||||||
self.wants_register = false;
|
|
||||||
}
|
}
|
||||||
pub fn append_seq_char(&mut self, ch: char) {
|
pub fn set_verb(&mut self, verb: VerbCmd) {
|
||||||
self.raw_seq.push(ch)
|
self.verb = Some(verb)
|
||||||
}
|
}
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn verb(&self) -> Option<&VerbCmd> {
|
||||||
!self.wants_register &&
|
|
||||||
self.register.name.is_none() &&
|
|
||||||
self.verb_count.is_none() &&
|
|
||||||
self.verb.is_none() &&
|
|
||||||
self.motion_count.is_none() &&
|
|
||||||
self.motion.is_none()
|
|
||||||
}
|
|
||||||
pub fn set_verb(&mut self, verb: Verb) {
|
|
||||||
self.verb = Some(verb);
|
|
||||||
self.verb_count = self.pending_count.take();
|
|
||||||
}
|
|
||||||
pub fn set_motion(&mut self, motion: Motion) {
|
|
||||||
self.motion = Some(motion);
|
|
||||||
self.motion_count = self.pending_count.take();
|
|
||||||
}
|
|
||||||
pub fn register(&self) -> RegisterName {
|
|
||||||
self.register
|
|
||||||
}
|
|
||||||
pub fn verb(&self) -> Option<&Verb> {
|
|
||||||
self.verb.as_ref()
|
self.verb.as_ref()
|
||||||
}
|
}
|
||||||
pub fn verb_count(&self) -> u16 {
|
pub fn motion(&self) -> Option<&MotionCmd> {
|
||||||
self.verb_count.unwrap_or(1)
|
|
||||||
}
|
|
||||||
pub fn motion(&self) -> Option<&Motion> {
|
|
||||||
self.motion.as_ref()
|
self.motion.as_ref()
|
||||||
}
|
}
|
||||||
pub fn motion_count(&self) -> u16 {
|
pub fn verb_count(&self) -> usize {
|
||||||
self.motion_count.unwrap_or(1)
|
self.verb.as_ref().map(|v| v.0).unwrap_or(1)
|
||||||
}
|
}
|
||||||
pub fn append_digit(&mut self, digit: char) {
|
pub fn motion_count(&self) -> usize {
|
||||||
// Convert char digit to a number (assuming ASCII '0'..'9')
|
self.motion.as_ref().map(|m| m.0).unwrap_or(1)
|
||||||
let digit_val = digit.to_digit(10).expect("digit must be 0-9") as u16;
|
|
||||||
self.pending_count = Some(match self.pending_count {
|
|
||||||
Some(count) => count * 10 + digit_val,
|
|
||||||
None => digit_val,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
pub fn is_building(&self) -> bool {
|
pub fn is_cmd_repeat(&self) -> bool {
|
||||||
matches!(self.verb, Some(Verb::Builder(_))) ||
|
self.verb.as_ref().is_some_and(|v| matches!(v.1,Verb::RepeatLast))
|
||||||
matches!(self.motion, Some(Motion::Builder(_))) ||
|
|
||||||
self.wants_register
|
|
||||||
}
|
}
|
||||||
pub fn is_complete(&self) -> bool {
|
pub fn is_motion_repeat(&self) -> bool {
|
||||||
!(
|
self.verb.as_ref().is_some_and(|v| matches!(v.1,Verb::RepeatMotion | Verb::RepeatMotionRev))
|
||||||
(self.verb.is_none() && self.motion.is_none()) ||
|
|
||||||
(self.verb.is_none() && self.motion.as_ref().is_some_and(|m| m.needs_verb())) ||
|
|
||||||
(self.motion.is_none() && self.verb.as_ref().is_some_and(|v| v.needs_motion())) ||
|
|
||||||
self.is_building()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
pub fn should_submit(&self) -> bool {
|
pub fn should_submit(&self) -> bool {
|
||||||
self.verb.as_ref().is_some_and(|v| *v == Verb::AcceptLine)
|
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::AcceptLine))
|
||||||
|
}
|
||||||
|
pub fn is_undo_op(&self) -> bool {
|
||||||
|
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::Undo | Verb::Redo))
|
||||||
}
|
}
|
||||||
pub fn is_mode_transition(&self) -> bool {
|
pub fn is_mode_transition(&self) -> bool {
|
||||||
self.verb.as_ref().is_some_and(|v| {
|
self.verb.as_ref().is_some_and(|v| {
|
||||||
matches!(*v, Verb::InsertMode | Verb::NormalMode | Verb::OverwriteMode | Verb::VisualMode)
|
matches!(v.1,
|
||||||
|
Verb::InsertMode |
|
||||||
|
Verb::InsertModeLineBreak(_) |
|
||||||
|
Verb::NormalMode |
|
||||||
|
Verb::VisualMode |
|
||||||
|
Verb::OverwriteMode
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone,Debug)]
|
||||||
|
pub struct VerbCmd(pub usize,pub Verb);
|
||||||
|
#[derive(Clone,Debug)]
|
||||||
|
pub struct MotionCmd(pub usize,pub Motion);
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum Verb {
|
pub enum Verb {
|
||||||
@@ -141,10 +125,14 @@ pub enum Verb {
|
|||||||
Complete,
|
Complete,
|
||||||
CompleteBackward,
|
CompleteBackward,
|
||||||
Undo,
|
Undo,
|
||||||
|
Redo,
|
||||||
RepeatLast,
|
RepeatLast,
|
||||||
|
RepeatMotion,
|
||||||
|
RepeatMotionRev,
|
||||||
Put(Anchor),
|
Put(Anchor),
|
||||||
OverwriteMode,
|
OverwriteMode,
|
||||||
InsertMode,
|
InsertMode,
|
||||||
|
InsertModeLineBreak(Anchor),
|
||||||
NormalMode,
|
NormalMode,
|
||||||
VisualMode,
|
VisualMode,
|
||||||
JoinLines,
|
JoinLines,
|
||||||
@@ -153,6 +141,7 @@ pub enum Verb {
|
|||||||
Breakline(Anchor),
|
Breakline(Anchor),
|
||||||
Indent,
|
Indent,
|
||||||
Dedent,
|
Dedent,
|
||||||
|
Equalize,
|
||||||
AcceptLine,
|
AcceptLine,
|
||||||
Builder(VerbBuilder),
|
Builder(VerbBuilder),
|
||||||
EndOfFile
|
EndOfFile
|
||||||
@@ -172,6 +161,28 @@ impl Verb {
|
|||||||
Self::Yank
|
Self::Yank
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
pub fn is_edit(&self) -> bool {
|
||||||
|
matches!(self,
|
||||||
|
Self::Delete |
|
||||||
|
Self::DeleteChar(_) |
|
||||||
|
Self::Change |
|
||||||
|
Self::ReplaceChar(_) |
|
||||||
|
Self::Substitute |
|
||||||
|
Self::ToggleCase |
|
||||||
|
Self::RepeatLast |
|
||||||
|
Self::Put(_) |
|
||||||
|
Self::OverwriteMode |
|
||||||
|
Self::InsertModeLineBreak(_) |
|
||||||
|
Self::JoinLines |
|
||||||
|
Self::InsertChar(_) |
|
||||||
|
Self::Insert(_) |
|
||||||
|
Self::Breakline(_) |
|
||||||
|
Self::EndOfFile
|
||||||
|
)
|
||||||
|
}
|
||||||
|
pub fn is_char_insert(&self) -> bool {
|
||||||
|
matches!(self, Self::InsertChar(_))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
@@ -185,7 +196,7 @@ pub enum Motion {
|
|||||||
/// end-of-line
|
/// end-of-line
|
||||||
EndOfLine,
|
EndOfLine,
|
||||||
/// backward-word, vi-prev-word
|
/// backward-word, vi-prev-word
|
||||||
BackwardWord(Word), // Backward until start of word
|
BackwardWord(To, Word), // Backward until start of word
|
||||||
/// forward-word, vi-end-word, vi-next-word
|
/// forward-word, vi-end-word, vi-next-word
|
||||||
ForwardWord(To, Word), // Forward until start/end of word
|
ForwardWord(To, Word), // Forward until start/end of word
|
||||||
/// character-search, character-search-backward, vi-char-search
|
/// character-search, character-search-backward, vi-char-search
|
||||||
@@ -204,6 +215,8 @@ pub enum Motion {
|
|||||||
BeginningOfBuffer,
|
BeginningOfBuffer,
|
||||||
/// end-of-register
|
/// end-of-register
|
||||||
EndOfBuffer,
|
EndOfBuffer,
|
||||||
|
ToColumn(usize),
|
||||||
|
Range(usize,usize),
|
||||||
Builder(MotionBuilder),
|
Builder(MotionBuilder),
|
||||||
Null
|
Null
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user