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",
|
"nix",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"regex",
|
"regex",
|
||||||
|
"unicode-segmentation",
|
||||||
"unicode-width",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -336,6 +337,12 @@ version = "1.0.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe"
|
checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-segmentation"
|
||||||
|
version = "1.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-width"
|
name = "unicode-width"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ insta = "1.42.2"
|
|||||||
nix = { version = "0.29.0", features = ["uio", "term", "user", "hostname", "fs", "default", "signal", "process", "event", "ioctl"] }
|
nix = { version = "0.29.0", features = ["uio", "term", "user", "hostname", "fs", "default", "signal", "process", "event", "ioctl"] }
|
||||||
pretty_assertions = "1.4.1"
|
pretty_assertions = "1.4.1"
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
|
unicode-segmentation = "1.12.0"
|
||||||
unicode-width = "0.2.0"
|
unicode-width = "0.2.0"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ pub mod highlight;
|
|||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use readline::FernReader;
|
use readline::FernVi;
|
||||||
|
|
||||||
use crate::{expand::expand_prompt, libsh::error::ShResult, prelude::*, state::read_shopts};
|
use crate::{expand::expand_prompt, libsh::error::ShResult, prelude::*, state::read_shopts};
|
||||||
|
|
||||||
@@ -22,6 +22,6 @@ fn get_prompt() -> ShResult<String> {
|
|||||||
|
|
||||||
pub fn read_line() -> ShResult<String> {
|
pub fn read_line() -> ShResult<String> {
|
||||||
let prompt = get_prompt()?;
|
let prompt = get_prompt()?;
|
||||||
let mut reader = FernReader::new(prompt);
|
let mut reader = FernVi::new(Some(prompt));
|
||||||
reader.readline()
|
reader.readline()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,63 +1,93 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
// Credit to Rustyline for the design ideas in this module
|
// Credit to Rustyline for the design ideas in this module
|
||||||
// https://github.com/kkawakam/rustyline
|
// https://github.com/kkawakam/rustyline
|
||||||
#[derive(Clone,Debug)]
|
#[derive(Clone,Debug)]
|
||||||
pub struct KeyEvent(pub KeyCode, pub ModKeys);
|
pub struct KeyEvent(pub KeyCode, pub ModKeys);
|
||||||
|
|
||||||
|
|
||||||
impl KeyEvent {
|
impl KeyEvent {
|
||||||
pub fn new(ch: char, mut mods: ModKeys) -> Self {
|
pub fn new(ch: &str, mut mods: ModKeys) -> Self {
|
||||||
use {KeyCode as K, KeyEvent as E, ModKeys as M};
|
use {KeyCode as K, KeyEvent as E, ModKeys as M};
|
||||||
|
|
||||||
if !ch.is_control() {
|
let mut graphemes = ch.graphemes(true);
|
||||||
if !mods.is_empty() {
|
|
||||||
mods.remove(M::SHIFT); // TODO Validate: no SHIFT even if
|
let first = match graphemes.next() {
|
||||||
// `c` is uppercase
|
Some(g) => g,
|
||||||
}
|
None => return E(K::Null, mods),
|
||||||
return E(K::Char(ch), 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
|
||||||
}
|
}
|
||||||
match ch {
|
|
||||||
'\x00' => E(K::Char('@'), mods | M::CTRL), // '\0'
|
let mut chars = first.chars();
|
||||||
'\x01' => E(K::Char('A'), mods | M::CTRL),
|
|
||||||
'\x02' => E(K::Char('B'), mods | M::CTRL),
|
let single_char = chars.next();
|
||||||
'\x03' => E(K::Char('C'), mods | M::CTRL),
|
let is_single_char = chars.next().is_none();
|
||||||
'\x04' => E(K::Char('D'), mods | M::CTRL),
|
|
||||||
'\x05' => E(K::Char('E'), mods | M::CTRL),
|
match single_char {
|
||||||
'\x06' => E(K::Char('F'), mods | M::CTRL),
|
Some(c) if is_single_char && c.is_control() => {
|
||||||
'\x07' => E(K::Char('G'), mods | M::CTRL), // '\a'
|
match c {
|
||||||
'\x08' => E(K::Backspace, mods), // '\b'
|
'\x00' => E(K::Char('@'), mods | M::CTRL),
|
||||||
'\x09' => {
|
'\x01' => E(K::Char('A'), mods | M::CTRL),
|
||||||
// '\t'
|
'\x02' => E(K::Char('B'), mods | M::CTRL),
|
||||||
if mods.contains(M::SHIFT) {
|
'\x03' => E(K::Char('C'), mods | M::CTRL),
|
||||||
mods.remove(M::SHIFT);
|
'\x04' => E(K::Char('D'), mods | M::CTRL),
|
||||||
E(K::BackTab, mods)
|
'\x05' => E(K::Char('E'), mods | M::CTRL),
|
||||||
} else {
|
'\x06' => E(K::Char('F'), mods | M::CTRL),
|
||||||
E(K::Tab, mods)
|
'\x07' => E(K::Char('G'), mods | M::CTRL),
|
||||||
|
'\x08' => E(K::Backspace, mods),
|
||||||
|
'\x09' => {
|
||||||
|
if mods.contains(M::SHIFT) {
|
||||||
|
mods.remove(M::SHIFT);
|
||||||
|
E(K::BackTab, mods)
|
||||||
|
} else {
|
||||||
|
E(K::Tab, mods)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'\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),
|
||||||
|
'\x0e' => E(K::Char('N'), mods | M::CTRL),
|
||||||
|
'\x0f' => E(K::Char('O'), mods | M::CTRL),
|
||||||
|
'\x10' => E(K::Char('P'), mods | M::CTRL),
|
||||||
|
'\x11' => E(K::Char('Q'), mods | M::CTRL),
|
||||||
|
'\x12' => E(K::Char('R'), mods | M::CTRL),
|
||||||
|
'\x13' => E(K::Char('S'), mods | M::CTRL),
|
||||||
|
'\x14' => E(K::Char('T'), mods | M::CTRL),
|
||||||
|
'\x15' => E(K::Char('U'), mods | M::CTRL),
|
||||||
|
'\x16' => E(K::Char('V'), mods | M::CTRL),
|
||||||
|
'\x17' => E(K::Char('W'), mods | M::CTRL),
|
||||||
|
'\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),
|
||||||
|
'\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),
|
||||||
|
'\u{9b}' => E(K::Esc, mods | M::SHIFT),
|
||||||
|
_ => E(K::Null, mods),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'\x0a' => E(K::Char('J'), mods | M::CTRL), // '\n' (10)
|
Some(c) if is_single_char => {
|
||||||
'\x0b' => E(K::Char('K'), mods | M::CTRL),
|
if !mods.is_empty() {
|
||||||
'\x0c' => E(K::Char('L'), mods | M::CTRL),
|
mods.remove(M::SHIFT);
|
||||||
'\x0d' => E(K::Enter, mods), // '\r' (13)
|
}
|
||||||
'\x0e' => E(K::Char('N'), mods | M::CTRL),
|
E(K::Char(c), mods)
|
||||||
'\x0f' => E(K::Char('O'), mods | M::CTRL),
|
}
|
||||||
'\x10' => E(K::Char('P'), mods | M::CTRL),
|
_ => {
|
||||||
'\x11' => E(K::Char('Q'), mods | M::CTRL),
|
// multi-char grapheme (emoji, accented, etc)
|
||||||
'\x12' => E(K::Char('R'), mods | M::CTRL),
|
if !mods.is_empty() {
|
||||||
'\x13' => E(K::Char('S'), mods | M::CTRL),
|
mods.remove(M::SHIFT);
|
||||||
'\x14' => E(K::Char('T'), mods | M::CTRL),
|
}
|
||||||
'\x15' => E(K::Char('U'), mods | M::CTRL),
|
E(K::Grapheme(Arc::from(first)), mods)
|
||||||
'\x16' => E(K::Char('V'), mods | M::CTRL),
|
}
|
||||||
'\x17' => E(K::Char('W'), mods | M::CTRL),
|
|
||||||
'\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'
|
|
||||||
'\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-?
|
|
||||||
'\u{9b}' => E(K::Esc, mods | M::SHIFT),
|
|
||||||
_ => E(K::Null, mods),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,6 +100,7 @@ pub enum KeyCode {
|
|||||||
BracketedPasteStart,
|
BracketedPasteStart,
|
||||||
BracketedPasteEnd,
|
BracketedPasteEnd,
|
||||||
Char(char),
|
Char(char),
|
||||||
|
Grapheme(Arc<str>),
|
||||||
Delete,
|
Delete,
|
||||||
Down,
|
Down,
|
||||||
End,
|
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 linebuf::{strip_ansi_codes_and_escapes, LineBuf, TermCharBuf};
|
||||||
use line::{strip_ansi_codes_and_escapes, LineBuf};
|
use mode::{CmdReplay, ViInsert, ViMode, ViNormal};
|
||||||
use linecmd::{Anchor, At, CharSearch, InputMode, LineCmd, MoveCmd, Movement, Verb, VerbCmd, ViCmd, ViCmdBuilder, Word};
|
|
||||||
use nix::{libc::STDIN_FILENO, sys::termios::{self, Termios}, unistd::read};
|
|
||||||
use term::Terminal;
|
use term::Terminal;
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
use vicmd::{Verb, ViCmd};
|
||||||
|
|
||||||
|
use crate::libsh::{error::ShResult, term::{Style, Styled}};
|
||||||
|
|
||||||
use crate::{libsh::{error::{ShErr, ShErrKind, ShResult}, sys::sh_quit}, prelude::*};
|
|
||||||
use linecmd::Repeat;
|
|
||||||
pub mod term;
|
|
||||||
pub mod line;
|
|
||||||
pub mod keys;
|
pub mod keys;
|
||||||
pub mod linecmd;
|
pub mod term;
|
||||||
|
pub mod linebuf;
|
||||||
|
pub mod vicmd;
|
||||||
|
pub mod mode;
|
||||||
|
pub mod register;
|
||||||
|
|
||||||
/// Add a verb to a specified ViCmdBuilder, then build it
|
pub struct FernVi {
|
||||||
///
|
term: Terminal,
|
||||||
/// Returns the built value as a LineCmd::ViCmd
|
line: LineBuf,
|
||||||
macro_rules! build_verb {
|
prompt: String,
|
||||||
($cmd:expr,$verb:expr) => {{
|
mode: Box<dyn ViMode>,
|
||||||
$cmd.with_verb($verb).build().map(|cmd| LineCmd::ViCmd(cmd))
|
repeat_action: Option<CmdReplay>,
|
||||||
}}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a movement to a specified ViCmdBuilder, then build it
|
impl FernVi {
|
||||||
///
|
pub fn new(prompt: Option<String>) -> Self {
|
||||||
/// Returns the built value as a LineCmd::ViCmd
|
let prompt = prompt.unwrap_or("$ ".styled(Style::Green | Style::Bold));
|
||||||
macro_rules! build_movement {
|
let line = LineBuf::new().with_initial("The quick brown fox jumps over the lazy dog");//\nThe quick brown fox jumps over the lazy dog\nThe quick brown fox jumps over the lazy dog\n");
|
||||||
($cmd:expr,$move:expr) => {{
|
|
||||||
$cmd.with_movement($move).build().map(|cmd| LineCmd::ViCmd(cmd))
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add both a movement and a verb to a specified ViCmdBuilder, then build it
|
|
||||||
///
|
|
||||||
/// Returns the built value as a LineCmd::ViCmd
|
|
||||||
macro_rules! build_moveverb {
|
|
||||||
($cmd:expr,$verb:expr,$move:expr) => {{
|
|
||||||
$cmd.with_movement($move).with_verb($verb).build().map(|cmd| LineCmd::ViCmd(cmd))
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default,Debug)]
|
|
||||||
pub struct FernReader {
|
|
||||||
pub term: Terminal,
|
|
||||||
pub prompt: String,
|
|
||||||
pub line: LineBuf,
|
|
||||||
pub edit_mode: InputMode,
|
|
||||||
pub last_vicmd: Option<Repeat>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FernReader {
|
|
||||||
pub fn new(prompt: String) -> Self {
|
|
||||||
let line = LineBuf::new().with_initial("The quick brown fox jumped over the lazy dog.");
|
|
||||||
Self {
|
Self {
|
||||||
term: Terminal::new(),
|
term: Terminal::new(),
|
||||||
prompt,
|
|
||||||
line,
|
line,
|
||||||
edit_mode: Default::default(),
|
prompt,
|
||||||
last_vicmd: Default::default()
|
mode: Box::new(ViInsert::new()),
|
||||||
|
repeat_action: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn pack_line(&mut self) -> String {
|
pub fn clear_line(&self) {
|
||||||
self.line
|
|
||||||
.buffer
|
|
||||||
.iter()
|
|
||||||
.collect::<String>()
|
|
||||||
}
|
|
||||||
pub fn readline(&mut self) -> ShResult<String> {
|
|
||||||
self.display_line(/*refresh: */ false);
|
|
||||||
loop {
|
|
||||||
let cmd = self.next_cmd()?;
|
|
||||||
if cmd == LineCmd::AcceptLine {
|
|
||||||
return Ok(self.pack_line())
|
|
||||||
}
|
|
||||||
self.execute_cmd(cmd)?;
|
|
||||||
self.display_line(/* refresh: */ true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn clear_line(&self) {
|
|
||||||
let prompt_lines = self.prompt.lines().count();
|
let prompt_lines = self.prompt.lines().count();
|
||||||
let buf_lines = self.line.count_lines().saturating_sub(1); // One of the buffer's lines will overlap with the prompt. probably.
|
let buf_lines = if self.prompt.ends_with('\n') {
|
||||||
|
self.line.count_lines()
|
||||||
|
} else {
|
||||||
|
// The prompt does not end with a newline, so one of the buffer's lines overlaps with it
|
||||||
|
self.line.count_lines().saturating_sub(1)
|
||||||
|
};
|
||||||
let total = prompt_lines + buf_lines;
|
let total = prompt_lines + buf_lines;
|
||||||
self.term.write_bytes(b"\r\n");
|
self.term.write_bytes(b"\r\n");
|
||||||
for _ in 0..total {
|
for _ in 0..total {
|
||||||
@@ -88,9 +51,9 @@ impl FernReader {
|
|||||||
}
|
}
|
||||||
self.term.write_bytes(b"\r\x1b[2K");
|
self.term.write_bytes(b"\r\x1b[2K");
|
||||||
}
|
}
|
||||||
fn display_line(&mut self, refresh: bool) {
|
pub fn print_buf(&self, refresh: bool) {
|
||||||
if refresh {
|
if refresh {
|
||||||
self.clear_line();
|
self.clear_line()
|
||||||
}
|
}
|
||||||
let mut prompt_lines = self.prompt.lines().peekable();
|
let mut prompt_lines = self.prompt.lines().peekable();
|
||||||
let mut last_line_len = 0;
|
let mut last_line_len = 0;
|
||||||
@@ -114,315 +77,54 @@ impl FernReader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if num_lines == 1 {
|
let (x, y) = self.line.cursor_display_coords();
|
||||||
let cursor_offset = self.line.cursor() + last_line_len;
|
let y = num_lines.saturating_sub(y + 1);
|
||||||
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);
|
|
||||||
if y > 0 {
|
|
||||||
self.term.write(&format!("\r\x1b[{}A", y))
|
|
||||||
}
|
|
||||||
self.term.write(&format!("\r\x1b[{}C", x+2)); // Factor in the line bullet thing
|
|
||||||
}
|
|
||||||
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};
|
|
||||||
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) |
|
if y > 0 {
|
||||||
E(K::Backspace, M::NONE) => LineCmd::backspace(),
|
self.term.write(&format!("\r\x1b[{}A", y));
|
||||||
|
|
||||||
E(K::BackTab, M::NONE) => LineCmd::CompleteBackward,
|
|
||||||
|
|
||||||
E(K::Char('I'), M::CTRL) |
|
|
||||||
E(K::Tab, M::NONE) => LineCmd::Complete,
|
|
||||||
|
|
||||||
E(K::Esc, M::NONE) => {
|
|
||||||
build_movement!(pending_cmd, Movement::BackwardChar)?
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
flog!(INFO, "unhandled key in get_insert_cmd, trying common_cmd...");
|
|
||||||
return self.common_cmd(key, pending_cmd)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let E(K::Char(digit @ '0'..='9'), M::NONE) = key {
|
// Add prompt offset to X only if cursor is on the last line (y == 0)
|
||||||
pending_cmd.append_digit(digit);
|
let cursor_x = if y == 0 { x + last_line_len } else { x };
|
||||||
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)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn common_cmd(&mut self, key: KeyEvent, pending_cmd: ViCmdBuilder) -> ShResult<LineCmd> {
|
self.term.write(&format!("\r\x1b[{}C", cursor_x));
|
||||||
use keys::{KeyEvent as E, KeyCode as K, ModKeys as M};
|
self.term.write(&self.mode.cursor_style());
|
||||||
match key {
|
}
|
||||||
E(K::Home, M::NONE) => build_movement!(pending_cmd,Movement::BeginningOfLine),
|
pub fn readline(&mut self) -> ShResult<String> {
|
||||||
E(K::End, M::NONE) => build_movement!(pending_cmd,Movement::EndOfLine),
|
self.print_buf(false);
|
||||||
E(K::Left, M::NONE) => build_movement!(pending_cmd,Movement::BackwardChar),
|
loop {
|
||||||
E(K::Right, M::NONE) => build_movement!(pending_cmd,Movement::ForwardChar),
|
let key = self.term.read_key();
|
||||||
E(K::Delete, M::NONE) => build_moveverb!(pending_cmd,Verb::Delete,Movement::ForwardChar),
|
let Some(cmd) = self.mode.handle_key(key) else {
|
||||||
E(K::Up, M::NONE) => Ok(LineCmd::LineUpOrPreviousHistory),
|
continue
|
||||||
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),
|
if cmd.should_submit() {
|
||||||
E(K::Backspace, M::NONE) |
|
return Ok(self.line.to_string());
|
||||||
E(K::Char('h'), M::CTRL) => {
|
|
||||||
Ok(LineCmd::backspace())
|
|
||||||
}
|
}
|
||||||
_ => Err(ShErr::simple(ShErrKind::ReadlineErr,format!("Unhandled common key event: {key:?}")))
|
|
||||||
|
self.exec_cmd(cmd.clone())?;
|
||||||
|
self.print_buf(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn handle_repeat(&mut self, cmd: &ViCmd) -> ShResult<()> {
|
pub fn exec_cmd(&mut self, cmd: ViCmd) -> ShResult<()> {
|
||||||
Ok(())
|
if cmd.is_mode_transition() {
|
||||||
}
|
let count = cmd.verb_count();
|
||||||
pub fn exec_vi_cmd(&mut self, cmd: ViCmd) -> ShResult<()> {
|
let mut mode: Box<dyn ViMode> = match cmd.verb().unwrap() {
|
||||||
self.last_vicmd = Some(Repeat::from_cmd(cmd.clone()));
|
Verb::InsertMode => Box::new(ViInsert::new().with_count(count)),
|
||||||
match cmd {
|
Verb::NormalMode => Box::new(ViNormal::new()),
|
||||||
ViCmd::MoveVerb(verb_cmd, move_cmd) => {
|
Verb::VisualMode => todo!(),
|
||||||
let VerbCmd { verb_count, verb } = verb_cmd;
|
Verb::OverwriteMode => todo!(),
|
||||||
for _ in 0..verb_count {
|
_ => unreachable!()
|
||||||
self.line.exec_vi_cmd(Some(verb.clone()), Some(move_cmd.clone()))?;
|
};
|
||||||
}
|
|
||||||
if verb == Verb::Change {
|
std::mem::swap(&mut mode, &mut self.mode);
|
||||||
self.set_insert_mode();
|
self.term.write(&mode.cursor_style());
|
||||||
}
|
|
||||||
}
|
if mode.is_repeatable() {
|
||||||
ViCmd::Verb(verb_cmd) => {
|
self.repeat_action = mode.as_replay();
|
||||||
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(())
|
self.line.exec_cmd(cmd)?;
|
||||||
}
|
|
||||||
pub fn execute_cmd(&mut self, cmd: LineCmd) -> ShResult<()> {
|
|
||||||
match cmd {
|
|
||||||
LineCmd::ViCmd(cmd) => self.exec_vi_cmd(cmd)?,
|
|
||||||
LineCmd::Abort => todo!("Unhandled cmd: {cmd:?}"),
|
|
||||||
LineCmd::BeginningOfHistory => todo!("Unhandled cmd: {cmd:?}"),
|
|
||||||
LineCmd::CapitalizeWord => todo!("Unhandled cmd: {cmd:?}"),
|
|
||||||
LineCmd::ClearScreen => todo!("Unhandled cmd: {cmd:?}"),
|
|
||||||
LineCmd::Complete => todo!("Unhandled cmd: {cmd:?}"),
|
|
||||||
LineCmd::CompleteBackward => todo!("Unhandled cmd: {cmd:?}"),
|
|
||||||
LineCmd::CompleteHint => todo!("Unhandled cmd: {cmd:?}"),
|
|
||||||
LineCmd::DowncaseWord => todo!("Unhandled cmd: {cmd:?}"),
|
|
||||||
LineCmd::EndOfFile => {
|
|
||||||
if self.line.buffer.is_empty() {
|
|
||||||
sh_quit(0);
|
|
||||||
} else {
|
|
||||||
self.line.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LineCmd::EndOfHistory => todo!("Unhandled cmd: {cmd:?}"),
|
|
||||||
LineCmd::ForwardSearchHistory => todo!("Unhandled cmd: {cmd:?}"),
|
|
||||||
LineCmd::HistorySearchBackward => todo!("Unhandled cmd: {cmd:?}"),
|
|
||||||
LineCmd::HistorySearchForward => todo!("Unhandled cmd: {cmd:?}"),
|
|
||||||
LineCmd::Insert(_) => todo!("Unhandled cmd: {cmd:?}"),
|
|
||||||
LineCmd::Interrupt => todo!("Unhandled cmd: {cmd:?}"),
|
|
||||||
LineCmd::Move(_) => todo!("Unhandled cmd: {cmd:?}"),
|
|
||||||
LineCmd::NextHistory => todo!("Unhandled cmd: {cmd:?}"),
|
|
||||||
LineCmd::Noop => todo!("Unhandled cmd: {cmd:?}"),
|
|
||||||
LineCmd::Repaint => todo!("Unhandled cmd: {cmd:?}"),
|
|
||||||
LineCmd::Overwrite(ch) => todo!("Unhandled cmd: {cmd:?}"),
|
|
||||||
LineCmd::PreviousHistory => todo!("Unhandled cmd: {cmd:?}"),
|
|
||||||
LineCmd::QuotedInsert => todo!("Unhandled cmd: {cmd:?}"),
|
|
||||||
LineCmd::ReverseSearchHistory => todo!("Unhandled cmd: {cmd:?}"),
|
|
||||||
LineCmd::Suspend => todo!("Unhandled cmd: {cmd:?}"),
|
|
||||||
LineCmd::TransposeChars => todo!("Unhandled cmd: {cmd:?}"),
|
|
||||||
LineCmd::TransposeWords => todo!("Unhandled cmd: {cmd:?}"),
|
|
||||||
LineCmd::Unknown => todo!("Unhandled cmd: {cmd:?}"),
|
|
||||||
LineCmd::YankPop => todo!("Unhandled cmd: {cmd:?}"),
|
|
||||||
LineCmd::LineUpOrPreviousHistory => todo!("Unhandled cmd: {cmd:?}"),
|
|
||||||
LineCmd::LineDownOrNextHistory => todo!("Unhandled cmd: {cmd:?}"),
|
|
||||||
LineCmd::Newline => todo!("Unhandled cmd: {cmd:?}"),
|
|
||||||
LineCmd::AcceptOrInsertLine { .. } => todo!("Unhandled cmd: {cmd:?}"),
|
|
||||||
LineCmd::Null => { /* Pass */ }
|
|
||||||
_ => todo!("Unhandled cmd: {cmd:?}"),
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for FernReader {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
self.term.write("\x1b[2 q");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
360
src/prompt/readline/mode.rs
Normal file
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 std::{os::fd::{BorrowedFd, RawFd}, thread::sleep, time::{Duration, Instant}};
|
||||||
use nix::{libc::STDIN_FILENO, sys::termios, unistd::{isatty, read, write}};
|
use nix::{errno::Errno, fcntl::{fcntl, FcntlArg, OFlag}, libc::STDIN_FILENO, sys::termios, unistd::{isatty, read, write}};
|
||||||
|
|
||||||
use super::keys::{KeyCode, KeyEvent, ModKeys};
|
use super::keys::{KeyCode, KeyEvent, ModKeys};
|
||||||
|
|
||||||
@@ -48,6 +48,44 @@ impl Terminal {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Same as read_byte(), only non-blocking with a very short timeout
|
||||||
|
pub fn peek_byte(&self, buf: &mut [u8]) -> usize {
|
||||||
|
const TIMEOUT_DUR: Duration = Duration::from_millis(50);
|
||||||
|
Self::with_raw_mode(|| {
|
||||||
|
self.read_blocks(false);
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
loop {
|
||||||
|
match read(self.stdin, buf) {
|
||||||
|
Ok(n) if n > 0 => {
|
||||||
|
self.read_blocks(true);
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) if e == Errno::EAGAIN => {}
|
||||||
|
Err(e) => panic!("nonblocking read failed: {e}")
|
||||||
|
}
|
||||||
|
|
||||||
|
if start.elapsed() >= TIMEOUT_DUR {
|
||||||
|
self.read_blocks(true);
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(Duration::from_millis(1));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_blocks(&self, yn: bool) {
|
||||||
|
let flags = OFlag::from_bits_truncate(fcntl(self.stdin, FcntlArg::F_GETFL).unwrap());
|
||||||
|
let new_flags = if !yn {
|
||||||
|
flags | OFlag::O_NONBLOCK
|
||||||
|
} else {
|
||||||
|
flags & !OFlag::O_NONBLOCK
|
||||||
|
};
|
||||||
|
fcntl(self.stdin, FcntlArg::F_SETFL(new_flags)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn write_bytes(&self, buf: &[u8]) {
|
pub fn write_bytes(&self, buf: &[u8]) {
|
||||||
Self::with_raw_mode(|| {
|
Self::with_raw_mode(|| {
|
||||||
write(unsafe{BorrowedFd::borrow_raw(self.stdout)}, buf).expect("Failed to write to stdout");
|
write(unsafe{BorrowedFd::borrow_raw(self.stdout)}, buf).expect("Failed to write to stdout");
|
||||||
@@ -69,27 +107,56 @@ impl Terminal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_key(&self) -> KeyEvent {
|
pub fn read_key(&self) -> KeyEvent {
|
||||||
let mut buf = [0;8];
|
use core::str;
|
||||||
let n = self.read_byte(&mut buf);
|
|
||||||
|
|
||||||
if buf[0] == 0x1b {
|
let mut buf = [0u8; 8];
|
||||||
if n >= 3 && buf[1] == b'[' {
|
let mut collected = Vec::with_capacity(5);
|
||||||
return match buf[2] {
|
|
||||||
b'A' => KeyEvent(KeyCode::Up, ModKeys::empty()),
|
loop {
|
||||||
b'B' => KeyEvent(KeyCode::Down, ModKeys::empty()),
|
let n = self.read_byte(&mut buf[..1]); // Read one byte at a time
|
||||||
b'C' => KeyEvent(KeyCode::Right, ModKeys::empty()),
|
if n == 0 {
|
||||||
b'D' => KeyEvent(KeyCode::Left, ModKeys::empty()),
|
continue;
|
||||||
_ => KeyEvent(KeyCode::Esc, ModKeys::empty()),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return KeyEvent(KeyCode::Esc, ModKeys::empty());
|
collected.push(buf[0]);
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(s) = core::str::from_utf8(&buf[..n]) {
|
// ESC sequences
|
||||||
if let Some(ch) = s.chars().next() {
|
if collected[0] == 0x1b && collected.len() == 1 {
|
||||||
return KeyEvent::new(ch, ModKeys::NONE);
|
// 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()),
|
||||||
|
b'D' => KeyEvent(KeyCode::Left, ModKeys::empty()),
|
||||||
|
_ => KeyEvent(KeyCode::Esc, ModKeys::empty()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return KeyEvent(KeyCode::Esc, ModKeys::empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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())
|
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