further work on implementing vi features
This commit is contained in:
7
Cargo.lock
generated
7
Cargo.lock
generated
@@ -160,6 +160,7 @@ dependencies = [
|
||||
"nix",
|
||||
"pretty_assertions",
|
||||
"regex",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
@@ -336,6 +337,12 @@ version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.0"
|
||||
|
||||
@@ -17,6 +17,7 @@ insta = "1.42.2"
|
||||
nix = { version = "0.29.0", features = ["uio", "term", "user", "hostname", "fs", "default", "signal", "process", "event", "ioctl"] }
|
||||
pretty_assertions = "1.4.1"
|
||||
regex = "1.11.1"
|
||||
unicode-segmentation = "1.12.0"
|
||||
unicode-width = "0.2.0"
|
||||
|
||||
[[bin]]
|
||||
|
||||
@@ -3,7 +3,7 @@ pub mod highlight;
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use readline::FernReader;
|
||||
use readline::FernVi;
|
||||
|
||||
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> {
|
||||
let prompt = get_prompt()?;
|
||||
let mut reader = FernReader::new(prompt);
|
||||
let mut reader = FernVi::new(Some(prompt));
|
||||
reader.readline()
|
||||
}
|
||||
|
||||
@@ -1,31 +1,46 @@
|
||||
use std::sync::Arc;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
// Credit to Rustyline for the design ideas in this module
|
||||
// https://github.com/kkawakam/rustyline
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct KeyEvent(pub KeyCode, pub ModKeys);
|
||||
|
||||
|
||||
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};
|
||||
|
||||
if !ch.is_control() {
|
||||
if !mods.is_empty() {
|
||||
mods.remove(M::SHIFT); // TODO Validate: no SHIFT even if
|
||||
// `c` is uppercase
|
||||
let mut graphemes = ch.graphemes(true);
|
||||
|
||||
let first = match graphemes.next() {
|
||||
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);
|
||||
}
|
||||
match ch {
|
||||
'\x00' => E(K::Char('@'), mods | M::CTRL), // '\0'
|
||||
|
||||
let mut chars = first.chars();
|
||||
|
||||
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),
|
||||
'\x02' => E(K::Char('B'), mods | M::CTRL),
|
||||
'\x03' => E(K::Char('C'), mods | M::CTRL),
|
||||
'\x04' => E(K::Char('D'), mods | M::CTRL),
|
||||
'\x05' => E(K::Char('E'), mods | M::CTRL),
|
||||
'\x06' => E(K::Char('F'), mods | M::CTRL),
|
||||
'\x07' => E(K::Char('G'), mods | M::CTRL), // '\a'
|
||||
'\x08' => E(K::Backspace, mods), // '\b'
|
||||
'\x07' => E(K::Char('G'), mods | M::CTRL),
|
||||
'\x08' => E(K::Backspace, mods),
|
||||
'\x09' => {
|
||||
// '\t'
|
||||
if mods.contains(M::SHIFT) {
|
||||
mods.remove(M::SHIFT);
|
||||
E(K::BackTab, mods)
|
||||
@@ -33,10 +48,10 @@ impl KeyEvent {
|
||||
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),
|
||||
'\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),
|
||||
'\x0f' => E(K::Char('O'), 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),
|
||||
'\x19' => E(K::Char('Y'), 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),
|
||||
'\x1d' => E(K::Char(']'), mods | M::CTRL),
|
||||
'\x1e' => 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),
|
||||
_ => 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)]
|
||||
@@ -70,6 +100,7 @@ pub enum KeyCode {
|
||||
BracketedPasteStart,
|
||||
BracketedPasteEnd,
|
||||
Char(char),
|
||||
Grapheme(Arc<str>),
|
||||
Delete,
|
||||
Down,
|
||||
End,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
798
src/prompt/readline/linebuf.rs
Normal file
798
src/prompt/readline/linebuf.rs
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,86 +1,49 @@
|
||||
use std::{arch::asm, os::fd::BorrowedFd};
|
||||
use std::{collections::HashMap, sync::Mutex};
|
||||
|
||||
use keys::KeyEvent;
|
||||
use line::{strip_ansi_codes_and_escapes, LineBuf};
|
||||
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 linebuf::{strip_ansi_codes_and_escapes, LineBuf, TermCharBuf};
|
||||
use mode::{CmdReplay, ViInsert, ViMode, ViNormal};
|
||||
use term::Terminal;
|
||||
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 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
|
||||
///
|
||||
/// Returns the built value as a LineCmd::ViCmd
|
||||
macro_rules! build_verb {
|
||||
($cmd:expr,$verb:expr) => {{
|
||||
$cmd.with_verb($verb).build().map(|cmd| LineCmd::ViCmd(cmd))
|
||||
}}
|
||||
pub struct FernVi {
|
||||
term: Terminal,
|
||||
line: LineBuf,
|
||||
prompt: String,
|
||||
mode: Box<dyn ViMode>,
|
||||
repeat_action: Option<CmdReplay>,
|
||||
}
|
||||
|
||||
/// Add a movement to a specified ViCmdBuilder, then build it
|
||||
///
|
||||
/// Returns the built value as a LineCmd::ViCmd
|
||||
macro_rules! build_movement {
|
||||
($cmd:expr,$move:expr) => {{
|
||||
$cmd.with_movement($move).build().map(|cmd| LineCmd::ViCmd(cmd))
|
||||
}}
|
||||
}
|
||||
impl FernVi {
|
||||
pub fn new(prompt: Option<String>) -> Self {
|
||||
let prompt = prompt.unwrap_or("$ ".styled(Style::Green | Style::Bold));
|
||||
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");
|
||||
|
||||
/// 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 {
|
||||
term: Terminal::new(),
|
||||
prompt,
|
||||
line,
|
||||
edit_mode: Default::default(),
|
||||
last_vicmd: Default::default()
|
||||
prompt,
|
||||
mode: Box::new(ViInsert::new()),
|
||||
repeat_action: None,
|
||||
}
|
||||
}
|
||||
fn pack_line(&mut self) -> String {
|
||||
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) {
|
||||
pub fn clear_line(&self) {
|
||||
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;
|
||||
self.term.write_bytes(b"\r\n");
|
||||
for _ in 0..total {
|
||||
@@ -88,9 +51,9 @@ impl FernReader {
|
||||
}
|
||||
self.term.write_bytes(b"\r\x1b[2K");
|
||||
}
|
||||
fn display_line(&mut self, refresh: bool) {
|
||||
pub fn print_buf(&self, refresh: bool) {
|
||||
if refresh {
|
||||
self.clear_line();
|
||||
self.clear_line()
|
||||
}
|
||||
let mut prompt_lines = self.prompt.lines().peekable();
|
||||
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();
|
||||
// Y-axis movements are 1-indexed and must move up from the bottom
|
||||
// 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);
|
||||
let y = num_lines.saturating_sub(y + 1);
|
||||
|
||||
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 {
|
||||
InputMode::Replace |
|
||||
InputMode::Insert => {
|
||||
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};
|
||||
pub fn readline(&mut self) -> ShResult<String> {
|
||||
self.print_buf(false);
|
||||
loop {
|
||||
let key = self.term.read_key();
|
||||
let cmd = match key {
|
||||
E(K::Char(ch), M::NONE) => build_verb!(pending_cmd, Verb::InsertChar(ch))?,
|
||||
|
||||
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)
|
||||
}
|
||||
let Some(cmd) = self.mode.handle_key(key) else {
|
||||
continue
|
||||
};
|
||||
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> {
|
||||
use keys::{KeyEvent as E, KeyCode as K, ModKeys as M};
|
||||
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))
|
||||
self.exec_cmd(cmd.clone())?;
|
||||
self.print_buf(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let E(K::Char(digit @ '0'..='9'), M::NONE) = key {
|
||||
pending_cmd.append_digit(digit);
|
||||
return self.get_normal_cmd(pending_cmd);
|
||||
}
|
||||
let cmd = match key {
|
||||
E(K::Char('h'), M::NONE) => {
|
||||
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)
|
||||
}
|
||||
pub fn exec_cmd(&mut self, cmd: ViCmd) -> ShResult<()> {
|
||||
if cmd.is_mode_transition() {
|
||||
let count = cmd.verb_count();
|
||||
let mut mode: Box<dyn ViMode> = match cmd.verb().unwrap() {
|
||||
Verb::InsertMode => Box::new(ViInsert::new().with_count(count)),
|
||||
Verb::NormalMode => Box::new(ViNormal::new()),
|
||||
Verb::VisualMode => todo!(),
|
||||
Verb::OverwriteMode => todo!(),
|
||||
_ => unreachable!()
|
||||
};
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
pub fn common_cmd(&mut self, key: KeyEvent, pending_cmd: ViCmdBuilder) -> ShResult<LineCmd> {
|
||||
use keys::{KeyEvent as E, KeyCode as K, ModKeys as M};
|
||||
match key {
|
||||
E(K::Home, M::NONE) => build_movement!(pending_cmd,Movement::BeginningOfLine),
|
||||
E(K::End, M::NONE) => build_movement!(pending_cmd,Movement::EndOfLine),
|
||||
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:?}")))
|
||||
std::mem::swap(&mut mode, &mut self.mode);
|
||||
self.term.write(&mode.cursor_style());
|
||||
|
||||
if mode.is_repeatable() {
|
||||
self.repeat_action = mode.as_replay();
|
||||
}
|
||||
}
|
||||
pub fn handle_repeat(&mut self, cmd: &ViCmd) -> ShResult<()> {
|
||||
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:?}"),
|
||||
}
|
||||
self.line.exec_cmd(cmd)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for FernReader {
|
||||
fn drop(&mut self) {
|
||||
self.term.write("\x1b[2 q");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
360
src/prompt/readline/mode.rs
Normal file
360
src/prompt/readline/mode.rs
Normal 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)
|
||||
}
|
||||
171
src/prompt/readline/register.rs
Normal file
171
src/prompt/readline/register.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::os::fd::{BorrowedFd, RawFd};
|
||||
use nix::{libc::STDIN_FILENO, sys::termios, unistd::{isatty, read, write}};
|
||||
use std::{os::fd::{BorrowedFd, RawFd}, thread::sleep, time::{Duration, Instant}};
|
||||
use nix::{errno::Errno, fcntl::{fcntl, FcntlArg, OFlag}, libc::STDIN_FILENO, sys::termios, unistd::{isatty, read, write}};
|
||||
|
||||
use 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]) {
|
||||
Self::with_raw_mode(|| {
|
||||
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 {
|
||||
let mut buf = [0;8];
|
||||
let n = self.read_byte(&mut buf);
|
||||
use core::str;
|
||||
|
||||
if buf[0] == 0x1b {
|
||||
if n >= 3 && buf[1] == b'[' {
|
||||
return match buf[2] {
|
||||
let mut buf = [0u8; 8];
|
||||
let mut collected = Vec::with_capacity(5);
|
||||
|
||||
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'B' => KeyEvent(KeyCode::Down, ModKeys::empty()),
|
||||
b'C' => KeyEvent(KeyCode::Right, ModKeys::empty()),
|
||||
@@ -82,14 +141,22 @@ impl Terminal {
|
||||
_ => KeyEvent(KeyCode::Esc, ModKeys::empty()),
|
||||
};
|
||||
}
|
||||
|
||||
return KeyEvent(KeyCode::Esc, ModKeys::empty());
|
||||
}
|
||||
|
||||
if let Ok(s) = core::str::from_utf8(&buf[..n]) {
|
||||
if let Some(ch) = s.chars().next() {
|
||||
return KeyEvent::new(ch, ModKeys::NONE);
|
||||
// Try parse valid UTF-8 from collected bytes
|
||||
if let Ok(s) = str::from_utf8(&collected) {
|
||||
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())
|
||||
}
|
||||
|
||||
|
||||
294
src/prompt/readline/vicmd.rs
Normal file
294
src/prompt/readline/vicmd.rs
Normal 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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user