further work on implementing vi features

This commit is contained in:
2025-05-22 03:36:27 -04:00
parent 47759a05d4
commit ac170d31c8
13 changed files with 1871 additions and 1624 deletions

7
Cargo.lock generated
View File

@@ -160,6 +160,7 @@ dependencies = [
"nix", "nix",
"pretty_assertions", "pretty_assertions",
"regex", "regex",
"unicode-segmentation",
"unicode-width", "unicode-width",
] ]
@@ -336,6 +337,12 @@ version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.2.0" version = "0.2.0"

View File

@@ -17,6 +17,7 @@ insta = "1.42.2"
nix = { version = "0.29.0", features = ["uio", "term", "user", "hostname", "fs", "default", "signal", "process", "event", "ioctl"] } nix = { version = "0.29.0", features = ["uio", "term", "user", "hostname", "fs", "default", "signal", "process", "event", "ioctl"] }
pretty_assertions = "1.4.1" pretty_assertions = "1.4.1"
regex = "1.11.1" regex = "1.11.1"
unicode-segmentation = "1.12.0"
unicode-width = "0.2.0" unicode-width = "0.2.0"
[[bin]] [[bin]]

View File

@@ -3,7 +3,7 @@ pub mod highlight;
use std::path::Path; use std::path::Path;
use readline::FernReader; use readline::FernVi;
use crate::{expand::expand_prompt, libsh::error::ShResult, prelude::*, state::read_shopts}; use crate::{expand::expand_prompt, libsh::error::ShResult, prelude::*, state::read_shopts};
@@ -22,6 +22,6 @@ fn get_prompt() -> ShResult<String> {
pub fn read_line() -> ShResult<String> { pub fn read_line() -> ShResult<String> {
let prompt = get_prompt()?; let prompt = get_prompt()?;
let mut reader = FernReader::new(prompt); let mut reader = FernVi::new(Some(prompt));
reader.readline() reader.readline()
} }

View File

@@ -1,31 +1,46 @@
use std::sync::Arc;
use unicode_segmentation::UnicodeSegmentation;
// Credit to Rustyline for the design ideas in this module // Credit to Rustyline for the design ideas in this module
// https://github.com/kkawakam/rustyline // https://github.com/kkawakam/rustyline
#[derive(Clone,Debug)] #[derive(Clone,Debug)]
pub struct KeyEvent(pub KeyCode, pub ModKeys); pub struct KeyEvent(pub KeyCode, pub ModKeys);
impl KeyEvent { impl KeyEvent {
pub fn new(ch: char, mut mods: ModKeys) -> Self { pub fn new(ch: &str, mut mods: ModKeys) -> Self {
use {KeyCode as K, KeyEvent as E, ModKeys as M}; use {KeyCode as K, KeyEvent as E, ModKeys as M};
if !ch.is_control() { let mut graphemes = ch.graphemes(true);
if !mods.is_empty() {
mods.remove(M::SHIFT); // TODO Validate: no SHIFT even if let first = match graphemes.next() {
// `c` is uppercase Some(g) => g,
None => return E(K::Null, mods),
};
// If more than one grapheme, it's not a single key event
if graphemes.next().is_some() {
return E(K::Null, mods); // Or panic, or wrap in Grapheme if desired
} }
return E(K::Char(ch), mods);
} let mut chars = first.chars();
match ch {
'\x00' => E(K::Char('@'), mods | M::CTRL), // '\0' let single_char = chars.next();
let is_single_char = chars.next().is_none();
match single_char {
Some(c) if is_single_char && c.is_control() => {
match c {
'\x00' => E(K::Char('@'), mods | M::CTRL),
'\x01' => E(K::Char('A'), mods | M::CTRL), '\x01' => E(K::Char('A'), mods | M::CTRL),
'\x02' => E(K::Char('B'), mods | M::CTRL), '\x02' => E(K::Char('B'), mods | M::CTRL),
'\x03' => E(K::Char('C'), mods | M::CTRL), '\x03' => E(K::Char('C'), mods | M::CTRL),
'\x04' => E(K::Char('D'), mods | M::CTRL), '\x04' => E(K::Char('D'), mods | M::CTRL),
'\x05' => E(K::Char('E'), mods | M::CTRL), '\x05' => E(K::Char('E'), mods | M::CTRL),
'\x06' => E(K::Char('F'), mods | M::CTRL), '\x06' => E(K::Char('F'), mods | M::CTRL),
'\x07' => E(K::Char('G'), mods | M::CTRL), // '\a' '\x07' => E(K::Char('G'), mods | M::CTRL),
'\x08' => E(K::Backspace, mods), // '\b' '\x08' => E(K::Backspace, mods),
'\x09' => { '\x09' => {
// '\t'
if mods.contains(M::SHIFT) { if mods.contains(M::SHIFT) {
mods.remove(M::SHIFT); mods.remove(M::SHIFT);
E(K::BackTab, mods) E(K::BackTab, mods)
@@ -33,10 +48,10 @@ impl KeyEvent {
E(K::Tab, mods) E(K::Tab, mods)
} }
} }
'\x0a' => E(K::Char('J'), mods | M::CTRL), // '\n' (10) '\x0a' => E(K::Char('J'), mods | M::CTRL),
'\x0b' => E(K::Char('K'), mods | M::CTRL), '\x0b' => E(K::Char('K'), mods | M::CTRL),
'\x0c' => E(K::Char('L'), mods | M::CTRL), '\x0c' => E(K::Char('L'), mods | M::CTRL),
'\x0d' => E(K::Enter, mods), // '\r' (13) '\x0d' => E(K::Enter, mods),
'\x0e' => E(K::Char('N'), mods | M::CTRL), '\x0e' => E(K::Char('N'), mods | M::CTRL),
'\x0f' => E(K::Char('O'), mods | M::CTRL), '\x0f' => E(K::Char('O'), mods | M::CTRL),
'\x10' => E(K::Char('P'), mods | M::CTRL), '\x10' => E(K::Char('P'), mods | M::CTRL),
@@ -50,16 +65,31 @@ impl KeyEvent {
'\x18' => E(K::Char('X'), mods | M::CTRL), '\x18' => E(K::Char('X'), mods | M::CTRL),
'\x19' => E(K::Char('Y'), mods | M::CTRL), '\x19' => E(K::Char('Y'), mods | M::CTRL),
'\x1a' => E(K::Char('Z'), mods | M::CTRL), '\x1a' => E(K::Char('Z'), mods | M::CTRL),
'\x1b' => E(K::Esc, mods), // Ctrl-[, '\e' '\x1b' => E(K::Esc, mods),
'\x1c' => E(K::Char('\\'), mods | M::CTRL), '\x1c' => E(K::Char('\\'), mods | M::CTRL),
'\x1d' => E(K::Char(']'), mods | M::CTRL), '\x1d' => E(K::Char(']'), mods | M::CTRL),
'\x1e' => E(K::Char('^'), mods | M::CTRL), '\x1e' => E(K::Char('^'), mods | M::CTRL),
'\x1f' => E(K::Char('_'), mods | M::CTRL), '\x1f' => E(K::Char('_'), mods | M::CTRL),
'\x7f' => E(K::Backspace, mods), // Rubout, Ctrl-? '\x7f' => E(K::Backspace, mods),
'\u{9b}' => E(K::Esc, mods | M::SHIFT), '\u{9b}' => E(K::Esc, mods | M::SHIFT),
_ => E(K::Null, mods), _ => E(K::Null, mods),
} }
} }
Some(c) if is_single_char => {
if !mods.is_empty() {
mods.remove(M::SHIFT);
}
E(K::Char(c), mods)
}
_ => {
// multi-char grapheme (emoji, accented, etc)
if !mods.is_empty() {
mods.remove(M::SHIFT);
}
E(K::Grapheme(Arc::from(first)), mods)
}
}
}
} }
#[derive(Clone,Debug)] #[derive(Clone,Debug)]
@@ -70,6 +100,7 @@ pub enum KeyCode {
BracketedPasteStart, BracketedPasteStart,
BracketedPasteEnd, BracketedPasteEnd,
Char(char), Char(char),
Grapheme(Arc<str>),
Delete, Delete,
Down, Down,
End, End,

View File

@@ -1,678 +0,0 @@
use std::ops::Range;
use crate::{libsh::{error::ShResult, term::{Style, Styled}}, prompt::readline::linecmd::Anchor};
use super::linecmd::{At, CharSearch, MoveCmd, Movement, Repeat, Verb, VerbCmd, Word};
#[derive(Default,Debug)]
pub struct LineBuf {
pub buffer: Vec<char>,
pub inserting: bool,
pub last_insert: String,
cursor: usize
}
impl LineBuf {
pub fn new() -> Self {
Self::default()
}
pub fn with_initial<S: ToString>(mut self, init: S) -> Self {
self.buffer = init.to_string().chars().collect();
self
}
pub fn begin_insert(&mut self) {
self.inserting = true;
}
pub fn finish_insert(&mut self) {
self.inserting = false;
}
pub fn take_ins_text(&mut self) -> String {
std::mem::take(&mut self.last_insert)
}
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 repos_cursor(&mut self) {
if self.cursor >= self.len() {
self.cursor = self.len_minus_one();
}
}
pub fn split_lines(&self) -> Vec<String> {
let line = self.prepare_line();
let mut lines = vec![];
let mut cur_line = String::new();
for ch in line.chars() {
match ch {
'\n' => lines.push(std::mem::take(&mut cur_line)),
_ => cur_line.push(ch)
}
}
lines.push(cur_line);
lines
}
pub fn count_lines(&self) -> usize {
self.buffer.iter().filter(|&&c| c == '\n').count()
}
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn prepare_line(&self) -> String {
self.buffer
.iter()
.filter(|&&c| c != '\r')
.collect::<String>()
}
pub fn clear(&mut self) {
self.buffer.clear();
self.cursor = 0;
}
pub fn cursor_display_coords(&self) -> (usize, usize) {
let mut x = 0;
let mut y = 0;
for i in 0..self.cursor() {
let ch = self.get_char(i);
match ch {
'\n' => {
y += 1;
x = 0;
}
'\r' => continue,
_ => {
x += 1;
}
}
}
(x, y)
}
pub fn cursor_real_coords(&self) -> (usize,usize) {
let mut x = 0;
let mut y = 0;
for i in 0..self.cursor() {
let ch = self.get_char(i);
match ch {
'\n' => {
y += 1;
x = 0;
}
_ => {
x += 1;
}
}
}
(x, y)
}
pub fn backspace(&mut self) {
if self.cursor() == 0 {
return
}
self.delete_pos(self.cursor() - 1);
}
pub fn delete(&mut self) {
if self.cursor() >= self.buffer.len() {
return
}
self.delete_pos(self.cursor());
}
pub fn delete_pos(&mut self, pos: usize) {
self.buffer.remove(pos);
if pos < self.cursor() {
self.cursor = self.cursor.saturating_sub(1)
}
}
pub fn insert_at_pos(&mut self, pos: usize, ch: char) {
self.buffer.insert(pos, ch)
}
pub fn insert_at_cursor(&mut self, ch: char) {
self.buffer.insert(self.cursor, ch);
self.move_cursor_right();
}
pub fn insert_after_cursor(&mut self, ch: char) {
self.buffer.insert(self.cursor, ch);
}
pub fn backspace_at_cursor(&mut self) {
assert!(self.cursor <= self.buffer.len());
if self.buffer.is_empty() {
return
}
self.buffer.remove(self.cursor.saturating_sub(1));
self.move_cursor_left();
}
pub fn del_at_cursor(&mut self) {
assert!(self.cursor <= self.buffer.len());
if self.buffer.is_empty() || self.cursor == self.buffer.len() {
return
}
self.buffer.remove(self.cursor);
}
pub fn move_cursor_left(&mut self) {
self.cursor = self.cursor.saturating_sub(1);
}
pub fn move_cursor_start(&mut self) {
self.cursor = 0;
}
pub fn move_cursor_end(&mut self) {
self.cursor = self.buffer.len();
}
pub fn move_cursor_right(&mut self) {
if self.cursor == self.buffer.len() {
return
}
self.cursor = self.cursor.saturating_add(1);
}
pub fn del_from_cursor(&mut self) {
self.buffer.truncate(self.cursor);
}
pub fn del_word_back(&mut self) {
if self.cursor == 0 {
return
}
let end = self.cursor;
let mut start = self.cursor;
while start > 0 && self.buffer[start - 1].is_whitespace() {
start -= 1;
}
while start > 0 && !self.buffer[start - 1].is_whitespace() {
start -= 1;
}
self.buffer.drain(start..end);
self.cursor = start;
}
pub fn len(&self) -> usize {
self.buffer.len()
}
pub fn len_minus_one(&self) -> usize {
self.buffer.len().saturating_sub(1)
}
pub fn is_empty(&self) -> bool {
self.buffer.is_empty()
}
pub fn cursor_char(&self) -> Option<&char> {
self.buffer.get(self.cursor())
}
pub fn get_char(&self, pos: usize) -> char {
assert!((0..self.len()).contains(&pos));
self.buffer[pos]
}
pub fn prev_char(&self) -> Option<char> {
if self.cursor() == 0 {
None
} else {
Some(self.get_char(self.cursor() - 1))
}
}
pub fn next_char(&self) -> Option<char> {
if self.cursor() == self.len_minus_one() {
None
} else {
Some(self.get_char(self.cursor() + 1))
}
}
pub fn on_word_bound_left(&self) -> bool {
if self.cursor() == 0 {
return false
}
let Some(ch) = self.cursor_char() else {
return false
};
let cur_char_class = CharClass::from(*ch);
let prev_char_pos = self.cursor().saturating_sub(1).max(0);
cur_char_class.is_opposite(self.get_char(prev_char_pos))
}
pub fn on_word_bound_right(&self) -> bool {
if self.cursor() >= self.len_minus_one() {
return false
}
let Some(ch) = self.cursor_char() else {
return false
};
let cur_char_class = CharClass::from(*ch);
let next_char_pos = self.cursor().saturating_add(1).min(self.len());
cur_char_class.is_opposite(self.get_char(next_char_pos))
}
fn backward_until<F: Fn(usize) -> bool>(&self, mut start: usize, cond: F) -> usize {
while start > 0 && !cond(start) {
start -= 1;
}
start
}
fn forward_until<F: Fn(usize) -> bool>(&self, mut start: usize, cond: F) -> usize {
while start < self.len() && !cond(start) {
start += 1;
}
start
}
pub fn calc_range(&mut self, movement: &Movement) -> Range<usize> {
let mut start = self.cursor();
let mut end = self.cursor();
match movement {
Movement::WholeLine => {
start = self.backward_until(start, |pos| self.buffer[pos] == '\n');
if self.buffer.get(start) == Some(&'\n') {
start += 1; // Exclude the previous newline
}
end = self.forward_until(end, |pos| self.buffer[pos] == '\n');
}
Movement::BeginningOfLine => {
start = self.backward_until(start, |pos| self.buffer[pos] == '\n');
}
Movement::BeginningOfFirstWord => {
let start_of_line = self.backward_until(start, |pos| self.buffer[pos] == '\n');
start = self.forward_until(start_of_line, |pos| !self.buffer[pos].is_whitespace());
}
Movement::EndOfLine => {
end = self.forward_until(end, |pos| self.buffer[pos] == '\n');
}
Movement::BackwardWord(word) => {
match word {
Word::Big => {
if self.cursor_char().is_none() {
self.cursor = self.cursor.saturating_sub(1);
start = start.saturating_sub(1)
}
// Skip whitespace
let Some(cur_char) = self.cursor_char() else {
return start..end
};
if cur_char.is_whitespace() {
start = self.backward_until(start, |pos| !self.buffer[pos].is_whitespace())
}
let ch_class = CharClass::from(self.get_char(start));
let should_step = self.prev_char().is_some_and(|ch| ch_class.is_opposite(ch));
// If we are on a word boundary, move forward one character
// If we are now on whitespace, skip it
if should_step {
start = start.saturating_sub(1).max(0);
if self.get_char(start).is_whitespace() {
start = self.backward_until(start, |pos| !self.buffer[pos].is_whitespace())
}
}
start = self.backward_until(start, |pos| self.buffer[pos].is_whitespace());
if self.get_char(start).is_whitespace() {
start += 1;
}
}
Word::Normal => {
if self.cursor_char().is_none() {
self.cursor = self.cursor.saturating_sub(1);
start = start.saturating_sub(1)
}
let Some(cur_char) = self.cursor_char() else {
return start..end
};
// Skip whitespace
if cur_char.is_whitespace() {
start = self.backward_until(start, |pos| !self.get_char(pos).is_whitespace())
}
let ch_class = CharClass::from(self.get_char(start));
let should_step = self.prev_char().is_some_and(|ch| ch_class.is_opposite(ch));
// If we are on a word boundary, move forward one character
// If we are now on whitespace, skip it
if should_step {
start = start.saturating_sub(1).max(0);
if self.get_char(start).is_whitespace() {
start = self.backward_until(start, |pos| !self.get_char(pos).is_whitespace())
}
}
// Find an alternate charclass to stop at
let cur_char = self.get_char(start);
let cur_char_class = CharClass::from(cur_char);
start = self.backward_until(start, |pos| cur_char_class.is_opposite(self.get_char(pos)));
if cur_char_class.is_opposite(self.get_char(start)) {
start += 1;
}
}
}
}
Movement::ForwardWord(at, word) => {
let Some(cur_char) = self.cursor_char() else {
return start..end
};
let is_ws = |pos: usize| self.buffer[pos].is_whitespace();
let not_ws = |pos: usize| !self.buffer[pos].is_whitespace();
match word {
Word::Big => {
if cur_char.is_whitespace() {
end = self.forward_until(end, not_ws);
}
let ch_class = CharClass::from(self.buffer[end]);
let should_step = self.next_char().is_some_and(|ch| ch_class.is_opposite(ch));
if should_step {
end = end.saturating_add(1).min(self.len_minus_one());
if self.get_char(end).is_whitespace() {
end = self.forward_until(end, |pos| !self.get_char(pos).is_whitespace())
}
}
match at {
At::Start => {
if !should_step {
end = self.forward_until(end, is_ws);
end = self.forward_until(end, not_ws);
}
}
At::AfterEnd => {
end = self.forward_until(end, is_ws);
}
At::BeforeEnd => {
end = self.forward_until(end, is_ws);
if self.buffer.get(end).is_some_and(|ch| ch.is_whitespace()) {
end = end.saturating_sub(1);
}
}
}
}
Word::Normal => {
if cur_char.is_whitespace() {
end = self.forward_until(end, not_ws);
}
let ch_class = CharClass::from(self.buffer[end]);
let should_step = self.next_char().is_some_and(|ch| ch_class.is_opposite(ch));
if should_step {
end = end.saturating_add(1).min(self.len_minus_one());
if self.get_char(end).is_whitespace() {
end = self.forward_until(end, |pos| !self.get_char(pos).is_whitespace())
}
}
match at {
At::Start => {
if !should_step {
end = self.forward_until(end, |pos| ch_class.is_opposite(self.buffer[pos]));
if self.get_char(end).is_whitespace() {
end = self.forward_until(end, |pos| !self.get_char(pos).is_whitespace())
}
}
}
At::AfterEnd => {
end = self.forward_until(end, |pos| ch_class.is_opposite(self.buffer[pos]));
}
At::BeforeEnd => {
end = self.forward_until(end, |pos| ch_class.is_opposite(self.buffer[pos]));
if self.buffer.get(end).is_some_and(|ch| ch.is_whitespace()) {
end = end.saturating_sub(1);
}
}
}
}
}
}
Movement::BackwardChar => {
start = start.saturating_sub(1);
}
Movement::ForwardChar => {
end = end.saturating_add(1);
}
Movement::TextObj(text_obj, bound) => todo!(),
Movement::CharSearch(char_search) => {
match char_search {
CharSearch::FindFwd(ch) => {
let ch = ch.unwrap();
end = end.saturating_add(1).min(self.len_minus_one());
let search = self.forward_until(end, |pos| self.buffer[pos] == ch);
// we check anyway because it may have reached the end without finding anything
if self.buffer.get(search).is_some_and(|&s_ch| s_ch == ch) {
end = search;
}
}
CharSearch::FwdTo(ch) => {
let ch = ch.unwrap();
end = end.saturating_add(1).min(self.len_minus_one());
let search = self.forward_until(end, |pos| self.buffer[pos] == ch);
// we check anyway because it may have reached the end without finding anything
if self.buffer.get(search).is_some_and(|&s_ch| s_ch == ch) {
end = search.saturating_sub(1);
}
}
CharSearch::FindBkwd(ch) => {
let ch = ch.unwrap();
start = start.saturating_sub(1);
let search = self.backward_until(start, |pos| self.buffer[pos] == ch);
// we check anyway because it may have reached the end without finding anything
if self.buffer.get(search).is_some_and(|&s_ch| s_ch == ch) {
start = search;
}
}
CharSearch::BkwdTo(ch) => {
let ch = ch.unwrap();
start = start.saturating_sub(1);
let search = self.backward_until(start, |pos| self.buffer[pos] == ch);
// we check anyway because it may have reached the end without finding anything
if self.buffer.get(search).is_some_and(|&s_ch| s_ch == ch) {
start = search.saturating_add(1);
}
}
}
}
Movement::LineUp => todo!(),
Movement::LineDown => todo!(),
Movement::WholeBuffer => {
start = 0;
end = self.len_minus_one();
}
Movement::BeginningOfBuffer => {
start = 0;
}
Movement::EndOfBuffer => {
end = self.len_minus_one();
}
Movement::Null => {/* nothing */}
}
end = end.min(self.len());
start..end
}
pub fn exec_vi_cmd(&mut self, verb: Option<Verb>, move_cmd: Option<MoveCmd>) -> ShResult<()> {
match (verb, move_cmd) {
(Some(v), None) => self.exec_vi_verb(v),
(None, Some(m)) => self.exec_vi_movement(m),
(Some(v), Some(m)) => self.exec_vi_moveverb(v,m),
(None, None) => unreachable!()
}
}
pub fn exec_vi_verb(&mut self, verb: Verb) -> ShResult<()> {
assert!(!verb.needs_movement());
match verb {
Verb::DeleteOne(anchor) => {
match anchor {
Anchor::After => {
self.delete();
}
Anchor::Before => {
self.backspace();
}
}
}
Verb::Breakline(anchor) => {
match anchor {
Anchor::Before => {
let last_newline = self.backward_until(self.cursor(), |pos| self.get_char(pos) == '\n');
self.cursor = last_newline;
self.insert_at_cursor('\n');
self.insert_at_cursor('\r');
}
Anchor::After => {
let next_newline = self.forward_until(self.cursor(), |pos| self.get_char(pos) == '\n');
self.cursor = next_newline;
self.insert_at_cursor('\n');
self.insert_at_cursor('\r');
}
}
}
Verb::InsertChar(ch) => {
if self.inserting {
self.last_insert.push(ch);
}
self.insert_at_cursor(ch)
}
Verb::Insert(text) => {
for ch in text.chars() {
if self.inserting {
self.last_insert.push(ch);
}
self.insert_at_cursor(ch);
}
}
Verb::InsertMode => todo!(),
Verb::JoinLines => todo!(),
Verb::ToggleCase => todo!(),
Verb::OverwriteMode => todo!(),
Verb::Substitute => todo!(),
Verb::Put(_) => todo!(),
Verb::Undo => todo!(),
Verb::RepeatLast => todo!(),
Verb::Dedent => {
let mut start_pos = self.backward_until(self.cursor(), |pos| self.get_char(pos) == '\n');
if self.get_char(start_pos) == '\n' {
start_pos += 1;
}
if self.get_char(start_pos) == '\t' {
self.delete_pos(start_pos);
}
}
Verb::Indent => {
let mut line_start = self.backward_until(self.cursor(), |pos| self.get_char(pos) == '\n');
if self.get_char(line_start) == '\n' {
line_start += 1;
}
self.insert_at_pos(line_start, '\t');
}
Verb::ReplaceChar(_) => todo!(),
_ => unreachable!()
}
Ok(())
}
pub fn exec_vi_movement(&mut self, move_cmd: MoveCmd) -> ShResult<()> {
let MoveCmd { move_count, movement } = move_cmd;
for _ in 0..move_count {
let range = self.calc_range(&movement);
if range.start != self.cursor() {
self.cursor = range.start.max(0);
} else {
self.cursor = range.end.min(self.len());
}
}
Ok(())
}
pub fn exec_vi_moveverb(&mut self, verb: Verb, move_cmd: MoveCmd) -> ShResult<()> {
let MoveCmd { move_count, movement } = move_cmd;
match verb {
Verb::Delete => {
(0..move_count).for_each(|_| {
let range = self.calc_range(&movement);
let range = range.start..(range.end + 1).min(self.len());
self.buffer.drain(range);
self.repos_cursor();
});
}
Verb::Change => {
(0..move_count).for_each(|_| {
let range = self.calc_range(&movement);
let range = range.start..(range.end + 1).min(self.len());
self.buffer.drain(range);
self.repos_cursor();
});
}
Verb::Repeat(rep) => {
}
Verb::DeleteOne(anchor) => todo!(),
Verb::Breakline(anchor) => todo!(),
Verb::Yank => todo!(),
Verb::ReplaceChar(_) => todo!(),
Verb::Substitute => todo!(),
Verb::ToggleCase => todo!(),
Verb::Undo => todo!(),
Verb::RepeatLast => todo!(),
Verb::Put(anchor) => todo!(),
Verb::OverwriteMode => todo!(),
Verb::InsertMode => todo!(),
Verb::JoinLines => todo!(),
Verb::InsertChar(_) => todo!(),
Verb::Indent => todo!(),
Verb::Dedent => todo!(),
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum CharClass {
AlphaNum,
Symbol,
}
impl CharClass {
pub fn is_opposite(&self, other: char) -> bool {
let other_class = CharClass::from(other);
other_class != *self
}
}
impl From<char> for CharClass {
fn from(value: char) -> Self {
if value.is_alphanumeric() || value == '_' {
CharClass::AlphaNum
} else {
CharClass::Symbol
}
}
}
pub fn strip_ansi_codes_and_escapes(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\x1b' && chars.peek() == Some(&'[') {
// Skip over the escape sequence
chars.next(); // consume '['
while let Some(&ch) = chars.peek() {
if ch.is_ascii_lowercase() || ch.is_ascii_uppercase() {
chars.next(); // consume final letter
break;
}
chars.next(); // consume intermediate characters
}
} else {
match c {
'\n' |
'\r' => { /* Continue */ }
_ => out.push(c)
}
}
}
out
}

View File

@@ -0,0 +1,798 @@
use std::{fmt::Display, ops::{Deref, DerefMut, Range}, sync::Arc};
use unicode_width::UnicodeWidthStr;
use crate::libsh::{error::ShResult, sys::sh_quit, term::{Style, Styled}};
use super::vicmd::{Anchor, Bound, Dest, Direction, Motion, RegisterName, TextObj, To, Verb, ViCmd, Word};
#[derive(Debug, PartialEq, Eq)]
pub enum CharClass {
Alphanum,
Symbol
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MotionKind {
Forward(usize),
To(usize),
Backward(usize),
Range(usize,usize),
Null
}
#[derive(Clone,Default,Debug)]
pub struct TermCharBuf(pub Vec<TermChar>);
impl Deref for TermCharBuf {
type Target = Vec<TermChar>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for TermCharBuf {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Display for TermCharBuf {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for ch in &self.0 {
match ch {
TermChar::Grapheme(str) => write!(f, "{str}")?,
TermChar::Newline => write!(f, "\r\n")?,
}
}
Ok(())
}
}
impl FromIterator<TermChar> for TermCharBuf {
fn from_iter<T: IntoIterator<Item = TermChar>>(iter: T) -> Self {
let mut buf = vec![];
for item in iter {
buf.push(item)
}
Self(buf)
}
}
impl From<TermCharBuf> for String {
fn from(value: TermCharBuf) -> Self {
let mut string = String::new();
for char in value.0 {
match char {
TermChar::Grapheme(str) => string.push_str(&str),
TermChar::Newline => {
string.push('\r');
string.push('\n');
}
}
}
string
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum TermChar {
Grapheme(Arc<str>),
// Treated as '\n' in the code, printed as '\r\n' to the terminal
Newline
}
impl TermChar {
pub fn is_whitespace(&self) -> bool {
match self {
TermChar::Newline => true,
TermChar::Grapheme(ch) => {
ch.chars().next().is_some_and(|c| c.is_whitespace())
}
}
}
pub fn matches(&self, other: &str) -> bool {
match self {
TermChar::Grapheme(ch) => {
ch.as_ref() == other
}
TermChar::Newline => other == "\n"
}
}
}
impl From<Arc<str>> for TermChar {
fn from(value: Arc<str>) -> Self {
Self::Grapheme(value)
}
}
impl From<char> for TermChar {
fn from(value: char) -> Self {
match value {
'\n' => Self::Newline,
ch => Self::Grapheme(Arc::from(ch.to_string()))
}
}
}
impl Display for TermChar {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TermChar::Grapheme(str) => {
write!(f,"{str}")
}
TermChar::Newline => {
write!(f,"\r\n")
}
}
}
}
impl From<&TermChar> for CharClass {
fn from(value: &TermChar) -> Self {
match value {
TermChar::Newline => Self::Symbol,
TermChar::Grapheme(ch) => {
if ch.chars().next().is_some_and(|c| c.is_alphanumeric()) {
Self::Alphanum
} else {
Self::Symbol
}
}
}
}
}
impl From<char> for CharClass {
fn from(value: char) -> Self {
if value.is_alphanumeric() {
Self::Alphanum
} else {
Self::Symbol
}
}
}
fn is_other_class_or_ws(a: &TermChar, b: &TermChar) -> bool {
if a.is_whitespace() || b.is_whitespace() {
return true;
}
CharClass::from(a) != CharClass::from(b)
}
#[derive(Default,Debug)]
pub struct LineBuf {
buffer: TermCharBuf,
cursor: usize,
}
impl LineBuf {
pub fn new() -> Self {
Self::default()
}
pub fn with_initial(mut self, initial: &str) -> Self {
let chars = initial.chars();
for char in chars {
self.buffer.push(char.into())
}
self
}
pub fn buffer(&self) -> &TermCharBuf {
&self.buffer
}
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn cursor_char(&self) -> Option<&TermChar> {
let tc = self.buffer.get(self.cursor())?;
Some(tc)
}
pub fn get_char(&self, pos: usize) -> Option<&TermChar> {
let tc = self.buffer.get(pos)?;
Some(tc)
}
pub fn insert_at_cursor(&mut self, tc: TermChar) {
let cursor = self.cursor();
self.buffer.insert(cursor,tc)
}
pub fn count_lines(&self) -> usize {
self.buffer.iter().filter(|&c| c == &TermChar::Newline).count()
}
pub fn cursor_back(&mut self, count: usize) {
self.cursor = self.cursor.saturating_sub(count)
}
pub fn cursor_fwd(&mut self, count: usize) {
self.cursor = self.num_or_len(self.cursor + count)
}
pub fn cursor_to(&mut self, pos: usize) {
self.cursor = self.num_or_len(pos)
}
pub fn prepare_line(&self) -> String {
self.buffer.to_string()
}
pub fn clamp_cursor(&mut self) {
if self.cursor_char().is_none() && !self.buffer.is_empty() {
self.cursor = self.cursor.saturating_sub(1)
}
}
pub fn cursor_display_coords(&self) -> (usize, usize) {
let mut x = 0;
let mut y = 0;
for i in 0..self.cursor() {
let ch = self.get_char(i).unwrap();
match ch {
TermChar::Grapheme(str) => x += str.width().max(1),
TermChar::Newline => {
y += 1;
x = 0;
}
}
}
(x, y)
}
pub fn split_lines(&self) -> Vec<String> {
let line = self.prepare_line();
let mut lines = vec![];
let mut cur_line = String::new();
for ch in line.chars() {
match ch {
'\n' => lines.push(std::mem::take(&mut cur_line)),
_ => cur_line.push(ch)
}
}
lines.push(cur_line);
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 {
let check_pos = match dir {
Direction::Forward => self.num_or_len(pos + 1),
Direction::Backward => pos.saturating_sub(1)
};
let Some(curr_char) = self.cursor_char() else {
return false
};
self.get_char(check_pos).is_some_and(|c| {
match word {
Word::Big => c.is_whitespace(),
Word::Normal => is_other_class_or_ws(curr_char, c)
}
})
}
fn backward_until<F: Fn(&TermChar) -> bool>(&self, mut start: usize, cond: F, inclusive: bool) -> usize {
while start > 0 && !cond(&self.buffer[start]) {
start -= 1;
}
if !inclusive {
if start > 0 {
start.saturating_add(1)
} else {
start
}
} else {
start
}
}
fn forward_until<F: Fn(&TermChar) -> bool>(&self, mut start: usize, cond: F, inclusive: bool) -> usize {
while start < self.buffer.len() && !cond(&self.buffer[start]) {
start += 1;
}
if !inclusive {
if start < self.buffer.len() {
start.saturating_sub(1)
} else {
start
}
} else {
start
}
}
pub fn find_word_pos(&self, word: Word, dest: To, dir: Direction) -> usize {
let mut pos = self.cursor();
match dir {
Direction::Forward => {
match word {
Word::Big => {
match dest {
To::Start => {
if self.on_word_bound(word, pos, dir) {
// Push the cursor off of the word
pos = self.num_or_len(pos + 1);
}
// Pass the current word if any
if self.get_char(pos).is_some_and(|c| !c.is_whitespace()) {
pos = self.forward_until(pos, |c| c.is_whitespace(), true);
}
// Land on the start of the next word
pos = self.forward_until(pos, |c| !c.is_whitespace(), true)
}
To::End => {
if self.on_word_bound(word, pos, dir) {
// Push the cursor off of the word
pos = self.num_or_len(pos + 1);
}
if self.get_char(pos).is_some_and(|c| !c.is_whitespace()) {
// We are in a word
// Go to the end of the current word
pos = self.forward_until(pos, |c| c.is_whitespace(), false)
} else {
// We are outside of a word
// Find the next word, then go to the end of it
pos = self.forward_until(pos, |c| !c.is_whitespace(), true);
pos = self.forward_until(pos, |c| c.is_whitespace(), false)
}
}
}
}
Word::Normal => {
match dest {
To::Start => {
if self.on_word_bound(word, pos, dir) {
// Push the cursor off of the word
pos = self.num_or_len(pos + 1);
}
if self.get_char(pos).is_some_and(|c| !c.is_whitespace()) {
// We are inside of a word
// Find the next instance of whitespace or a different char class
let this_char = self.get_char(pos).unwrap();
pos = self.forward_until(pos, |c| is_other_class_or_ws(this_char, c), true);
// If we found whitespace, continue until we find non-whitespace
if self.get_char(pos).is_some_and(|c| c.is_whitespace()) {
pos = self.forward_until(pos, |c| !c.is_whitespace(), true)
}
} else {
// We are in whitespace, proceed to the next word
pos = self.forward_until(pos, |c| !c.is_whitespace(), true)
}
}
To::End => {
if self.on_word_bound(word, pos, dir) {
// Push the cursor off of the word
pos = self.num_or_len(pos + 1);
}
if self.get_char(pos).is_some_and(|c| !c.is_whitespace()) {
// Proceed up until the next differing char class
let this_char = self.get_char(pos).unwrap();
pos = self.forward_until(pos, |c| is_other_class_or_ws(this_char, c), false);
} else {
// Find the next non-whitespace character
pos = self.forward_until(pos, |c| !c.is_whitespace(), true);
// Then proceed until a differing char class is found
let this_char = self.get_char(pos).unwrap();
pos = self.forward_until(pos, |c|is_other_class_or_ws(this_char, c), false);
}
}
}
}
}
}
Direction::Backward => {
match word {
Word::Big => {
match dest {
To::Start => {
if self.on_word_bound(word, pos, dir) {
// Push the cursor off
pos = pos.saturating_sub(1);
}
if self.get_char(pos).is_some_and(|c| !c.is_whitespace()) {
// We are in a word, go to the start of it
pos = self.backward_until(pos, |c| c.is_whitespace(), false);
} else {
// We are not in a word, find one and go to the start of it
pos = self.backward_until(pos, |c| !c.is_whitespace(), true);
pos = self.backward_until(pos, |c| c.is_whitespace(), false);
}
}
To::End => unreachable!()
}
}
Word::Normal => {
match dest {
To::Start => {
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()) {
let this_char = self.get_char(pos).unwrap();
pos = self.backward_until(pos, |c| is_other_class_or_ws(this_char, c), false)
} else {
pos = self.backward_until(pos, |c| !c.is_whitespace(), true);
let this_char = self.get_char(pos).unwrap();
pos = self.backward_until(pos, |c| is_other_class_or_ws(this_char, c), false);
}
}
To::End => unreachable!()
}
}
}
}
}
pos
}
pub fn eval_text_obj(&self, obj: TextObj, bound: Bound) -> Range<usize> {
let mut start = self.cursor();
let mut end = self.cursor();
match obj {
TextObj::Word(word) => {
start = match self.on_word_bound(word, self.cursor(), Direction::Backward) {
true => self.cursor(),
false => self.find_word_pos(word, To::Start, Direction::Backward),
};
end = match self.on_word_bound(word, self.cursor(), Direction::Forward) {
true => self.cursor(),
false => self.find_word_pos(word, To::End, Direction::Forward),
};
end = self.num_or_len(end + 1);
if bound == Bound::Around {
end = self.forward_until(end, |c| c.is_whitespace(), true);
end = self.forward_until(end, |c| !c.is_whitespace(), true);
}
return start..end
}
TextObj::Line => {
let cursor = self.cursor();
start = self.backward_until(cursor, |c| c == &TermChar::Newline, false);
end = self.forward_until(cursor, |c| c == &TermChar::Newline, true);
}
TextObj::Sentence => todo!(),
TextObj::Paragraph => todo!(),
TextObj::DoubleQuote => {
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("\""), 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::Custom(_) => todo!(),
}
if bound == Bound::Inside {
start = self.num_or_len(start + 1);
end = end.saturating_sub(1);
}
start..end
}
/// Clamp a number to the length of the buffer
pub fn num_or_len(&self, num: usize) -> usize {
num.min(self.buffer.len().saturating_sub(1))
}
pub fn eval_motion(&self, motion: Motion) -> MotionKind {
match motion {
Motion::WholeLine => {
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) => {
let range = self.eval_text_obj(text_obj, bound);
let range = mk_range(range.start, range.end);
MotionKind::Range(range.start,range.end)
}
Motion::BeginningOfFirstWord => {
let cursor = self.cursor();
let line_start = self.backward_until(cursor, |c| c == &TermChar::Newline, true);
let first_print = self.forward_until(line_start, |c| !c.is_whitespace(), true);
MotionKind::To(first_print)
}
Motion::BeginningOfLine => {
let cursor = self.cursor();
let line_start = self.backward_until(cursor, |c| c == &TermChar::Newline, false);
MotionKind::To(line_start)
}
Motion::EndOfLine => {
let cursor = self.cursor();
let line_end = self.forward_until(cursor, |c| c == &TermChar::Newline, false);
MotionKind::To(line_end)
}
Motion::BackwardWord(word) => MotionKind::To(self.find_word_pos(word, To::Start, Direction::Backward)),
Motion::ForwardWord(dest, word) => MotionKind::To(self.find_word_pos(word, dest, Direction::Forward)),
Motion::CharSearch(direction, dest, ch) => {
let mut cursor = self.cursor();
let inclusive = matches!(dest, Dest::On);
let stop_condition = |c: &TermChar| {
c == &TermChar::Newline ||
c == &ch
};
if self.cursor_char().is_some_and(|c| c == &ch) {
// We are already on the character we are looking for
// Let's nudge the cursor
match direction {
Direction::Backward => cursor = self.cursor().saturating_sub(1),
Direction::Forward => cursor = self.num_or_len(self.cursor() + 1),
}
}
let stop_pos = match direction {
Direction::Forward => self.forward_until(cursor, stop_condition, inclusive),
Direction::Backward => self.backward_until(cursor, stop_condition, inclusive),
};
let found_char = match dest {
Dest::On => self.get_char(stop_pos).is_some_and(|c| c == &ch),
_ => {
match direction {
Direction::Forward => self.get_char(stop_pos + 1).is_some_and(|c| c == &ch),
Direction::Backward => self.get_char(stop_pos.saturating_sub(1)).is_some_and(|c| c == &ch),
}
}
};
if found_char {
MotionKind::To(stop_pos)
} else {
MotionKind::Null
}
}
Motion::BackwardChar => MotionKind::Backward(1),
Motion::ForwardChar => MotionKind::Forward(1),
Motion::LineUp => todo!(),
Motion::LineDown => todo!(),
Motion::WholeBuffer => MotionKind::Range(0,self.buffer.len().saturating_sub(1)),
Motion::BeginningOfBuffer => MotionKind::To(0),
Motion::EndOfBuffer => MotionKind::To(self.buffer.len().saturating_sub(1)),
Motion::Null => MotionKind::Null,
Motion::Builder(_) => unreachable!(),
}
}
pub fn exec_verb(&mut self, verb: Verb, motion: MotionKind, register: RegisterName) -> ShResult<()> {
match verb {
Verb::Change |
Verb::Delete => {
let deleted;
match motion {
MotionKind::Forward(n) => {
let fwd = self.num_or_len(self.cursor() + n);
let cursor = self.cursor();
deleted = self.buffer.drain(cursor..=fwd).collect::<TermCharBuf>();
}
MotionKind::To(pos) => {
let range = mk_range(self.cursor(), pos);
deleted = self.buffer.drain(range.clone()).collect::<TermCharBuf>();
self.apply_motion(MotionKind::To(range.start));
}
MotionKind::Backward(n) => {
let back = self.cursor.saturating_sub(n);
let cursor = self.cursor();
deleted = self.buffer.drain(back..cursor).collect::<TermCharBuf>();
self.apply_motion(MotionKind::To(back));
}
MotionKind::Range(s, e) => {
deleted = self.buffer.drain(s..e).collect::<TermCharBuf>();
self.apply_motion(MotionKind::To(s));
}
MotionKind::Null => return Ok(())
}
register.write_to_register(deleted);
}
Verb::DeleteChar(anchor) => {
match anchor {
Anchor::After => {
let pos = self.num_or_len(self.cursor() + 1);
self.buffer.remove(pos);
}
Anchor::Before => {
let pos = self.cursor.saturating_sub(1);
self.buffer.remove(pos);
self.cursor = self.cursor.saturating_sub(1);
}
}
}
Verb::Yank => {
let yanked;
match motion {
MotionKind::Forward(n) => {
let fwd = self.num_or_len(self.cursor() + n);
let cursor = self.cursor();
yanked = self.buffer[cursor..=fwd]
.iter()
.cloned()
.collect::<TermCharBuf>();
}
MotionKind::To(pos) => {
let range = mk_range(self.cursor(), pos);
yanked = self.buffer[range.clone()]
.iter()
.cloned()
.collect::<TermCharBuf>();
self.apply_motion(MotionKind::To(range.start));
}
MotionKind::Backward(n) => {
let back = self.cursor.saturating_sub(n);
let cursor = self.cursor();
yanked = self.buffer[back..cursor]
.iter()
.cloned()
.collect::<TermCharBuf>();
self.apply_motion(MotionKind::To(back));
}
MotionKind::Range(s, e) => {
yanked = self.buffer[s..e]
.iter()
.cloned()
.collect::<TermCharBuf>();
self.apply_motion(MotionKind::To(s));
}
MotionKind::Null => return Ok(())
}
register.write_to_register(yanked);
}
Verb::ReplaceChar(_) => todo!(),
Verb::Substitute => todo!(),
Verb::ToggleCase => todo!(),
Verb::Complete => todo!(),
Verb::CompleteBackward => todo!(),
Verb::Undo => todo!(),
Verb::RepeatLast => todo!(),
Verb::Put(anchor) => {
if let Some(charbuf) = register.read_from_register() {
let chars = charbuf.0.into_iter();
if anchor == Anchor::Before {
self.cursor_back(1);
}
for char in chars {
self.insert_at_cursor(char);
self.cursor_fwd(1);
}
}
}
Verb::JoinLines => todo!(),
Verb::InsertChar(ch) => {
self.insert_at_cursor(ch);
self.apply_motion(motion);
}
Verb::Insert(_) => todo!(),
Verb::Breakline(anchor) => todo!(),
Verb::Indent => todo!(),
Verb::Dedent => todo!(),
Verb::AcceptLine => todo!(),
Verb::EndOfFile => {
if self.buffer.is_empty() {
sh_quit(0)
} else {
self.buffer.clear();
self.cursor = 0;
}
}
Verb::InsertMode |
Verb::NormalMode |
Verb::VisualMode |
Verb::OverwriteMode => {
self.apply_motion(motion);
}
}
Ok(())
}
pub fn apply_motion(&mut self, motion: MotionKind) {
match motion {
MotionKind::Forward(n) => self.cursor_fwd(n),
MotionKind::To(pos) => self.cursor_to(pos),
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::Null => { /* Pass */ }
}
}
pub fn exec_cmd(&mut self, cmd: ViCmd) -> ShResult<()> {
let ViCmd { register, verb_count, verb, motion_count, motion, .. } = cmd;
for _ in 0..verb_count.unwrap_or(1) {
for _ in 0..motion_count.unwrap_or(1) {
let motion = motion
.clone()
.map(|m| self.eval_motion(m))
.unwrap_or(MotionKind::Null);
if let Some(verb) = verb.clone() {
self.exec_verb(verb, motion, register)?;
} else {
self.apply_motion(motion);
}
}
}
self.clamp_cursor();
Ok(())
}
}
impl Display for LineBuf {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f,"{}",self.buffer)
}
}
pub fn strip_ansi_codes_and_escapes(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\x1b' && chars.peek() == Some(&'[') {
// Skip over the escape sequence
chars.next(); // consume '['
while let Some(&ch) = chars.peek() {
if ch.is_ascii_lowercase() || ch.is_ascii_uppercase() {
chars.next(); // consume final letter
break;
}
chars.next(); // consume intermediate characters
}
} else {
match c {
'\n' |
'\r' => { /* Continue */ }
_ => out.push(c)
}
}
}
out
}
fn mk_range(a: usize, b: usize) -> Range<usize> {
std::cmp::min(a, b)..std::cmp::max(a, b)
}

View File

@@ -1,420 +0,0 @@
// Credit to Rustyline for enumerating these editor commands
// https://github.com/kkawakam/rustyline
use crate::libsh::error::{ShErr, ShErrKind, ShResult};
use crate::prelude::*;
pub type RepeatCount = u16;
#[derive(Default, Debug, Clone, Eq, PartialEq)]
pub struct ViCmdBuilder {
verb_count: Option<u16>,
verb: Option<Verb>,
move_count: Option<u16>,
movement: Option<Movement>,
}
impl ViCmdBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn with_verb_count(self, verb_count: u16) -> Self {
let Self { verb_count: _, verb, move_count, movement } = self;
Self { verb_count: Some(verb_count), verb, move_count, movement }
}
pub fn with_verb(self, verb: Verb) -> Self {
let Self { verb_count, verb: _, move_count, movement } = self;
Self { verb_count, verb: Some(verb), move_count, movement }
}
pub fn with_move_count(self, move_count: u16) -> Self {
let Self { verb_count, verb, move_count: _, movement } = self;
Self { verb_count, verb, move_count: Some(move_count), movement }
}
pub fn with_movement(self, movement: Movement) -> Self {
let Self { verb_count, verb, move_count, movement: _ } = self;
Self { verb_count, verb, move_count, movement: Some(movement) }
}
pub fn verb_count(&self) -> Option<u16> {
self.verb_count
}
pub fn move_count(&self) -> Option<u16> {
self.move_count
}
pub fn movement(&self) -> Option<&Movement> {
self.movement.as_ref()
}
pub fn verb(&self) -> Option<&Verb> {
self.verb.as_ref()
}
pub fn append_digit(&mut self, digit: char) {
// Convert char digit to a number (assuming ASCII '0'..'9')
let digit_val = digit.to_digit(10).expect("digit must be 0-9") as u16;
if self.verb.is_none() {
// Append to verb_count
self.verb_count = Some(match self.verb_count {
Some(count) => count * 10 + digit_val,
None => digit_val,
});
} else {
// Append to move_count
self.move_count = Some(match self.move_count {
Some(count) => count * 10 + digit_val,
None => digit_val,
});
}
}
pub fn is_unfinished(&self) -> bool {
(self.verb.is_none() && self.movement.is_none()) ||
(self.verb.is_none() && self.movement.as_ref().is_some_and(|m| m.needs_verb())) ||
(self.movement.is_none() && self.verb.as_ref().is_some_and(|v| v.needs_movement()))
}
pub fn build(self) -> ShResult<ViCmd> {
if self.is_unfinished() {
flog!(ERROR, "Unfinished Builder: {:?}", self);
return Err(
ShErr::simple(ShErrKind::ReadlineErr, "called ViCmdBuilder::build() with an unfinished builder")
)
}
let Self { verb_count, verb, move_count, movement } = self;
let verb_count = verb_count.unwrap_or(1);
let move_count = move_count.unwrap_or(if verb.is_none() { verb_count } else { 1 });
let verb = verb.map(|v| VerbCmd { verb_count, verb: v });
let movement = movement.map(|m| MoveCmd { move_count, movement: m });
Ok(match (verb, movement) {
(Some(v), Some(m)) => ViCmd::MoveVerb(v, m),
(Some(v), None) => ViCmd::Verb(v),
(None, Some(m)) => ViCmd::Move(m),
(None, None) => unreachable!(),
})
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum ViCmd {
MoveVerb(VerbCmd, MoveCmd),
Verb(VerbCmd),
Move(MoveCmd)
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct VerbCmd {
pub verb_count: u16,
pub verb: Verb
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct MoveCmd {
pub move_count: u16,
pub movement: Movement
}
#[derive(Default, Debug, Clone, Eq, PartialEq)]
pub struct Repeat {
pub movement: Option<MoveCmd>,
pub verb: Option<Box<VerbCmd>>,
pub ins_text: Option<String>
}
impl Repeat {
pub fn from_cmd(cmd: ViCmd) -> Self {
match cmd {
ViCmd::MoveVerb(verb_cmd, move_cmd) => {
Self {
movement: Some(move_cmd),
verb: Some(Box::new(verb_cmd)),
ins_text: None,
}
}
ViCmd::Verb(verb_cmd) => {
Self {
movement: None,
verb: Some(Box::new(verb_cmd)),
ins_text: None,
}
}
ViCmd::Move(move_cmd) => {
Self {
movement: Some(move_cmd),
verb: None,
ins_text: None,
}
}
}
}
pub fn set_ins_text(&mut self, ins_text: String) {
self.ins_text = Some(ins_text);
}
fn is_empty(&self) -> bool {
self.movement.is_none() && self.verb.is_none() && self.ins_text.is_none()
}
pub fn to_option(self) -> Option<Self> {
if self.is_empty() {
None
} else {
Some(self)
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
#[non_exhaustive]
pub enum Verb {
/// `d`, `D` — delete motion or line
Delete,
/// `x`, `X` — delete one char, forward or back
DeleteOne(Anchor),
/// `c`, `C` — change (delete + insert)
Change,
/// `y`, `Y` — yank (copy)
Yank,
/// `r` — replace a single character
ReplaceChar(char),
/// `s` or `S` — substitute (change + single char or line)
Substitute,
/// `~` — swap case
ToggleCase,
/// `u` — undo
Undo,
/// `.` — repeat last edit
RepeatLast,
/// `p`, `P` — paste
Put(Anchor),
/// `R` — overwrite characters
OverwriteMode,
/// `i`, `a`, `I`, `A`, `o`, `O` — insert/append text
InsertMode,
/// `J` — join lines
JoinLines,
Repeat(Repeat),
InsertChar(char),
Insert(String),
Breakline(Anchor),
Indent,
Dedent
}
impl Verb {
pub fn needs_movement(&self) -> bool {
match self {
Verb::DeleteOne(_) |
Verb::InsertMode |
Verb::JoinLines |
Verb::ToggleCase |
Verb::OverwriteMode |
Verb::Substitute |
Verb::Put(_) |
Verb::Undo |
Verb::RepeatLast |
Verb::Dedent |
Verb::Indent |
Verb::InsertChar(_) |
Verb::Breakline(_) |
Verb::Repeat(_) |
Verb::Insert(_) |
Verb::ReplaceChar(_) => false,
Verb::Delete |
Verb::Change |
Verb::Yank => true
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Movement {
/// Whole current line (not really a movement but a range)
WholeLine,
TextObj(TextObj, Bound),
BeginningOfFirstWord,
/// beginning-of-line
BeginningOfLine,
/// end-of-line
EndOfLine,
/// backward-word, vi-prev-word
BackwardWord(Word), // Backward until start of word
/// forward-word, vi-end-word, vi-next-word
ForwardWord(At, Word), // Forward until start/end of word
/// character-search, character-search-backward, vi-char-search
CharSearch(CharSearch),
/// backward-char
BackwardChar,
/// forward-char
ForwardChar,
/// move to the same column on the previous line
LineUp,
/// move to the same column on the next line
LineDown,
/// Whole user input (not really a movement but a range)
WholeBuffer,
/// beginning-of-buffer
BeginningOfBuffer,
/// end-of-buffer
EndOfBuffer,
Null
}
impl Movement {
pub fn needs_verb(&self) -> bool {
match self {
Self::WholeLine |
Self::BeginningOfLine |
Self::BeginningOfFirstWord |
Self::EndOfLine |
Self::BackwardWord(_) |
Self::ForwardWord(_, _) |
Self::CharSearch(_) |
Self::BackwardChar |
Self::ForwardChar |
Self::LineUp |
Self::LineDown |
Self::WholeBuffer |
Self::BeginningOfBuffer |
Self::EndOfBuffer => false,
Self::Null |
Self::TextObj(_, _) => true
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum TextObj {
/// `iw`, `aw` — inner word, around word
Word,
/// `is`, `as` — inner sentence, around sentence
Sentence,
/// `ip`, `ap` — inner paragraph, around paragraph
Paragraph,
/// `i"`, `a"` — inner/around double quotes
DoubleQuote,
/// `i'`, `a'`
SingleQuote,
/// `i\``, `a\``
BacktickQuote,
/// `i)`, `a)` — round parens
Paren,
/// `i]`, `a]`
Bracket,
/// `i}`, `a}`
Brace,
/// `i<`, `a<`
Angle,
/// `it`, `at` — HTML/XML tags (if you support it)
Tag,
/// Custom user-defined objects maybe?
Custom(char),
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Bound {
Inside,
Around
}
#[derive(Debug, Clone, Eq, PartialEq)]
#[non_exhaustive]
pub enum LineCmd {
Abort,
AcceptLine,
BeginningOfHistory,
CapitalizeWord,
ClearScreen,
Complete,
CompleteBackward,
CompleteHint,
DowncaseWord,
EndOfFile,
EndOfHistory,
ForwardSearchHistory,
HistorySearchBackward,
HistorySearchForward,
Insert(String),
Interrupt,
Move(Movement),
NextHistory,
Noop,
Overwrite(char),
PreviousHistory,
QuotedInsert,
Repaint,
ReverseSearchHistory,
Suspend,
TransposeChars,
TransposeWords,
YankPop,
LineUpOrPreviousHistory,
LineDownOrNextHistory,
Newline,
AcceptOrInsertLine { accept_in_the_middle: bool },
/// 🧵 New: vi-style editing command
ViCmd(ViCmd),
/// unknown/unmapped key
Unknown,
Null,
}
impl LineCmd {
pub fn backspace() -> Self {
let cmd = ViCmdBuilder::new()
.with_verb(Verb::DeleteOne(Anchor::Before))
.build()
.unwrap();
Self::ViCmd(cmd)
}
const fn is_repeatable_change(&self) -> bool {
matches!(
*self,
Self::Insert(..)
| Self::ViCmd(..)
)
}
const fn is_repeatable(&self) -> bool {
match *self {
Self::Move(_) => true,
_ => self.is_repeatable_change(),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Copy)]
pub enum At {
Start,
BeforeEnd,
AfterEnd
}
#[derive(Debug, Clone, Eq, PartialEq, Copy)]
pub enum Anchor {
After,
Before
}
#[derive(Debug, Clone, Eq, PartialEq, Copy)]
pub enum CharSearch {
FindFwd(Option<char>),
FwdTo(Option<char>),
FindBkwd(Option<char>),
BkwdTo(Option<char>),
}
#[derive(Debug, Clone, Eq, PartialEq, Copy)]
pub enum Word {
Big,
Normal
}
#[derive(Default,Debug,Clone,Copy,PartialEq,Eq,PartialOrd,Ord)]
pub enum InputMode {
Normal,
#[default]
Insert,
Visual,
Replace
}

View File

@@ -1,86 +1,49 @@
use std::{arch::asm, os::fd::BorrowedFd}; use std::{collections::HashMap, sync::Mutex};
use keys::KeyEvent; use linebuf::{strip_ansi_codes_and_escapes, LineBuf, TermCharBuf};
use line::{strip_ansi_codes_and_escapes, LineBuf}; use mode::{CmdReplay, ViInsert, ViMode, ViNormal};
use linecmd::{Anchor, At, CharSearch, InputMode, LineCmd, MoveCmd, Movement, Verb, VerbCmd, ViCmd, ViCmdBuilder, Word};
use nix::{libc::STDIN_FILENO, sys::termios::{self, Termios}, unistd::read};
use term::Terminal; use term::Terminal;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use vicmd::{Verb, ViCmd};
use crate::libsh::{error::ShResult, term::{Style, Styled}};
use crate::{libsh::{error::{ShErr, ShErrKind, ShResult}, sys::sh_quit}, prelude::*};
use linecmd::Repeat;
pub mod term;
pub mod line;
pub mod keys; pub mod keys;
pub mod linecmd; pub mod term;
pub mod linebuf;
pub mod vicmd;
pub mod mode;
pub mod register;
/// Add a verb to a specified ViCmdBuilder, then build it pub struct FernVi {
/// term: Terminal,
/// Returns the built value as a LineCmd::ViCmd line: LineBuf,
macro_rules! build_verb { prompt: String,
($cmd:expr,$verb:expr) => {{ mode: Box<dyn ViMode>,
$cmd.with_verb($verb).build().map(|cmd| LineCmd::ViCmd(cmd)) repeat_action: Option<CmdReplay>,
}}
} }
/// Add a movement to a specified ViCmdBuilder, then build it impl FernVi {
/// pub fn new(prompt: Option<String>) -> Self {
/// Returns the built value as a LineCmd::ViCmd let prompt = prompt.unwrap_or("$ ".styled(Style::Green | Style::Bold));
macro_rules! build_movement { let line = LineBuf::new().with_initial("The quick brown fox jumps over the lazy dog");//\nThe quick brown fox jumps over the lazy dog\nThe quick brown fox jumps over the lazy dog\n");
($cmd:expr,$move:expr) => {{
$cmd.with_movement($move).build().map(|cmd| LineCmd::ViCmd(cmd))
}}
}
/// Add both a movement and a verb to a specified ViCmdBuilder, then build it
///
/// Returns the built value as a LineCmd::ViCmd
macro_rules! build_moveverb {
($cmd:expr,$verb:expr,$move:expr) => {{
$cmd.with_movement($move).with_verb($verb).build().map(|cmd| LineCmd::ViCmd(cmd))
}}
}
#[derive(Default,Debug)]
pub struct FernReader {
pub term: Terminal,
pub prompt: String,
pub line: LineBuf,
pub edit_mode: InputMode,
pub last_vicmd: Option<Repeat>,
}
impl FernReader {
pub fn new(prompt: String) -> Self {
let line = LineBuf::new().with_initial("The quick brown fox jumped over the lazy dog.");
Self { Self {
term: Terminal::new(), term: Terminal::new(),
prompt,
line, line,
edit_mode: Default::default(), prompt,
last_vicmd: Default::default() mode: Box::new(ViInsert::new()),
repeat_action: None,
} }
} }
fn pack_line(&mut self) -> String { pub fn clear_line(&self) {
self.line
.buffer
.iter()
.collect::<String>()
}
pub fn readline(&mut self) -> ShResult<String> {
self.display_line(/*refresh: */ false);
loop {
let cmd = self.next_cmd()?;
if cmd == LineCmd::AcceptLine {
return Ok(self.pack_line())
}
self.execute_cmd(cmd)?;
self.display_line(/* refresh: */ true);
}
}
fn clear_line(&self) {
let prompt_lines = self.prompt.lines().count(); let prompt_lines = self.prompt.lines().count();
let buf_lines = self.line.count_lines().saturating_sub(1); // One of the buffer's lines will overlap with the prompt. probably. let buf_lines = if self.prompt.ends_with('\n') {
self.line.count_lines()
} else {
// 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)
};
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");
for _ in 0..total { for _ in 0..total {
@@ -88,9 +51,9 @@ impl FernReader {
} }
self.term.write_bytes(b"\r\x1b[2K"); self.term.write_bytes(b"\r\x1b[2K");
} }
fn display_line(&mut self, refresh: bool) { pub fn print_buf(&self, refresh: bool) {
if refresh { if refresh {
self.clear_line(); self.clear_line()
} }
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;
@@ -114,315 +77,54 @@ impl FernReader {
} }
} }
if num_lines == 1 {
let cursor_offset = self.line.cursor() + last_line_len;
self.term.write(&format!("\r\x1b[{}C", cursor_offset));
} else {
let (x, y) = self.line.cursor_display_coords(); let (x, y) = self.line.cursor_display_coords();
// Y-axis movements are 1-indexed and must move up from the bottom let y = num_lines.saturating_sub(y + 1);
// Therefore, add 1 to Y and subtract that number from the number of lines
// to find the number of times we have to push the cursor upward
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[{}A", y));
} }
self.term.write(&format!("\r\x1b[{}C", x+2)); // Factor in the line bullet thing
// 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 };
self.term.write(&format!("\r\x1b[{}C", cursor_x));
self.term.write(&self.mode.cursor_style());
} }
match self.edit_mode { pub fn readline(&mut self) -> ShResult<String> {
InputMode::Replace | self.print_buf(false);
InputMode::Insert => { loop {
self.term.write("\x1b[6 q")
}
InputMode::Normal |
InputMode::Visual => {
self.term.write("\x1b[2 q")
}
}
}
pub fn set_normal_mode(&mut self) {
self.edit_mode = InputMode::Normal;
self.line.finish_insert();
let ins_text = self.line.take_ins_text();
self.last_vicmd.as_mut().map(|cmd| cmd.set_ins_text(ins_text));
}
pub fn set_insert_mode(&mut self) {
self.edit_mode = InputMode::Insert;
self.line.begin_insert();
}
pub fn next_cmd(&mut self) -> ShResult<LineCmd> {
let vi_cmd = ViCmdBuilder::new();
match self.edit_mode {
InputMode::Normal => self.get_normal_cmd(vi_cmd),
InputMode::Insert => self.get_insert_cmd(vi_cmd),
InputMode::Visual => todo!(),
InputMode::Replace => todo!(),
}
}
pub fn get_insert_cmd(&mut self, pending_cmd: ViCmdBuilder) -> ShResult<LineCmd> {
use keys::{KeyEvent as E, KeyCode as K, ModKeys as M};
let key = self.term.read_key(); let key = self.term.read_key();
let cmd = match key { let Some(cmd) = self.mode.handle_key(key) else {
E(K::Char(ch), M::NONE) => build_verb!(pending_cmd, Verb::InsertChar(ch))?, continue
E(K::Char('H'), M::CTRL) |
E(K::Backspace, M::NONE) => LineCmd::backspace(),
E(K::BackTab, M::NONE) => LineCmd::CompleteBackward,
E(K::Char('I'), M::CTRL) |
E(K::Tab, M::NONE) => LineCmd::Complete,
E(K::Esc, M::NONE) => {
build_movement!(pending_cmd, Movement::BackwardChar)?
}
_ => {
flog!(INFO, "unhandled key in get_insert_cmd, trying common_cmd...");
return self.common_cmd(key, pending_cmd)
}
}; };
Ok(cmd)
if cmd.should_submit() {
return Ok(self.line.to_string());
} }
pub fn get_normal_cmd(&mut self, mut pending_cmd: ViCmdBuilder) -> ShResult<LineCmd> { self.exec_cmd(cmd.clone())?;
use keys::{KeyEvent as E, KeyCode as K, ModKeys as M}; self.print_buf(true);
let key = self.term.read_key();
if let E(K::Char(ch), M::NONE) = key {
if pending_cmd.movement().is_some_and(|m| matches!(m, Movement::CharSearch(_))) {
let Movement::CharSearch(charsearch) = pending_cmd.movement().unwrap() else {unreachable!()};
match charsearch {
CharSearch::FindFwd(_) => {
let finalized = CharSearch::FindFwd(Some(ch));
return build_movement!(pending_cmd, Movement::CharSearch(finalized))
}
CharSearch::FwdTo(_) => {
let finalized = CharSearch::FwdTo(Some(ch));
return build_movement!(pending_cmd, Movement::CharSearch(finalized))
}
CharSearch::FindBkwd(_) => {
let finalized = CharSearch::FindBkwd(Some(ch));
return build_movement!(pending_cmd, Movement::CharSearch(finalized))
}
CharSearch::BkwdTo(_) => {
let finalized = CharSearch::BkwdTo(Some(ch));
return build_movement!(pending_cmd, Movement::CharSearch(finalized))
} }
} }
} pub fn exec_cmd(&mut self, cmd: ViCmd) -> ShResult<()> {
} if cmd.is_mode_transition() {
let count = cmd.verb_count();
if let E(K::Char(digit @ '0'..='9'), M::NONE) = key { let mut mode: Box<dyn ViMode> = match cmd.verb().unwrap() {
pending_cmd.append_digit(digit); Verb::InsertMode => Box::new(ViInsert::new().with_count(count)),
return self.get_normal_cmd(pending_cmd); Verb::NormalMode => Box::new(ViNormal::new()),
} Verb::VisualMode => todo!(),
let cmd = match key { Verb::OverwriteMode => todo!(),
E(K::Char('h'), M::NONE) => { _ => unreachable!()
let cmd = pending_cmd
.with_movement(Movement::BackwardChar)
.build()?;
LineCmd::ViCmd(cmd)
}
E(K::Char('.'), M::NONE) => {
match &self.last_vicmd {
None => LineCmd::Null,
Some(cmd) => {
build_verb!(pending_cmd, Verb::Repeat(cmd.clone()))?
}
}
}
E(K::Char('j'), M::NONE) => LineCmd::LineDownOrNextHistory,
E(K::Char('k'), M::NONE) => LineCmd::LineUpOrPreviousHistory,
E(K::Char('D'), M::NONE) => build_moveverb!(pending_cmd,Verb::Delete,Movement::EndOfLine)?,
E(K::Char('C'), M::NONE) => build_moveverb!(pending_cmd,Verb::Change,Movement::EndOfLine)?,
E(K::Char('Y'), M::NONE) => build_moveverb!(pending_cmd,Verb::Yank,Movement::EndOfLine)?,
E(K::Char('l'), M::NONE) => build_movement!(pending_cmd,Movement::ForwardChar)?,
E(K::Char('w'), M::NONE) => build_movement!(pending_cmd,Movement::ForwardWord(At::Start, Word::Normal))?,
E(K::Char('W'), M::NONE) => build_movement!(pending_cmd,Movement::ForwardWord(At::Start, Word::Big))?,
E(K::Char('b'), M::NONE) => build_movement!(pending_cmd,Movement::BackwardWord(Word::Normal))?,
E(K::Char('B'), M::NONE) => build_movement!(pending_cmd,Movement::BackwardWord(Word::Big))?,
E(K::Char('e'), M::NONE) => build_movement!(pending_cmd,Movement::ForwardWord(At::BeforeEnd, Word::Normal))?,
E(K::Char('E'), M::NONE) => build_movement!(pending_cmd,Movement::ForwardWord(At::BeforeEnd, Word::Big))?,
E(K::Char('^'), M::NONE) => build_movement!(pending_cmd,Movement::BeginningOfFirstWord)?,
E(K::Char('0'), M::NONE) => build_movement!(pending_cmd,Movement::BeginningOfLine)?,
E(K::Char('$'), M::NONE) => build_movement!(pending_cmd,Movement::EndOfLine)?,
E(K::Char('x'), M::NONE) => build_verb!(pending_cmd,Verb::DeleteOne(Anchor::After))?,
E(K::Char('o'), M::NONE) => {
self.set_insert_mode();
build_verb!(pending_cmd,Verb::Breakline(Anchor::After))?
}
E(K::Char('O'), M::NONE) => {
self.set_insert_mode();
build_verb!(pending_cmd,Verb::Breakline(Anchor::Before))?
}
E(K::Char('i'), M::NONE) => {
self.set_insert_mode();
LineCmd::Null
}
E(K::Char('I'), M::NONE) => {
self.set_insert_mode();
build_movement!(pending_cmd,Movement::BeginningOfFirstWord)?
}
E(K::Char('a'), M::NONE) => {
self.set_insert_mode();
build_movement!(pending_cmd,Movement::ForwardChar)?
}
E(K::Char('A'), M::NONE) => {
self.set_insert_mode();
build_movement!(pending_cmd,Movement::EndOfLine)?
}
E(K::Char('c'), M::NONE) => {
if pending_cmd.verb() == Some(&Verb::Change) {
build_moveverb!(pending_cmd,Verb::Change,Movement::WholeLine)?
} else {
pending_cmd = pending_cmd.with_verb(Verb::Change);
self.get_normal_cmd(pending_cmd)?
}
}
E(K::Char('>'), M::NONE) => {
if pending_cmd.verb() == Some(&Verb::Indent) {
build_verb!(pending_cmd,Verb::Indent)?
} else {
pending_cmd = pending_cmd.with_verb(Verb::Indent);
self.get_normal_cmd(pending_cmd)?
}
}
E(K::Char('<'), M::NONE) => {
if pending_cmd.verb() == Some(&Verb::Dedent) {
build_verb!(pending_cmd,Verb::Dedent)?
} else {
pending_cmd = pending_cmd.with_verb(Verb::Dedent);
self.get_normal_cmd(pending_cmd)?
}
}
E(K::Char('d'), M::NONE) => {
if pending_cmd.verb() == Some(&Verb::Delete) {
LineCmd::ViCmd(pending_cmd.with_movement(Movement::WholeLine).build()?)
} else {
pending_cmd = pending_cmd.with_verb(Verb::Delete);
self.get_normal_cmd(pending_cmd)?
}
}
E(K::Char('f'), M::NONE) => {
pending_cmd = pending_cmd.with_movement(Movement::CharSearch(CharSearch::FindFwd(None)));
self.get_normal_cmd(pending_cmd)?
}
E(K::Char('F'), M::NONE) => {
pending_cmd = pending_cmd.with_movement(Movement::CharSearch(CharSearch::FindBkwd(None)));
self.get_normal_cmd(pending_cmd)?
}
E(K::Char('t'), M::NONE) => {
pending_cmd = pending_cmd.with_movement(Movement::CharSearch(CharSearch::FwdTo(None)));
self.get_normal_cmd(pending_cmd)?
}
E(K::Char('T'), M::NONE) => {
pending_cmd = pending_cmd.with_movement(Movement::CharSearch(CharSearch::BkwdTo(None)));
self.get_normal_cmd(pending_cmd)?
}
_ => {
flog!(INFO, "unhandled key in get_normal_cmd, trying common_cmd...");
return self.common_cmd(key, pending_cmd)
}
}; };
Ok(cmd)
}
pub fn common_cmd(&mut self, key: KeyEvent, pending_cmd: ViCmdBuilder) -> ShResult<LineCmd> { std::mem::swap(&mut mode, &mut self.mode);
use keys::{KeyEvent as E, KeyCode as K, ModKeys as M}; self.term.write(&mode.cursor_style());
match key {
E(K::Home, M::NONE) => build_movement!(pending_cmd,Movement::BeginningOfLine), if mode.is_repeatable() {
E(K::End, M::NONE) => build_movement!(pending_cmd,Movement::EndOfLine), self.repeat_action = mode.as_replay();
E(K::Left, M::NONE) => build_movement!(pending_cmd,Movement::BackwardChar),
E(K::Right, M::NONE) => build_movement!(pending_cmd,Movement::ForwardChar),
E(K::Delete, M::NONE) => build_moveverb!(pending_cmd,Verb::Delete,Movement::ForwardChar),
E(K::Up, M::NONE) => Ok(LineCmd::LineUpOrPreviousHistory),
E(K::Down, M::NONE) => Ok(LineCmd::LineDownOrNextHistory),
E(K::Enter, M::NONE) => Ok(LineCmd::AcceptLine),
E(K::Char('D'), M::CTRL) => Ok(LineCmd::EndOfFile),
E(K::Backspace, M::NONE) |
E(K::Char('h'), M::CTRL) => {
Ok(LineCmd::backspace())
}
_ => Err(ShErr::simple(ShErrKind::ReadlineErr,format!("Unhandled common key event: {key:?}")))
} }
} }
pub fn handle_repeat(&mut self, cmd: &ViCmd) -> ShResult<()> { self.line.exec_cmd(cmd)?;
Ok(())
}
pub fn exec_vi_cmd(&mut self, cmd: ViCmd) -> ShResult<()> {
self.last_vicmd = Some(Repeat::from_cmd(cmd.clone()));
match cmd {
ViCmd::MoveVerb(verb_cmd, move_cmd) => {
let VerbCmd { verb_count, verb } = verb_cmd;
for _ in 0..verb_count {
self.line.exec_vi_cmd(Some(verb.clone()), Some(move_cmd.clone()))?;
}
if verb == Verb::Change {
self.set_insert_mode();
}
}
ViCmd::Verb(verb_cmd) => {
let VerbCmd { verb_count, verb } = verb_cmd;
for _ in 0..verb_count {
self.line.exec_vi_cmd(Some(verb.clone()), None)?;
}
}
ViCmd::Move(move_cmd) => {
self.line.exec_vi_cmd(None, Some(move_cmd))?;
}
}
Ok(())
}
pub fn execute_cmd(&mut self, cmd: LineCmd) -> ShResult<()> {
match cmd {
LineCmd::ViCmd(cmd) => self.exec_vi_cmd(cmd)?,
LineCmd::Abort => todo!("Unhandled cmd: {cmd:?}"),
LineCmd::BeginningOfHistory => todo!("Unhandled cmd: {cmd:?}"),
LineCmd::CapitalizeWord => todo!("Unhandled cmd: {cmd:?}"),
LineCmd::ClearScreen => todo!("Unhandled cmd: {cmd:?}"),
LineCmd::Complete => todo!("Unhandled cmd: {cmd:?}"),
LineCmd::CompleteBackward => todo!("Unhandled cmd: {cmd:?}"),
LineCmd::CompleteHint => todo!("Unhandled cmd: {cmd:?}"),
LineCmd::DowncaseWord => todo!("Unhandled cmd: {cmd:?}"),
LineCmd::EndOfFile => {
if self.line.buffer.is_empty() {
sh_quit(0);
} else {
self.line.clear();
}
}
LineCmd::EndOfHistory => todo!("Unhandled cmd: {cmd:?}"),
LineCmd::ForwardSearchHistory => todo!("Unhandled cmd: {cmd:?}"),
LineCmd::HistorySearchBackward => todo!("Unhandled cmd: {cmd:?}"),
LineCmd::HistorySearchForward => todo!("Unhandled cmd: {cmd:?}"),
LineCmd::Insert(_) => todo!("Unhandled cmd: {cmd:?}"),
LineCmd::Interrupt => todo!("Unhandled cmd: {cmd:?}"),
LineCmd::Move(_) => todo!("Unhandled cmd: {cmd:?}"),
LineCmd::NextHistory => todo!("Unhandled cmd: {cmd:?}"),
LineCmd::Noop => todo!("Unhandled cmd: {cmd:?}"),
LineCmd::Repaint => todo!("Unhandled cmd: {cmd:?}"),
LineCmd::Overwrite(ch) => todo!("Unhandled cmd: {cmd:?}"),
LineCmd::PreviousHistory => todo!("Unhandled cmd: {cmd:?}"),
LineCmd::QuotedInsert => todo!("Unhandled cmd: {cmd:?}"),
LineCmd::ReverseSearchHistory => todo!("Unhandled cmd: {cmd:?}"),
LineCmd::Suspend => todo!("Unhandled cmd: {cmd:?}"),
LineCmd::TransposeChars => todo!("Unhandled cmd: {cmd:?}"),
LineCmd::TransposeWords => todo!("Unhandled cmd: {cmd:?}"),
LineCmd::Unknown => todo!("Unhandled cmd: {cmd:?}"),
LineCmd::YankPop => todo!("Unhandled cmd: {cmd:?}"),
LineCmd::LineUpOrPreviousHistory => todo!("Unhandled cmd: {cmd:?}"),
LineCmd::LineDownOrNextHistory => todo!("Unhandled cmd: {cmd:?}"),
LineCmd::Newline => todo!("Unhandled cmd: {cmd:?}"),
LineCmd::AcceptOrInsertLine { .. } => todo!("Unhandled cmd: {cmd:?}"),
LineCmd::Null => { /* Pass */ }
_ => todo!("Unhandled cmd: {cmd:?}"),
}
Ok(()) Ok(())
} }
} }
impl Drop for FernReader {
fn drop(&mut self) {
self.term.write("\x1b[2 q");
}
}

360
src/prompt/readline/mode.rs Normal file
View File

@@ -0,0 +1,360 @@
use super::keys::{KeyEvent as E, KeyCode as K, ModKeys as M};
use super::linebuf::TermChar;
use super::vicmd::{Anchor, Bound, Dest, Direction, Motion, MotionBuilder, TextObj, To, Verb, VerbBuilder, ViCmd, Word};
pub struct CmdReplay {
cmds: Vec<ViCmd>,
repeat: u16
}
impl CmdReplay {
pub fn new(cmds: Vec<ViCmd>, repeat: u16) -> Self {
Self { cmds, repeat }
}
}
pub trait ViMode {
fn handle_key(&mut self, key: E) -> Option<ViCmd>;
fn is_repeatable(&self) -> bool;
fn as_replay(&self) -> Option<CmdReplay>;
fn cursor_style(&self) -> String;
}
#[derive(Default,Debug)]
pub struct ViInsert {
cmds: Vec<ViCmd>,
pending_cmd: ViCmd,
repeat_count: u16
}
impl ViInsert {
pub fn new() -> Self {
Self::default()
}
pub fn with_count(mut self, repeat_count: u16) -> Self {
self.repeat_count = repeat_count;
self
}
pub fn register_and_return(&mut self) -> Option<ViCmd> {
let cmd = self.take_cmd();
self.register_cmd(&cmd);
return Some(cmd)
}
pub fn register_cmd(&mut self, cmd: &ViCmd) {
self.cmds.push(cmd.clone())
}
pub fn take_cmd(&mut self) -> ViCmd {
std::mem::take(&mut self.pending_cmd)
}
}
impl ViMode for ViInsert {
fn handle_key(&mut self, key: E) -> Option<ViCmd> {
match key {
E(K::Grapheme(ch), M::NONE) => {
let ch = TermChar::from(ch);
self.pending_cmd.set_verb(Verb::InsertChar(ch));
self.pending_cmd.set_motion(Motion::ForwardChar);
self.register_and_return()
}
E(K::Char(ch), M::NONE) => {
self.pending_cmd.set_verb(Verb::InsertChar(TermChar::from(ch)));
self.pending_cmd.set_motion(Motion::ForwardChar);
self.register_and_return()
}
E(K::Char('H'), M::CTRL) |
E(K::Backspace, M::NONE) => {
self.pending_cmd.set_verb(Verb::Delete);
self.pending_cmd.set_motion(Motion::BackwardChar);
self.register_and_return()
}
E(K::BackTab, M::NONE) => {
self.pending_cmd.set_verb(Verb::CompleteBackward);
self.register_and_return()
}
E(K::Char('I'), M::CTRL) |
E(K::Tab, M::NONE) => {
self.pending_cmd.set_verb(Verb::Complete);
self.register_and_return()
}
E(K::Esc, M::NONE) => {
self.pending_cmd.set_verb(Verb::NormalMode);
self.pending_cmd.set_motion(Motion::BackwardChar);
self.register_and_return()
}
_ => common_cmds(key)
}
}
fn is_repeatable(&self) -> bool {
true
}
fn as_replay(&self) -> Option<CmdReplay> {
Some(CmdReplay::new(self.cmds.clone(), self.repeat_count))
}
fn cursor_style(&self) -> String {
"\x1b[6 q".to_string()
}
}
#[derive(Default,Debug)]
pub struct ViNormal {
pending_cmd: ViCmd,
}
impl ViNormal {
pub fn new() -> Self {
Self::default()
}
pub fn take_cmd(&mut self) -> ViCmd {
std::mem::take(&mut self.pending_cmd)
}
pub fn clear_cmd(&mut self) {
self.pending_cmd = ViCmd::new();
}
fn handle_pending_builder(&mut self, key: E) -> Option<ViCmd> {
if self.pending_cmd.wants_register {
if let E(K::Char(ch @ ('a'..='z' | 'A'..='Z')), M::NONE) = key {
self.pending_cmd.set_register(ch);
return None
} else {
self.clear_cmd();
return None
}
} 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;
}
}
}
}
None
}
}
impl ViMode for ViNormal {
fn handle_key(&mut self, key: E) -> Option<ViCmd> {
if let E(K::Char(ch),M::NONE) = key {
self.pending_cmd.append_seq_char(ch);
}
if self.pending_cmd.is_building() {
return self.handle_pending_builder(key)
}
match key {
E(K::Char(digit @ '0'..='9'), M::NONE) => self.pending_cmd.append_digit(digit),
E(K::Char('"'),M::NONE) => {
if self.pending_cmd.is_empty() {
if self.pending_cmd.register().name().is_none() {
self.pending_cmd.wants_register = true;
} else {
self.clear_cmd();
}
} else {
self.clear_cmd();
}
return 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
}
}
fn is_repeatable(&self) -> bool {
false
}
fn as_replay(&self) -> Option<CmdReplay> {
None
}
fn cursor_style(&self) -> String {
"\x1b[2 q".to_string()
}
}
pub fn common_cmds(key: E) -> Option<ViCmd> {
let mut pending_cmd = ViCmd::new();
match key {
E(K::Home, M::NONE) => pending_cmd.set_motion(Motion::BeginningOfLine),
E(K::End, M::NONE) => pending_cmd.set_motion(Motion::EndOfLine),
E(K::Left, M::NONE) => pending_cmd.set_motion(Motion::BackwardChar),
E(K::Right, M::NONE) => pending_cmd.set_motion(Motion::ForwardChar),
E(K::Up, M::NONE) => pending_cmd.set_motion(Motion::LineUp),
E(K::Down, M::NONE) => pending_cmd.set_motion(Motion::LineDown),
E(K::Enter, M::NONE) => pending_cmd.set_verb(Verb::AcceptLine),
E(K::Char('D'), M::CTRL) => pending_cmd.set_verb(Verb::EndOfFile),
E(K::Backspace, M::NONE) |
E(K::Char('H'), M::CTRL) => {
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
}
Some(pending_cmd)
}

View File

@@ -0,0 +1,171 @@
use std::sync::Mutex;
use super::linebuf::TermCharBuf;
pub static REGISTERS: Mutex<Registers> = Mutex::new(Registers::new());
pub fn read_register(ch: Option<char>) -> Option<TermCharBuf> {
let lock = REGISTERS.lock().unwrap();
lock.get_reg(ch).map(|r| r.buf().clone())
}
pub fn write_register(ch: Option<char>, buf: TermCharBuf) {
let mut lock = REGISTERS.lock().unwrap();
if let Some(r) = lock.get_reg_mut(ch) { r.write(buf) }
}
pub fn append_register(ch: Option<char>, buf: TermCharBuf) {
let mut lock = REGISTERS.lock().unwrap();
if let Some(r) = lock.get_reg_mut(ch) { r.append(buf) }
}
#[derive(Default,Debug)]
pub struct Registers {
default: Register,
a: Register,
b: Register,
c: Register,
d: Register,
e: Register,
f: Register,
g: Register,
h: Register,
i: Register,
j: Register,
k: Register,
l: Register,
m: Register,
n: Register,
o: Register,
p: Register,
q: Register,
r: Register,
s: Register,
t: Register,
u: Register,
v: Register,
w: Register,
x: Register,
y: Register,
z: Register,
}
impl Registers {
pub const fn new() -> Self {
Self {
default: Register(TermCharBuf(vec![])),
a: Register(TermCharBuf(vec![])),
b: Register(TermCharBuf(vec![])),
c: Register(TermCharBuf(vec![])),
d: Register(TermCharBuf(vec![])),
e: Register(TermCharBuf(vec![])),
f: Register(TermCharBuf(vec![])),
g: Register(TermCharBuf(vec![])),
h: Register(TermCharBuf(vec![])),
i: Register(TermCharBuf(vec![])),
j: Register(TermCharBuf(vec![])),
k: Register(TermCharBuf(vec![])),
l: Register(TermCharBuf(vec![])),
m: Register(TermCharBuf(vec![])),
n: Register(TermCharBuf(vec![])),
o: Register(TermCharBuf(vec![])),
p: Register(TermCharBuf(vec![])),
q: Register(TermCharBuf(vec![])),
r: Register(TermCharBuf(vec![])),
s: Register(TermCharBuf(vec![])),
t: Register(TermCharBuf(vec![])),
u: Register(TermCharBuf(vec![])),
v: Register(TermCharBuf(vec![])),
w: Register(TermCharBuf(vec![])),
x: Register(TermCharBuf(vec![])),
y: Register(TermCharBuf(vec![])),
z: Register(TermCharBuf(vec![])),
}
}
pub fn get_reg(&self, ch: Option<char>) -> Option<&Register> {
let Some(ch) = ch else {
return Some(&self.default)
};
match ch {
'a' => Some(&self.a),
'b' => Some(&self.b),
'c' => Some(&self.c),
'd' => Some(&self.d),
'e' => Some(&self.e),
'f' => Some(&self.f),
'g' => Some(&self.g),
'h' => Some(&self.h),
'i' => Some(&self.i),
'j' => Some(&self.j),
'k' => Some(&self.k),
'l' => Some(&self.l),
'm' => Some(&self.m),
'n' => Some(&self.n),
'o' => Some(&self.o),
'p' => Some(&self.p),
'q' => Some(&self.q),
'r' => Some(&self.r),
's' => Some(&self.s),
't' => Some(&self.t),
'u' => Some(&self.u),
'v' => Some(&self.v),
'w' => Some(&self.w),
'x' => Some(&self.x),
'y' => Some(&self.y),
'z' => Some(&self.z),
_ => None
}
}
pub fn get_reg_mut(&mut self, ch: Option<char>) -> Option<&mut Register> {
let Some(ch) = ch else {
return Some(&mut self.default)
};
match ch {
'a' => Some(&mut self.a),
'b' => Some(&mut self.b),
'c' => Some(&mut self.c),
'd' => Some(&mut self.d),
'e' => Some(&mut self.e),
'f' => Some(&mut self.f),
'g' => Some(&mut self.g),
'h' => Some(&mut self.h),
'i' => Some(&mut self.i),
'j' => Some(&mut self.j),
'k' => Some(&mut self.k),
'l' => Some(&mut self.l),
'm' => Some(&mut self.m),
'n' => Some(&mut self.n),
'o' => Some(&mut self.o),
'p' => Some(&mut self.p),
'q' => Some(&mut self.q),
'r' => Some(&mut self.r),
's' => Some(&mut self.s),
't' => Some(&mut self.t),
'u' => Some(&mut self.u),
'v' => Some(&mut self.v),
'w' => Some(&mut self.w),
'x' => Some(&mut self.x),
'y' => Some(&mut self.y),
'z' => Some(&mut self.z),
_ => None
}
}
}
#[derive(Clone,Default,Debug)]
pub struct Register(TermCharBuf);
impl Register {
pub fn buf(&self) -> &TermCharBuf {
&self.0
}
pub fn write(&mut self, buf: TermCharBuf) {
self.0 = buf
}
pub fn append(&mut self, mut buf: TermCharBuf) {
self.0.0.append(&mut buf.0)
}
pub fn clear(&mut self) {
self.0.clear()
}
}

View File

@@ -1,5 +1,5 @@
use std::os::fd::{BorrowedFd, RawFd}; use std::{os::fd::{BorrowedFd, RawFd}, thread::sleep, time::{Duration, Instant}};
use nix::{libc::STDIN_FILENO, sys::termios, unistd::{isatty, read, write}}; use nix::{errno::Errno, fcntl::{fcntl, FcntlArg, OFlag}, libc::STDIN_FILENO, sys::termios, unistd::{isatty, read, write}};
use super::keys::{KeyCode, KeyEvent, ModKeys}; use super::keys::{KeyCode, KeyEvent, ModKeys};
@@ -48,6 +48,44 @@ impl Terminal {
}) })
} }
/// Same as read_byte(), only non-blocking with a very short timeout
pub fn peek_byte(&self, buf: &mut [u8]) -> usize {
const TIMEOUT_DUR: Duration = Duration::from_millis(50);
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 n
}
Ok(_) => {}
Err(e) if e == Errno::EAGAIN => {}
Err(e) => panic!("nonblocking read failed: {e}")
}
if start.elapsed() >= TIMEOUT_DUR {
self.read_blocks(true);
return 0
}
sleep(Duration::from_millis(1));
}
})
}
pub fn read_blocks(&self, yn: bool) {
let flags = OFlag::from_bits_truncate(fcntl(self.stdin, FcntlArg::F_GETFL).unwrap());
let new_flags = if !yn {
flags | OFlag::O_NONBLOCK
} else {
flags & !OFlag::O_NONBLOCK
};
fcntl(self.stdin, FcntlArg::F_SETFL(new_flags)).unwrap();
}
pub fn write_bytes(&self, buf: &[u8]) { pub fn write_bytes(&self, buf: &[u8]) {
Self::with_raw_mode(|| { Self::with_raw_mode(|| {
write(unsafe{BorrowedFd::borrow_raw(self.stdout)}, buf).expect("Failed to write to stdout"); write(unsafe{BorrowedFd::borrow_raw(self.stdout)}, buf).expect("Failed to write to stdout");
@@ -69,12 +107,33 @@ impl Terminal {
} }
pub fn read_key(&self) -> KeyEvent { pub fn read_key(&self) -> KeyEvent {
let mut buf = [0;8]; use core::str;
let n = self.read_byte(&mut buf);
if buf[0] == 0x1b { let mut buf = [0u8; 8];
if n >= 3 && buf[1] == b'[' { let mut collected = Vec::with_capacity(5);
return match buf[2] {
loop {
let n = self.read_byte(&mut buf[..1]); // Read one byte at a time
if n == 0 {
continue;
}
collected.push(buf[0]);
// ESC sequences
if collected[0] == 0x1b && collected.len() == 1 {
// Peek next byte if any
let n = self.peek_byte(&mut buf[..1]);
if n == 0 {
return KeyEvent(KeyCode::Esc, ModKeys::empty());
}
collected.push(buf[0]);
if buf[0] == b'[' {
// Read third byte
let _ = self.read_byte(&mut buf[..1]);
collected.push(buf[0]);
return match buf[0] {
b'A' => KeyEvent(KeyCode::Up, ModKeys::empty()), b'A' => KeyEvent(KeyCode::Up, ModKeys::empty()),
b'B' => KeyEvent(KeyCode::Down, ModKeys::empty()), b'B' => KeyEvent(KeyCode::Down, ModKeys::empty()),
b'C' => KeyEvent(KeyCode::Right, ModKeys::empty()), b'C' => KeyEvent(KeyCode::Right, ModKeys::empty()),
@@ -82,14 +141,22 @@ impl Terminal {
_ => KeyEvent(KeyCode::Esc, ModKeys::empty()), _ => KeyEvent(KeyCode::Esc, ModKeys::empty()),
}; };
} }
return KeyEvent(KeyCode::Esc, ModKeys::empty()); return KeyEvent(KeyCode::Esc, ModKeys::empty());
} }
if let Ok(s) = core::str::from_utf8(&buf[..n]) { // Try parse valid UTF-8 from collected bytes
if let Some(ch) = s.chars().next() { if let Ok(s) = str::from_utf8(&collected) {
return KeyEvent::new(ch, ModKeys::NONE); return KeyEvent::new(s, ModKeys::empty());
}
// If it's not valid UTF-8 yet, loop to collect more bytes
if collected.len() >= 4 {
// UTF-8 max char length is 4; if it's still invalid, give up
break;
} }
} }
KeyEvent(KeyCode::Null, ModKeys::empty()) KeyEvent(KeyCode::Null, ModKeys::empty())
} }

View File

@@ -0,0 +1,294 @@
use super::{linebuf::{TermChar, TermCharBuf}, register::{append_register, read_register, write_register}};
#[derive(Clone,Copy,Default,Debug)]
pub struct RegisterName {
name: Option<char>,
append: bool
}
impl RegisterName {
pub fn name(&self) -> Option<char> {
self.name
}
pub fn is_append(&self) -> bool {
self.append
}
pub fn write_to_register(&self, buf: TermCharBuf) {
if self.append {
append_register(self.name, buf);
} else {
write_register(self.name, buf);
}
}
pub fn read_from_register(&self) -> Option<TermCharBuf> {
read_register(self.name)
}
}
#[derive(Clone,Default,Debug)]
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,
/// Verb to perform
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,
}
impl ViCmd {
pub fn new() -> Self {
Self::default()
}
pub fn set_register(&mut self, register: char) {
let append = register.is_uppercase();
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) {
self.raw_seq.push(ch)
}
pub fn is_empty(&self) -> bool {
!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()
}
pub fn verb_count(&self) -> u16 {
self.verb_count.unwrap_or(1)
}
pub fn motion(&self) -> Option<&Motion> {
self.motion.as_ref()
}
pub fn motion_count(&self) -> u16 {
self.motion_count.unwrap_or(1)
}
pub fn append_digit(&mut self, digit: char) {
// Convert char digit to a number (assuming ASCII '0'..'9')
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 {
matches!(self.verb, Some(Verb::Builder(_))) ||
matches!(self.motion, Some(Motion::Builder(_))) ||
self.wants_register
}
pub fn is_complete(&self) -> bool {
!(
(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 {
self.verb.as_ref().is_some_and(|v| *v == Verb::AcceptLine)
}
pub fn is_mode_transition(&self) -> bool {
self.verb.as_ref().is_some_and(|v| {
matches!(*v, Verb::InsertMode | Verb::NormalMode | Verb::OverwriteMode | Verb::VisualMode)
})
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
#[non_exhaustive]
pub enum Verb {
Delete,
DeleteChar(Anchor),
Change,
Yank,
ReplaceChar(char),
Substitute,
ToggleCase,
Complete,
CompleteBackward,
Undo,
RepeatLast,
Put(Anchor),
OverwriteMode,
InsertMode,
NormalMode,
VisualMode,
JoinLines,
InsertChar(TermChar),
Insert(String),
Breakline(Anchor),
Indent,
Dedent,
AcceptLine,
Builder(VerbBuilder),
EndOfFile
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum VerbBuilder {
}
impl Verb {
pub fn needs_motion(&self) -> bool {
matches!(self,
Self::Indent |
Self::Dedent |
Self::Delete |
Self::Change |
Self::Yank
)
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Motion {
/// Whole current line (not really a movement but a range)
WholeLine,
TextObj(TextObj, Bound),
BeginningOfFirstWord,
/// beginning-of-line
BeginningOfLine,
/// end-of-line
EndOfLine,
/// backward-word, vi-prev-word
BackwardWord(Word), // Backward until start of word
/// forward-word, vi-end-word, vi-next-word
ForwardWord(To, Word), // Forward until start/end of word
/// character-search, character-search-backward, vi-char-search
CharSearch(Direction,Dest,TermChar),
/// backward-char
BackwardChar,
/// forward-char
ForwardChar,
/// move to the same column on the previous line
LineUp,
/// move to the same column on the next line
LineDown,
/// Whole user input (not really a movement but a range)
WholeBuffer,
/// beginning-of-register
BeginningOfBuffer,
/// end-of-register
EndOfBuffer,
Builder(MotionBuilder),
Null
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum MotionBuilder {
CharSearch(Option<Direction>,Option<Dest>,Option<char>),
TextObj(Option<TextObj>,Option<Bound>)
}
impl Motion {
pub fn needs_verb(&self) -> bool {
matches!(self, Self::TextObj(_, _))
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Anchor {
After,
Before
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum TextObj {
/// `iw`, `aw` — inner word, around word
Word(Word),
/// for stuff like 'dd'
Line,
/// `is`, `as` — inner sentence, around sentence
Sentence,
/// `ip`, `ap` — inner paragraph, around paragraph
Paragraph,
/// `i"`, `a"` — inner/around double quotes
DoubleQuote,
/// `i'`, `a'`
SingleQuote,
/// `i\``, `a\``
BacktickQuote,
/// `i)`, `a)` — round parens
Paren,
/// `i]`, `a]`
Bracket,
/// `i}`, `a}`
Brace,
/// `i<`, `a<`
Angle,
/// `it`, `at` — HTML/XML tags (if you support it)
Tag,
/// Custom user-defined objects maybe?
Custom(char),
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Word {
Big,
Normal
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Bound {
Inside,
Around
}
#[derive(Default, Debug, Clone, Copy, Eq, PartialEq)]
pub enum Direction {
#[default]
Forward,
Backward
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Dest {
On,
Before,
After
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum To {
Start,
End
}

View File

@@ -1,86 +0,0 @@
use rustyline::{completion::Completer, hint::{Hint, Hinter}, history::SearchDirection, validate::{ValidationResult, Validator}, Helper};
use crate::{libsh::term::{Style, Styled}, parse::{lex::{LexFlags, LexStream}, ParseStream}};
use crate::prelude::*;
#[derive(Default,Debug)]
pub struct FernReadline;
impl FernReadline {
pub fn new() -> Self {
Self
}
pub fn search_hist(value: &str, ctx: &rustyline::Context<'_>) -> Option<String> {
let len = ctx.history().len();
for i in 0..len {
let entry = ctx.history().get(i, SearchDirection::Reverse).unwrap().unwrap();
if entry.entry.starts_with(value) {
return Some(entry.entry.into_owned())
}
}
None
}
}
impl Helper for FernReadline {}
impl Completer for FernReadline {
type Candidate = String;
}
pub struct FernHint {
raw: String,
styled: String
}
impl FernHint {
pub fn new(raw: String) -> Self {
let styled = (&raw).styled(Style::Dim | Style::BrightBlack);
Self { raw, styled }
}
}
impl Hint for FernHint {
fn display(&self) -> &str {
&self.styled
}
fn completion(&self) -> Option<&str> {
if !self.raw.is_empty() {
Some(&self.raw)
} else {
None
}
}
}
impl Hinter for FernReadline {
type Hint = FernHint;
fn hint(&self, line: &str, pos: usize, ctx: &rustyline::Context<'_>) -> Option<Self::Hint> {
if line.is_empty() {
return None
}
let ent = Self::search_hist(line,ctx)?;
let entry_raw = ent.get(pos..)?.to_string();
Some(FernHint::new(entry_raw))
}
}
impl Validator for FernReadline {
fn validate(&self, ctx: &mut rustyline::validate::ValidationContext) -> rustyline::Result<rustyline::validate::ValidationResult> {
let mut tokens = vec![];
let tk_stream = LexStream::new(Arc::new(ctx.input().to_string()), LexFlags::empty());
for tk in tk_stream {
if tk.is_err() {
return Ok(ValidationResult::Incomplete)
}
tokens.push(tk.unwrap());
}
let nd_stream = ParseStream::new(tokens);
for nd in nd_stream {
if nd.is_err() {
return Ok(ValidationResult::Incomplete)
}
}
Ok(ValidationResult::Valid(None))
}
}