more work on re-implementing the readline module
This commit is contained in:
@@ -25,8 +25,8 @@ fn get_prompt() -> ShResult<String> {
|
||||
pub fn readline(edit_mode: FernEditMode) -> ShResult<String> {
|
||||
let prompt = get_prompt()?;
|
||||
let mut reader: Box<dyn Readline> = match edit_mode {
|
||||
FernEditMode::Vi => Box::new(FernVi::new()),
|
||||
FernEditMode::Vi => Box::new(FernVi::new(Some(prompt))),
|
||||
FernEditMode::Emacs => todo!()
|
||||
};
|
||||
reader.readline(Some(prompt))
|
||||
reader.readline()
|
||||
}
|
||||
|
||||
142
src/prompt/readline/keys.rs
Normal file
142
src/prompt/readline/keys.rs
Normal file
@@ -0,0 +1,142 @@
|
||||
use std::sync::Arc;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
// Credit to Rustyline for the design ideas in this module
|
||||
// https://github.com/kkawakam/rustyline
|
||||
#[derive(Clone,PartialEq,Eq,Debug)]
|
||||
pub struct KeyEvent(pub KeyCode, pub ModKeys);
|
||||
|
||||
|
||||
impl KeyEvent {
|
||||
pub fn new(ch: &str, mut mods: ModKeys) -> Self {
|
||||
use {KeyCode as K, KeyEvent as E, ModKeys as M};
|
||||
|
||||
let mut graphemes = ch.graphemes(true);
|
||||
|
||||
let first = match graphemes.next() {
|
||||
Some(g) => g,
|
||||
None => return E(K::Null, mods),
|
||||
};
|
||||
|
||||
// If more than one grapheme, it's not a single key event
|
||||
if graphemes.next().is_some() {
|
||||
return E(K::Null, mods); // Or panic, or wrap in Grapheme if desired
|
||||
}
|
||||
|
||||
let mut chars = first.chars();
|
||||
|
||||
let single_char = chars.next();
|
||||
let is_single_char = chars.next().is_none();
|
||||
|
||||
match single_char {
|
||||
Some(c) if is_single_char && c.is_control() => {
|
||||
match c {
|
||||
'\x00' => E(K::Char('@'), mods | M::CTRL),
|
||||
'\x01' => E(K::Char('A'), mods | M::CTRL),
|
||||
'\x02' => E(K::Char('B'), mods | M::CTRL),
|
||||
'\x03' => E(K::Char('C'), mods | M::CTRL),
|
||||
'\x04' => E(K::Char('D'), mods | M::CTRL),
|
||||
'\x05' => E(K::Char('E'), mods | M::CTRL),
|
||||
'\x06' => E(K::Char('F'), mods | M::CTRL),
|
||||
'\x07' => E(K::Char('G'), mods | M::CTRL),
|
||||
'\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),
|
||||
}
|
||||
}
|
||||
Some(c) if is_single_char => {
|
||||
if !mods.is_empty() {
|
||||
mods.remove(M::SHIFT);
|
||||
}
|
||||
E(K::Char(c), mods)
|
||||
}
|
||||
_ => {
|
||||
// multi-char grapheme (emoji, accented, etc)
|
||||
if !mods.is_empty() {
|
||||
mods.remove(M::SHIFT);
|
||||
}
|
||||
E(K::Grapheme(Arc::from(first)), mods)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,PartialEq,Eq,Debug)]
|
||||
pub enum KeyCode {
|
||||
UnknownEscSeq,
|
||||
Backspace,
|
||||
BackTab,
|
||||
BracketedPasteStart,
|
||||
BracketedPasteEnd,
|
||||
Char(char),
|
||||
Grapheme(Arc<str>),
|
||||
Delete,
|
||||
Down,
|
||||
End,
|
||||
Enter,
|
||||
Esc,
|
||||
F(u8),
|
||||
Home,
|
||||
Insert,
|
||||
Left,
|
||||
Null,
|
||||
PageDown,
|
||||
PageUp,
|
||||
Right,
|
||||
Tab,
|
||||
Up,
|
||||
}
|
||||
|
||||
bitflags::bitflags! {
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct ModKeys: u8 {
|
||||
/// Control modifier
|
||||
const CTRL = 1<<3;
|
||||
/// Escape or Alt modifier
|
||||
const ALT = 1<<2;
|
||||
/// Shift modifier
|
||||
const SHIFT = 1<<1;
|
||||
|
||||
/// No modifier
|
||||
const NONE = 0;
|
||||
/// Ctrl + Shift
|
||||
const CTRL_SHIFT = Self::CTRL.bits() | Self::SHIFT.bits();
|
||||
/// Alt + Shift
|
||||
const ALT_SHIFT = Self::ALT.bits() | Self::SHIFT.bits();
|
||||
/// Ctrl + Alt
|
||||
const CTRL_ALT = Self::CTRL.bits() | Self::ALT.bits();
|
||||
/// Ctrl + Alt + Shift
|
||||
const CTRL_ALT_SHIFT = Self::CTRL.bits() | Self::ALT.bits() | Self::SHIFT.bits();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,32 +1,249 @@
|
||||
use linebuf::LineBuf;
|
||||
use term::TermReader;
|
||||
use keys::{KeyCode, KeyEvent, ModKeys};
|
||||
use linebuf::{LineBuf, SelectAnchor, SelectMode};
|
||||
use nix::libc::STDOUT_FILENO;
|
||||
use term::{Layout, LineWriter, TermReader};
|
||||
use vicmd::{Motion, MotionCmd, RegisterName, Verb, VerbCmd, ViCmd};
|
||||
use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual};
|
||||
|
||||
use crate::libsh::error::ShResult;
|
||||
use crate::libsh::{error::ShResult, sys::sh_quit, term::{Style, Styled}};
|
||||
use crate::prelude::*;
|
||||
|
||||
pub mod term;
|
||||
pub mod linebuf;
|
||||
pub mod layout;
|
||||
pub mod keys;
|
||||
pub mod vicmd;
|
||||
pub mod register;
|
||||
pub mod vimode;
|
||||
|
||||
pub trait Readline {
|
||||
fn readline(&mut self, prompt: Option<String>) -> ShResult<String>;
|
||||
fn readline(&mut self) -> ShResult<String>;
|
||||
}
|
||||
|
||||
pub struct FernVi {
|
||||
reader: TermReader,
|
||||
writer: TermWriter,
|
||||
writer: LineWriter,
|
||||
prompt: String,
|
||||
mode: Box<dyn ViMode>,
|
||||
old_layout: Option<Layout>,
|
||||
repeat_action: Option<CmdReplay>,
|
||||
repeat_motion: Option<MotionCmd>,
|
||||
editor: LineBuf
|
||||
}
|
||||
|
||||
impl Readline for FernVi {
|
||||
fn readline(&mut self, prompt: Option<String>) -> ShResult<String> {
|
||||
todo!()
|
||||
fn readline(&mut self) -> ShResult<String> {
|
||||
self.editor = LineBuf::new().with_initial("The quick brown fox jumps over the lazy dogThe quick brown fox jumps over the a", 1004);
|
||||
let raw_mode = self.reader.raw_mode(); // Restores termios state on drop
|
||||
|
||||
loop {
|
||||
let new_layout = self.get_layout();
|
||||
if let Some(layout) = self.old_layout.as_ref() {
|
||||
flog!(DEBUG, "clearing???");
|
||||
self.writer.clear_rows(layout)?;
|
||||
}
|
||||
raw_mode.disable_for(|| self.print_line(new_layout))?;
|
||||
let key = self.reader.read_key()?;
|
||||
flog!(DEBUG, key);
|
||||
|
||||
let Some(cmd) = self.mode.handle_key(key) else {
|
||||
continue
|
||||
};
|
||||
|
||||
if cmd.verb().is_some_and(|v| v.1 == Verb::EndOfFile) {
|
||||
if self.editor.buffer.is_empty() {
|
||||
std::mem::drop(raw_mode);
|
||||
sh_quit(0);
|
||||
} else {
|
||||
self.editor.buffer.clear();
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
self.exec_cmd(cmd)?;
|
||||
|
||||
flog!(DEBUG,self.editor.buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FernVi {
|
||||
fn default() -> Self {
|
||||
Self::new(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl FernVi {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(prompt: Option<String>) -> Self {
|
||||
Self {
|
||||
}
|
||||
reader: TermReader::new(),
|
||||
writer: LineWriter::new(STDOUT_FILENO),
|
||||
prompt: prompt.unwrap_or("$ ".styled(Style::Green)),
|
||||
mode: Box::new(ViInsert::new()),
|
||||
old_layout: None,
|
||||
repeat_action: None,
|
||||
repeat_motion: None,
|
||||
editor: LineBuf::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_layout(&mut self) -> Layout {
|
||||
let line = self.editor.as_str().to_string();
|
||||
let to_cursor = self.editor.slice_to_cursor().unwrap();
|
||||
self.writer.get_layout_from_parts(&self.prompt, to_cursor, &line)
|
||||
}
|
||||
|
||||
pub fn print_line(&mut self, new_layout: Layout) -> ShResult<()> {
|
||||
|
||||
self.writer.redraw(
|
||||
&self.prompt,
|
||||
&self.editor,
|
||||
&new_layout
|
||||
)?;
|
||||
|
||||
self.writer.flush_write(&self.mode.cursor_style())?;
|
||||
|
||||
self.old_layout = Some(new_layout);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn exec_cmd(&mut self, mut cmd: ViCmd) -> ShResult<()> {
|
||||
let mut selecting = false;
|
||||
if cmd.is_mode_transition() {
|
||||
let count = cmd.verb_count();
|
||||
let mut mode: Box<dyn ViMode> = match cmd.verb().unwrap().1 {
|
||||
Verb::Change |
|
||||
Verb::InsertModeLineBreak(_) |
|
||||
Verb::InsertMode => Box::new(ViInsert::new().with_count(count as u16)),
|
||||
|
||||
Verb::NormalMode => Box::new(ViNormal::new()),
|
||||
|
||||
Verb::ReplaceMode => Box::new(ViReplace::new()),
|
||||
|
||||
Verb::VisualModeSelectLast => {
|
||||
if self.mode.report_mode() != ModeReport::Visual {
|
||||
self.editor.start_selecting(SelectMode::Char(SelectAnchor::End));
|
||||
}
|
||||
let mut mode: Box<dyn ViMode> = Box::new(ViVisual::new());
|
||||
std::mem::swap(&mut mode, &mut self.mode);
|
||||
self.editor.set_cursor_clamp(self.mode.clamp_cursor());
|
||||
|
||||
return self.editor.exec_cmd(cmd)
|
||||
}
|
||||
Verb::VisualMode => {
|
||||
selecting = true;
|
||||
Box::new(ViVisual::new())
|
||||
}
|
||||
|
||||
_ => unreachable!()
|
||||
};
|
||||
|
||||
std::mem::swap(&mut mode, &mut self.mode);
|
||||
|
||||
self.editor.set_cursor_clamp(self.mode.clamp_cursor());
|
||||
if mode.is_repeatable() {
|
||||
self.repeat_action = mode.as_replay();
|
||||
}
|
||||
|
||||
self.editor.exec_cmd(cmd)?;
|
||||
|
||||
if selecting {
|
||||
self.editor.start_selecting(SelectMode::Char(SelectAnchor::End));
|
||||
} else {
|
||||
self.editor.stop_selecting();
|
||||
}
|
||||
return Ok(())
|
||||
} else if cmd.is_cmd_repeat() {
|
||||
let Some(replay) = self.repeat_action.clone() else {
|
||||
return Ok(())
|
||||
};
|
||||
let ViCmd { verb, .. } = cmd;
|
||||
let VerbCmd(count,_) = verb.unwrap();
|
||||
match replay {
|
||||
CmdReplay::ModeReplay { cmds, mut repeat } => {
|
||||
if count > 1 {
|
||||
repeat = count as u16;
|
||||
}
|
||||
for _ in 0..repeat {
|
||||
let cmds = cmds.clone();
|
||||
for cmd in cmds {
|
||||
self.editor.exec_cmd(cmd)?
|
||||
}
|
||||
}
|
||||
}
|
||||
CmdReplay::Single(mut cmd) => {
|
||||
if count > 1 {
|
||||
// Override the counts with the one passed to the '.' command
|
||||
if cmd.verb.is_some() {
|
||||
if let Some(v_mut) = cmd.verb.as_mut() {
|
||||
v_mut.0 = count
|
||||
}
|
||||
if let Some(m_mut) = cmd.motion.as_mut() {
|
||||
m_mut.0 = 1
|
||||
}
|
||||
} else {
|
||||
return Ok(()) // it has to have a verb to be repeatable, something weird happened
|
||||
}
|
||||
}
|
||||
self.editor.exec_cmd(cmd)?;
|
||||
}
|
||||
_ => unreachable!("motions should be handled in the other branch")
|
||||
}
|
||||
return Ok(())
|
||||
} else if cmd.is_motion_repeat() {
|
||||
match cmd.motion.as_ref().unwrap() {
|
||||
MotionCmd(count,Motion::RepeatMotion) => {
|
||||
let Some(motion) = self.repeat_motion.clone() else {
|
||||
return Ok(())
|
||||
};
|
||||
let repeat_cmd = ViCmd {
|
||||
register: RegisterName::default(),
|
||||
verb: None,
|
||||
motion: Some(motion),
|
||||
raw_seq: format!("{count};")
|
||||
};
|
||||
return self.editor.exec_cmd(repeat_cmd);
|
||||
}
|
||||
MotionCmd(count,Motion::RepeatMotionRev) => {
|
||||
let Some(motion) = self.repeat_motion.clone() else {
|
||||
return Ok(())
|
||||
};
|
||||
let mut new_motion = motion.invert_char_motion();
|
||||
new_motion.0 = *count;
|
||||
let repeat_cmd = ViCmd {
|
||||
register: RegisterName::default(),
|
||||
verb: None,
|
||||
motion: Some(new_motion),
|
||||
raw_seq: format!("{count},")
|
||||
};
|
||||
return self.editor.exec_cmd(repeat_cmd);
|
||||
}
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
if cmd.is_repeatable() {
|
||||
if self.mode.report_mode() == ModeReport::Visual {
|
||||
// The motion is assigned in the line buffer execution, so we also have to assign it here
|
||||
// in order to be able to repeat it
|
||||
let range = self.editor.select_range().unwrap();
|
||||
cmd.motion = Some(MotionCmd(1,Motion::Range(range.0, range.1)))
|
||||
}
|
||||
self.repeat_action = Some(CmdReplay::Single(cmd.clone()));
|
||||
}
|
||||
|
||||
if cmd.is_char_search() {
|
||||
self.repeat_motion = cmd.motion.clone()
|
||||
}
|
||||
|
||||
self.editor.exec_cmd(cmd.clone())?;
|
||||
|
||||
if self.mode.report_mode() == ModeReport::Visual && cmd.verb().is_some_and(|v| v.1.is_edit()) {
|
||||
self.editor.stop_selecting();
|
||||
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new());
|
||||
std::mem::swap(&mut mode, &mut self.mode);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
168
src/prompt/readline/register.rs
Normal file
168
src/prompt/readline/register.rs
Normal file
@@ -0,0 +1,168 @@
|
||||
use std::sync::Mutex;
|
||||
|
||||
pub static REGISTERS: Mutex<Registers> = Mutex::new(Registers::new());
|
||||
|
||||
pub fn read_register(ch: Option<char>) -> Option<String> {
|
||||
let lock = REGISTERS.lock().unwrap();
|
||||
lock.get_reg(ch).map(|r| r.buf().clone())
|
||||
}
|
||||
|
||||
pub fn write_register(ch: Option<char>, buf: String) {
|
||||
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: String) {
|
||||
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(String::new()),
|
||||
a: Register(String::new()),
|
||||
b: Register(String::new()),
|
||||
c: Register(String::new()),
|
||||
d: Register(String::new()),
|
||||
e: Register(String::new()),
|
||||
f: Register(String::new()),
|
||||
g: Register(String::new()),
|
||||
h: Register(String::new()),
|
||||
i: Register(String::new()),
|
||||
j: Register(String::new()),
|
||||
k: Register(String::new()),
|
||||
l: Register(String::new()),
|
||||
m: Register(String::new()),
|
||||
n: Register(String::new()),
|
||||
o: Register(String::new()),
|
||||
p: Register(String::new()),
|
||||
q: Register(String::new()),
|
||||
r: Register(String::new()),
|
||||
s: Register(String::new()),
|
||||
t: Register(String::new()),
|
||||
u: Register(String::new()),
|
||||
v: Register(String::new()),
|
||||
w: Register(String::new()),
|
||||
x: Register(String::new()),
|
||||
y: Register(String::new()),
|
||||
z: Register(String::new()),
|
||||
}
|
||||
}
|
||||
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(String);
|
||||
impl Register {
|
||||
pub fn buf(&self) -> &String {
|
||||
&self.0
|
||||
}
|
||||
pub fn write(&mut self, buf: String) {
|
||||
self.0 = buf
|
||||
}
|
||||
pub fn append(&mut self, buf: String) {
|
||||
self.0.push_str(&buf)
|
||||
}
|
||||
pub fn clear(&mut self) {
|
||||
self.0.clear()
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
use std::{env, fmt::Write, io::{BufRead, BufReader, Read}, ops::{Deref, DerefMut}, os::fd::{AsFd, BorrowedFd, RawFd}};
|
||||
use std::{env, fmt::{Debug, Write}, io::{BufRead, BufReader, Read}, os::fd::{AsFd, BorrowedFd, RawFd}};
|
||||
|
||||
use nix::{errno::Errno, libc, poll::{self, PollFlags, PollTimeout}, unistd::isatty};
|
||||
use nix::{errno::Errno, libc::{self, STDIN_FILENO}, poll::{self, PollFlags, PollTimeout}, sys::termios, unistd::isatty};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||||
|
||||
use crate::libsh::error::{ShErr, ShErrKind, ShResult};
|
||||
use crate::{libsh::error::{ShErr, ShErrKind, ShResult}, prompt::readline::keys::{KeyCode, ModKeys}};
|
||||
use crate::prelude::*;
|
||||
|
||||
use super::linebuf::LineBuf;
|
||||
use super::{keys::KeyEvent, linebuf::LineBuf};
|
||||
|
||||
pub type Row = u16;
|
||||
pub type Col = u16;
|
||||
@@ -43,7 +44,7 @@ pub fn get_win_size(fd: RawFd) -> (Col,Row) {
|
||||
} else {
|
||||
size.ws_row
|
||||
};
|
||||
(cols.into(), rows.into())
|
||||
(cols, rows)
|
||||
}
|
||||
_ => (80,24)
|
||||
}
|
||||
@@ -182,7 +183,7 @@ pub struct TermBuffer {
|
||||
|
||||
impl TermBuffer {
|
||||
pub fn new(tty: RawFd) -> Self {
|
||||
assert!(isatty(tty).is_ok_and(|r| r == true));
|
||||
assert!(isatty(tty).is_ok_and(|r| r));
|
||||
Self {
|
||||
tty
|
||||
}
|
||||
@@ -191,7 +192,7 @@ impl TermBuffer {
|
||||
|
||||
impl Read for TermBuffer {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
assert!(isatty(self.tty).is_ok_and(|r| r == true));
|
||||
assert!(isatty(self.tty).is_ok_and(|r| r));
|
||||
loop {
|
||||
match nix::unistd::read(self.tty, buf) {
|
||||
Ok(n) => return Ok(n),
|
||||
@@ -202,10 +203,52 @@ impl Read for TermBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RawModeGuard {
|
||||
orig: termios::Termios,
|
||||
fd: RawFd,
|
||||
}
|
||||
|
||||
impl RawModeGuard {
|
||||
/// Disable raw mode temporarily for a specific operation
|
||||
pub fn disable_for<F: FnOnce() -> R, R>(&self, func: F) -> R {
|
||||
unsafe {
|
||||
let fd = BorrowedFd::borrow_raw(self.fd);
|
||||
// Temporarily restore the original termios
|
||||
termios::tcsetattr(fd, termios::SetArg::TCSANOW, &self.orig)
|
||||
.expect("Failed to temporarily disable raw mode");
|
||||
|
||||
// Run the function
|
||||
let result = func();
|
||||
|
||||
// Re-enable raw mode
|
||||
let mut raw = self.orig.clone();
|
||||
termios::cfmakeraw(&mut raw);
|
||||
termios::tcsetattr(fd, termios::SetArg::TCSANOW, &raw)
|
||||
.expect("Failed to re-enable raw mode");
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for RawModeGuard {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
let _ = termios::tcsetattr(BorrowedFd::borrow_raw(self.fd), termios::SetArg::TCSANOW, &self.orig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TermReader {
|
||||
buffer: BufReader<TermBuffer>
|
||||
}
|
||||
|
||||
impl Default for TermReader {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl TermReader {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
@@ -213,8 +256,22 @@ impl TermReader {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn raw_mode(&self) -> RawModeGuard {
|
||||
let fd = self.buffer.get_ref().tty;
|
||||
let orig = termios::tcgetattr(unsafe{BorrowedFd::borrow_raw(STDIN_FILENO)}).expect("Failed to get terminal attributes");
|
||||
let mut raw = orig.clone();
|
||||
termios::cfmakeraw(&mut raw);
|
||||
termios::tcsetattr(unsafe{BorrowedFd::borrow_raw(STDIN_FILENO)}, termios::SetArg::TCSANOW, &raw)
|
||||
.expect("Failed to set terminal to raw mode");
|
||||
RawModeGuard { orig, fd }
|
||||
}
|
||||
|
||||
/// Execute some logic in raw mode
|
||||
///
|
||||
/// Saves the termios before running the given function.
|
||||
/// If the given function panics, the panic will halt momentarily to restore the termios
|
||||
pub fn poll(&mut self, timeout: PollTimeout) -> ShResult<bool> {
|
||||
if self.buffer.buffer().len() > 0 {
|
||||
if !self.buffer.buffer().is_empty() {
|
||||
return Ok(true)
|
||||
}
|
||||
|
||||
@@ -234,6 +291,7 @@ impl TermReader {
|
||||
}
|
||||
|
||||
pub fn peek_byte(&mut self) -> std::io::Result<u8> {
|
||||
flog!(DEBUG,"filling buffer");
|
||||
let buf = self.buffer.fill_buf()?;
|
||||
if buf.is_empty() {
|
||||
Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "EOF"))
|
||||
@@ -245,6 +303,113 @@ impl TermReader {
|
||||
pub fn consume_byte(&mut self) {
|
||||
self.buffer.consume(1);
|
||||
}
|
||||
|
||||
pub fn read_key(&mut self) -> ShResult<KeyEvent> {
|
||||
use core::str;
|
||||
|
||||
let mut collected = Vec::with_capacity(4);
|
||||
|
||||
loop {
|
||||
let byte = self.next_byte()?;
|
||||
collected.push(byte);
|
||||
|
||||
// If it's an escape seq, delegate to ESC sequence handler
|
||||
if collected[0] == 0x1b && collected.len() == 1 && self.poll(PollTimeout::ZERO)? {
|
||||
return self.parse_esc_seq();
|
||||
}
|
||||
|
||||
// Try parse as valid UTF-8
|
||||
if let Ok(s) = str::from_utf8(&collected) {
|
||||
return Ok(KeyEvent::new(s, ModKeys::empty()));
|
||||
}
|
||||
|
||||
// UTF-8 max 4 bytes — if it’s invalid at this point, bail
|
||||
if collected.len() >= 4 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(KeyEvent(KeyCode::Null, ModKeys::empty()))
|
||||
}
|
||||
|
||||
pub fn parse_esc_seq(&mut self) -> ShResult<KeyEvent> {
|
||||
let mut seq = vec![0x1b];
|
||||
|
||||
let b1 = self.peek_byte()?;
|
||||
self.consume_byte();
|
||||
seq.push(b1);
|
||||
|
||||
match b1 {
|
||||
b'[' => {
|
||||
let b2 = self.peek_byte()?;
|
||||
self.consume_byte();
|
||||
seq.push(b2);
|
||||
|
||||
match b2 {
|
||||
b'A' => Ok(KeyEvent(KeyCode::Up, ModKeys::empty())),
|
||||
b'B' => Ok(KeyEvent(KeyCode::Down, ModKeys::empty())),
|
||||
b'C' => Ok(KeyEvent(KeyCode::Right, ModKeys::empty())),
|
||||
b'D' => Ok(KeyEvent(KeyCode::Left, ModKeys::empty())),
|
||||
b'1'..=b'9' => {
|
||||
let mut digits = vec![b2];
|
||||
|
||||
loop {
|
||||
let b = self.peek_byte()?;
|
||||
seq.push(b);
|
||||
self.consume_byte();
|
||||
|
||||
if b == b'~' || b == b';' {
|
||||
break;
|
||||
} else if b.is_ascii_digit() {
|
||||
digits.push(b);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let key = match digits.as_slice() {
|
||||
[b'1'] => KeyCode::Home,
|
||||
[b'3'] => KeyCode::Delete,
|
||||
[b'4'] => KeyCode::End,
|
||||
[b'5'] => KeyCode::PageUp,
|
||||
[b'6'] => KeyCode::PageDown,
|
||||
[b'7'] => KeyCode::Home, // xterm alternate
|
||||
[b'8'] => KeyCode::End, // xterm alternate
|
||||
|
||||
[b'1', b'5'] => KeyCode::F(5),
|
||||
[b'1', b'7'] => KeyCode::F(6),
|
||||
[b'1', b'8'] => KeyCode::F(7),
|
||||
[b'1', b'9'] => KeyCode::F(8),
|
||||
[b'2', b'0'] => KeyCode::F(9),
|
||||
[b'2', b'1'] => KeyCode::F(10),
|
||||
[b'2', b'3'] => KeyCode::F(11),
|
||||
[b'2', b'4'] => KeyCode::F(12),
|
||||
_ => KeyCode::Esc,
|
||||
};
|
||||
|
||||
Ok(KeyEvent(key, ModKeys::empty()))
|
||||
}
|
||||
_ => Ok(KeyEvent(KeyCode::Esc, ModKeys::empty())),
|
||||
}
|
||||
}
|
||||
b'O' => {
|
||||
let b2 = self.peek_byte()?;
|
||||
self.consume_byte();
|
||||
seq.push(b2);
|
||||
|
||||
let key = match b2 {
|
||||
b'P' => KeyCode::F(1),
|
||||
b'Q' => KeyCode::F(2),
|
||||
b'R' => KeyCode::F(3),
|
||||
b'S' => KeyCode::F(4),
|
||||
_ => KeyCode::Esc,
|
||||
};
|
||||
|
||||
Ok(KeyEvent(key, ModKeys::empty()))
|
||||
}
|
||||
_ => Ok(KeyEvent(KeyCode::Esc, ModKeys::empty())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsFd for TermReader {
|
||||
@@ -261,6 +426,15 @@ pub struct Layout {
|
||||
pub end: Pos
|
||||
}
|
||||
|
||||
impl Debug for Layout {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
writeln!(f, "Layout: ")?;
|
||||
writeln!(f, "\tPrompt End: {:?}",self.prompt_end)?;
|
||||
writeln!(f, "\tCursor: {:?}",self.cursor)?;
|
||||
writeln!(f, "\tEnd: {:?}",self.end)
|
||||
}
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
pub fn new() -> Self {
|
||||
let w_calc = width_calculator();
|
||||
@@ -273,6 +447,12 @@ impl Layout {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Layout {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LineWriter {
|
||||
out: RawFd,
|
||||
t_cols: Col, // terminal width
|
||||
@@ -297,7 +477,8 @@ impl LineWriter {
|
||||
write_all(self.out, buf)?;
|
||||
Ok(())
|
||||
}
|
||||
pub fn clear_rows(&mut self, layout: &Layout) {
|
||||
pub fn clear_rows(&mut self, layout: &Layout) -> ShResult<()> {
|
||||
self.buffer.clear();
|
||||
let rows_to_clear = layout.end.row;
|
||||
let cursor_row = layout.cursor.row;
|
||||
|
||||
@@ -307,27 +488,31 @@ impl LineWriter {
|
||||
}
|
||||
|
||||
for _ in 0..rows_to_clear {
|
||||
self.buffer.push_str("\x1b[K\x1b[A");
|
||||
self.buffer.push_str("\x1b[2K\x1b[A");
|
||||
}
|
||||
self.buffer.push_str("\x1b[K");
|
||||
}
|
||||
pub fn move_cursor(&mut self, old: Pos, new: Pos) -> ShResult<()> {
|
||||
self.buffer.push_str("\x1b[2K");
|
||||
flog!(DEBUG, self.buffer);
|
||||
write_all(self.out,self.buffer.as_str())?;
|
||||
self.buffer.clear();
|
||||
let err = |_| ShErr::simple(ShErrKind::InternalErr, "Failed to write to LineWriter internal buffer");
|
||||
Ok(())
|
||||
}
|
||||
pub fn get_cursor_movement(&self, old: Pos, new: Pos) -> ShResult<String> {
|
||||
let mut buffer = String::new();
|
||||
let err = |_| ShErr::simple(ShErrKind::InternalErr, "Failed to write to cursor movement buffer");
|
||||
|
||||
match new.row.cmp(&old.row) {
|
||||
std::cmp::Ordering::Greater => {
|
||||
let shift = new.row - old.row;
|
||||
match shift {
|
||||
1 => self.buffer.push_str("\x1b[B"),
|
||||
_ => write!(self.buffer, "\x1b[{shift}B").map_err(err)?
|
||||
1 => buffer.push_str("\x1b[B"),
|
||||
_ => write!(buffer, "\x1b[{shift}B").map_err(err)?
|
||||
}
|
||||
}
|
||||
std::cmp::Ordering::Less => {
|
||||
let shift = old.row - new.row;
|
||||
match shift {
|
||||
1 => self.buffer.push_str("\x1b[A"),
|
||||
_ => write!(self.buffer, "\x1b[{shift}A").map_err(err)?
|
||||
1 => buffer.push_str("\x1b[A"),
|
||||
_ => write!(buffer, "\x1b[{shift}A").map_err(err)?
|
||||
}
|
||||
}
|
||||
std::cmp::Ordering::Equal => { /* Do nothing */ }
|
||||
@@ -337,20 +522,26 @@ impl LineWriter {
|
||||
std::cmp::Ordering::Greater => {
|
||||
let shift = new.col - old.col;
|
||||
match shift {
|
||||
1 => self.buffer.push_str("\x1b[C"),
|
||||
_ => write!(self.buffer, "\x1b[{shift}C").map_err(err)?
|
||||
1 => buffer.push_str("\x1b[C"),
|
||||
_ => write!(buffer, "\x1b[{shift}C").map_err(err)?
|
||||
}
|
||||
}
|
||||
std::cmp::Ordering::Less => {
|
||||
let shift = old.col - new.col;
|
||||
match shift {
|
||||
1 => self.buffer.push_str("\x1b[D"),
|
||||
_ => write!(self.buffer, "\x1b[{shift}D").map_err(err)?
|
||||
1 => buffer.push_str("\x1b[D"),
|
||||
_ => write!(buffer, "\x1b[{shift}D").map_err(err)?
|
||||
}
|
||||
}
|
||||
std::cmp::Ordering::Equal => { /* Do nothing */ }
|
||||
}
|
||||
write_all(self.out, self.buffer.as_str())?;
|
||||
Ok(buffer)
|
||||
}
|
||||
pub fn move_cursor(&mut self, old: Pos, new: Pos) -> ShResult<()> {
|
||||
self.buffer.clear();
|
||||
let movement = self.get_cursor_movement(old, new)?;
|
||||
|
||||
write_all(self.out, &movement)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -358,46 +549,37 @@ impl LineWriter {
|
||||
&mut self,
|
||||
prompt: &str,
|
||||
line: &LineBuf,
|
||||
old_layout: &Layout,
|
||||
new_layout: &Layout,
|
||||
) -> ShResult<()> {
|
||||
let err = |_| ShErr::simple(ShErrKind::InternalErr, "Failed to write to LineWriter internal buffer");
|
||||
self.buffer.clear();
|
||||
|
||||
self.clear_rows(old_layout);
|
||||
|
||||
let end = new_layout.end;
|
||||
let cursor = new_layout.cursor;
|
||||
|
||||
self.buffer.push_str(prompt);
|
||||
self.buffer.push_str(line.as_str());
|
||||
|
||||
if end.col == 0
|
||||
&& end.row > 0
|
||||
{
|
||||
if end.col == 0 && end.row > 0 {
|
||||
// The line has wrapped. We need to use our own line break.
|
||||
self.buffer.push('\n')
|
||||
self.buffer.push('\n');
|
||||
}
|
||||
|
||||
let cursor_row_offset = end.row - cursor.row;
|
||||
|
||||
match cursor_row_offset {
|
||||
0 => { /* Do nothing */ }
|
||||
1 => self.buffer.push_str("\x1b[A"),
|
||||
_ => write!(self.buffer, "\x1b[{cursor_row_offset}A").map_err(err)?
|
||||
}
|
||||
|
||||
let cursor_col = cursor.col;
|
||||
match cursor_col {
|
||||
0 => self.buffer.push('\r'),
|
||||
1 => self.buffer.push_str("\x1b[C"),
|
||||
_ => write!(self.buffer, "\x1b[{cursor_col}C").map_err(err)?
|
||||
}
|
||||
let movement = self.get_cursor_movement(end, cursor)?;
|
||||
write!(self.buffer, "{}", &movement).map_err(err)?;
|
||||
|
||||
write_all(self.out, self.buffer.as_str())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_layout_from_parts(&mut self, prompt: &str, to_cursor: &str, to_end: &str) -> Layout {
|
||||
self.update_t_cols();
|
||||
let prompt_end = self.calc_pos(prompt, Pos { col: 0, row: 0 });
|
||||
let cursor = self.calc_pos(to_cursor, prompt_end);
|
||||
let end = self.calc_pos(to_end, prompt_end);
|
||||
Layout { w_calc: width_calculator(), prompt_end, cursor, end }
|
||||
}
|
||||
|
||||
pub fn calc_pos(&self, s: &str, orig: Pos) -> Pos {
|
||||
let mut pos = orig;
|
||||
let mut esc_seq = 0;
|
||||
@@ -417,7 +599,7 @@ impl LineWriter {
|
||||
pos.col = c_width;
|
||||
}
|
||||
}
|
||||
if pos.col > self.t_cols {
|
||||
if pos.col >= self.t_cols {
|
||||
pos.row += 1;
|
||||
pos.col = 0;
|
||||
}
|
||||
@@ -430,32 +612,47 @@ impl LineWriter {
|
||||
self.t_cols = t_cols;
|
||||
}
|
||||
|
||||
pub fn move_cursor_at_leftmost(&mut self, rdr: &mut TermReader) -> ShResult<()> {
|
||||
if rdr.poll(PollTimeout::ZERO)? {
|
||||
pub fn move_cursor_at_leftmost(&mut self, rdr: &mut TermReader, use_newline: bool) -> ShResult<()> {
|
||||
let result = rdr.poll(PollTimeout::ZERO)?;
|
||||
if result {
|
||||
// The terminals reply is going to be stuck behind the currently buffered output
|
||||
// So let's get out of here
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
// Ping the cursor's position
|
||||
self.flush_write("\x1b[6n")?;
|
||||
self.flush_write("\x1b[6n\n")?;
|
||||
|
||||
// Excessively paranoid invariant checking
|
||||
if !rdr.poll(PollTimeout::from(100u8))?
|
||||
|| rdr.next_byte()? as char != '\x1b'
|
||||
|| rdr.next_byte()? as char != '['
|
||||
|| read_digits_until(rdr, ';')?.is_none() {
|
||||
// Invariant is broken, get out
|
||||
if !rdr.poll(PollTimeout::from(255u8))? {
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
if rdr.next_byte()? as char != '\x1b' {
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
if rdr.next_byte()? as char != '[' {
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
if read_digits_until(rdr, ';')?.is_none() {
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
// We just consumed everything up to the column number, so let's get that now
|
||||
let col = read_digits_until(rdr, 'R')?;
|
||||
|
||||
// The cursor is not at the leftmost, so let's fix that
|
||||
if col != Some(1) {
|
||||
// We use '\n' instead of '\r' because if there's a bunch of garbage on this line,
|
||||
if use_newline {
|
||||
// We use '\n' instead of '\r' sometimes because if there's a bunch of garbage on this line,
|
||||
// It might pollute the prompt/line buffer if those are shorter than said garbage
|
||||
self.flush_write("\n")?;
|
||||
} else {
|
||||
// Sometimes though, we know that there's nothing to the right of the cursor after moving
|
||||
// So we just move to the left.
|
||||
self.flush_write("\r")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
393
src/prompt/readline/vicmd.rs
Normal file
393
src/prompt/readline/vicmd.rs
Normal file
@@ -0,0 +1,393 @@
|
||||
use super::register::{append_register, read_register, write_register};
|
||||
|
||||
//TODO: write tests that take edit results and cursor positions from actual neovim edits and test them against the behavior of this editor
|
||||
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
pub struct RegisterName {
|
||||
name: Option<char>,
|
||||
count: usize,
|
||||
append: bool
|
||||
}
|
||||
|
||||
impl RegisterName {
|
||||
pub fn new(name: Option<char>, count: Option<usize>) -> Self {
|
||||
let Some(ch) = name else {
|
||||
return Self::default()
|
||||
};
|
||||
|
||||
let append = ch.is_uppercase();
|
||||
let name = ch.to_ascii_lowercase();
|
||||
Self {
|
||||
name: Some(name),
|
||||
count: count.unwrap_or(1),
|
||||
append
|
||||
}
|
||||
}
|
||||
pub fn name(&self) -> Option<char> {
|
||||
self.name
|
||||
}
|
||||
pub fn is_append(&self) -> bool {
|
||||
self.append
|
||||
}
|
||||
pub fn count(&self) -> usize {
|
||||
self.count
|
||||
}
|
||||
pub fn write_to_register(&self, buf: String) {
|
||||
if self.append {
|
||||
append_register(self.name, buf);
|
||||
} else {
|
||||
write_register(self.name, buf);
|
||||
}
|
||||
}
|
||||
pub fn read_from_register(&self) -> Option<String> {
|
||||
read_register(self.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RegisterName {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: None,
|
||||
count: 1,
|
||||
append: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,Default,Debug)]
|
||||
pub struct ViCmd {
|
||||
pub register: RegisterName,
|
||||
pub verb: Option<VerbCmd>,
|
||||
pub motion: Option<MotionCmd>,
|
||||
pub raw_seq: String,
|
||||
}
|
||||
|
||||
impl ViCmd {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
pub fn set_motion(&mut self, motion: MotionCmd) {
|
||||
self.motion = Some(motion)
|
||||
}
|
||||
pub fn set_verb(&mut self, verb: VerbCmd) {
|
||||
self.verb = Some(verb)
|
||||
}
|
||||
pub fn verb(&self) -> Option<&VerbCmd> {
|
||||
self.verb.as_ref()
|
||||
}
|
||||
pub fn motion(&self) -> Option<&MotionCmd> {
|
||||
self.motion.as_ref()
|
||||
}
|
||||
pub fn verb_count(&self) -> usize {
|
||||
self.verb.as_ref().map(|v| v.0).unwrap_or(1)
|
||||
}
|
||||
pub fn motion_count(&self) -> usize {
|
||||
self.motion.as_ref().map(|m| m.0).unwrap_or(1)
|
||||
}
|
||||
pub fn is_repeatable(&self) -> bool {
|
||||
self.verb.as_ref().is_some_and(|v| v.1.is_repeatable())
|
||||
}
|
||||
pub fn is_cmd_repeat(&self) -> bool {
|
||||
self.verb.as_ref().is_some_and(|v| matches!(v.1,Verb::RepeatLast))
|
||||
}
|
||||
pub fn is_motion_repeat(&self) -> bool {
|
||||
self.motion.as_ref().is_some_and(|m| matches!(m.1,Motion::RepeatMotion | Motion::RepeatMotionRev))
|
||||
}
|
||||
pub fn is_char_search(&self) -> bool {
|
||||
self.motion.as_ref().is_some_and(|m| matches!(m.1, Motion::CharSearch(..)))
|
||||
}
|
||||
pub fn should_submit(&self) -> bool {
|
||||
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::AcceptLineOrNewline))
|
||||
}
|
||||
pub fn is_undo_op(&self) -> bool {
|
||||
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::Undo | Verb::Redo))
|
||||
}
|
||||
pub fn is_line_motion(&self) -> bool {
|
||||
self.motion.as_ref().is_some_and(|m| matches!(m.1, Motion::LineUp | Motion::LineDown))
|
||||
}
|
||||
pub fn is_mode_transition(&self) -> bool {
|
||||
self.verb.as_ref().is_some_and(|v| {
|
||||
matches!(v.1,
|
||||
Verb::Change |
|
||||
Verb::InsertMode |
|
||||
Verb::InsertModeLineBreak(_) |
|
||||
Verb::NormalMode |
|
||||
Verb::VisualModeSelectLast |
|
||||
Verb::VisualMode |
|
||||
Verb::ReplaceMode
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct VerbCmd(pub usize,pub Verb);
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct MotionCmd(pub usize,pub Motion);
|
||||
|
||||
impl MotionCmd {
|
||||
pub fn invert_char_motion(self) -> Self {
|
||||
let MotionCmd(count,Motion::CharSearch(dir, dest, ch)) = self else {
|
||||
unreachable!()
|
||||
};
|
||||
let new_dir = match dir {
|
||||
Direction::Forward => Direction::Backward,
|
||||
Direction::Backward => Direction::Forward,
|
||||
};
|
||||
MotionCmd(count,Motion::CharSearch(new_dir, dest, ch))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum Verb {
|
||||
Delete,
|
||||
Change,
|
||||
Yank,
|
||||
Rot13, // lol
|
||||
ReplaceChar(char),
|
||||
ToggleCase,
|
||||
ToLower,
|
||||
ToUpper,
|
||||
Complete,
|
||||
CompleteBackward,
|
||||
Undo,
|
||||
Redo,
|
||||
RepeatLast,
|
||||
Put(Anchor),
|
||||
ReplaceMode,
|
||||
InsertMode,
|
||||
InsertModeLineBreak(Anchor),
|
||||
NormalMode,
|
||||
VisualMode,
|
||||
VisualModeLine,
|
||||
VisualModeBlock, // dont even know if im going to implement this
|
||||
VisualModeSelectLast,
|
||||
SwapVisualAnchor,
|
||||
JoinLines,
|
||||
InsertChar(char),
|
||||
Insert(String),
|
||||
Breakline(Anchor),
|
||||
Indent,
|
||||
Dedent,
|
||||
Equalize,
|
||||
AcceptLineOrNewline,
|
||||
EndOfFile
|
||||
}
|
||||
|
||||
|
||||
impl Verb {
|
||||
pub fn is_repeatable(&self) -> bool {
|
||||
matches!(self,
|
||||
Self::Delete |
|
||||
Self::Change |
|
||||
Self::ReplaceChar(_) |
|
||||
Self::ToLower |
|
||||
Self::ToUpper |
|
||||
Self::ToggleCase |
|
||||
Self::Put(_) |
|
||||
Self::ReplaceMode |
|
||||
Self::InsertModeLineBreak(_) |
|
||||
Self::JoinLines |
|
||||
Self::InsertChar(_) |
|
||||
Self::Insert(_) |
|
||||
Self::Breakline(_) |
|
||||
Self::Indent |
|
||||
Self::Dedent |
|
||||
Self::Equalize
|
||||
)
|
||||
}
|
||||
pub fn is_edit(&self) -> bool {
|
||||
matches!(self,
|
||||
Self::Delete |
|
||||
Self::Change |
|
||||
Self::ReplaceChar(_) |
|
||||
Self::ToggleCase |
|
||||
Self::ToLower |
|
||||
Self::ToUpper |
|
||||
Self::RepeatLast |
|
||||
Self::Put(_) |
|
||||
Self::ReplaceMode |
|
||||
Self::InsertModeLineBreak(_) |
|
||||
Self::JoinLines |
|
||||
Self::InsertChar(_) |
|
||||
Self::Insert(_) |
|
||||
Self::Breakline(_) |
|
||||
Self::Rot13 |
|
||||
Self::EndOfFile
|
||||
)
|
||||
}
|
||||
pub fn is_char_insert(&self) -> bool {
|
||||
matches!(self,
|
||||
Self::Change |
|
||||
Self::InsertChar(_) |
|
||||
Self::ReplaceChar(_)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum Motion {
|
||||
WholeLine,
|
||||
TextObj(TextObj, Bound),
|
||||
EndOfLastWord,
|
||||
BeginningOfFirstWord,
|
||||
BeginningOfLine,
|
||||
EndOfLine,
|
||||
WordMotion(To,Word,Direction),
|
||||
CharSearch(Direction,Dest,char),
|
||||
BackwardChar,
|
||||
ForwardChar,
|
||||
LineUp,
|
||||
LineUpCharwise,
|
||||
ScreenLineUp,
|
||||
ScreenLineUpCharwise,
|
||||
LineDown,
|
||||
LineDownCharwise,
|
||||
ScreenLineDown,
|
||||
ScreenLineDownCharwise,
|
||||
BeginningOfScreenLine,
|
||||
FirstGraphicalOnScreenLine,
|
||||
HalfOfScreen,
|
||||
HalfOfScreenLineText,
|
||||
WholeBuffer,
|
||||
BeginningOfBuffer,
|
||||
EndOfBuffer,
|
||||
ToColumn(usize),
|
||||
ToDelimMatch,
|
||||
ToBrace(Direction),
|
||||
ToBracket(Direction),
|
||||
ToParen(Direction),
|
||||
Range(usize,usize),
|
||||
RepeatMotion,
|
||||
RepeatMotionRev,
|
||||
Null
|
||||
}
|
||||
|
||||
#[derive(Clone,Copy,PartialEq,Eq,Debug)]
|
||||
pub enum MotionBehavior {
|
||||
Exclusive,
|
||||
Inclusive,
|
||||
Linewise
|
||||
}
|
||||
|
||||
impl Motion {
|
||||
pub fn behavior(&self) -> MotionBehavior {
|
||||
if self.is_linewise() {
|
||||
MotionBehavior::Linewise
|
||||
} else if self.is_exclusive() {
|
||||
MotionBehavior::Exclusive
|
||||
} else {
|
||||
MotionBehavior::Inclusive
|
||||
}
|
||||
}
|
||||
pub fn is_exclusive(&self) -> bool {
|
||||
matches!(&self,
|
||||
Self::BeginningOfLine |
|
||||
Self::BeginningOfFirstWord |
|
||||
Self::BeginningOfScreenLine |
|
||||
Self::FirstGraphicalOnScreenLine |
|
||||
Self::LineDownCharwise |
|
||||
Self::LineUpCharwise |
|
||||
Self::ScreenLineUpCharwise |
|
||||
Self::ScreenLineDownCharwise |
|
||||
Self::ToColumn(_) |
|
||||
Self::TextObj(TextObj::ForwardSentence,_) |
|
||||
Self::TextObj(TextObj::BackwardSentence,_) |
|
||||
Self::TextObj(TextObj::ForwardParagraph,_) |
|
||||
Self::TextObj(TextObj::BackwardParagraph,_) |
|
||||
Self::CharSearch(Direction::Backward, _, _) |
|
||||
Self::WordMotion(To::Start,_,_) |
|
||||
Self::ToBrace(_) |
|
||||
Self::ToBracket(_) |
|
||||
Self::ToParen(_) |
|
||||
Self::ScreenLineDown |
|
||||
Self::ScreenLineUp |
|
||||
Self::Range(_, _)
|
||||
)
|
||||
}
|
||||
pub fn is_linewise(&self) -> bool {
|
||||
matches!(self,
|
||||
Self::WholeLine |
|
||||
Self::LineUp |
|
||||
Self::LineDown |
|
||||
Self::ScreenLineDown |
|
||||
Self::ScreenLineUp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[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
|
||||
ForwardSentence,
|
||||
BackwardSentence,
|
||||
|
||||
/// `ip`, `ap` — inner paragraph, around paragraph
|
||||
ForwardParagraph,
|
||||
BackwardParagraph,
|
||||
|
||||
/// `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
|
||||
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
|
||||
}
|
||||
1520
src/prompt/readline/vimode.rs
Normal file
1520
src/prompt/readline/vimode.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -26,6 +26,7 @@ pub mod error;
|
||||
pub mod getopt;
|
||||
pub mod script;
|
||||
pub mod highlight;
|
||||
pub mod readline;
|
||||
|
||||
/// Unsafe to use outside of tests
|
||||
pub fn get_nodes<F1>(input: &str, filter: F1) -> Vec<Node>
|
||||
|
||||
131
src/tests/readline.rs
Normal file
131
src/tests/readline.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
use crate::prompt::readline::linebuf::LineBuf;
|
||||
|
||||
use super::super::*;
|
||||
|
||||
#[test]
|
||||
fn linebuf_empty_linebuf() {
|
||||
let mut buf = LineBuf::new();
|
||||
assert_eq!(buf.as_str(), "");
|
||||
buf.update_graphemes_lazy();
|
||||
assert_eq!(buf.grapheme_indices(), &[]);
|
||||
assert!(buf.slice(0..0).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linebuf_ascii_content() {
|
||||
let mut buf = LineBuf::new().with_initial("hello", 0);
|
||||
|
||||
buf.update_graphemes_lazy();
|
||||
assert_eq!(buf.grapheme_indices(), &[0, 1, 2, 3, 4]);
|
||||
|
||||
assert_eq!(buf.grapheme_at(0), Some("h"));
|
||||
assert_eq!(buf.grapheme_at(4), Some("o"));
|
||||
assert_eq!(buf.slice(1..4), Some("ell"));
|
||||
assert_eq!(buf.slice_to(2), Some("he"));
|
||||
assert_eq!(buf.slice_from(2), Some("llo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linebuf_unicode_graphemes() {
|
||||
let mut buf = LineBuf::new().with_initial("a🇺🇸b́c", 0);
|
||||
|
||||
buf.update_graphemes_lazy();
|
||||
let indices = buf.grapheme_indices();
|
||||
assert_eq!(indices.len(), 4); // 4 graphemes + 1 end marker
|
||||
|
||||
assert_eq!(buf.grapheme_at(0), Some("a"));
|
||||
assert_eq!(buf.grapheme_at(1), Some("🇺🇸"));
|
||||
assert_eq!(buf.grapheme_at(2), Some("b́")); // b + combining accent
|
||||
assert_eq!(buf.grapheme_at(3), Some("c"));
|
||||
assert_eq!(buf.grapheme_at(4), None); // out of bounds
|
||||
|
||||
assert_eq!(buf.slice(0..2), Some("a🇺🇸"));
|
||||
assert_eq!(buf.slice(1..3), Some("🇺🇸b́"));
|
||||
assert_eq!(buf.slice(2..4), Some("b́c"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linebuf_slice_to_from_cursor() {
|
||||
let mut buf = LineBuf::new().with_initial("abçd", 2);
|
||||
|
||||
buf.update_graphemes_lazy();
|
||||
assert_eq!(buf.slice_to_cursor(), Some("ab"));
|
||||
assert_eq!(buf.slice_from_cursor(), Some("çd"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linebuf_out_of_bounds_slices() {
|
||||
let mut buf = LineBuf::new().with_initial("test", 0);
|
||||
|
||||
buf.update_graphemes_lazy();
|
||||
|
||||
assert_eq!(buf.grapheme_at(5), None); // out of bounds
|
||||
assert_eq!(buf.slice(2..5), None); // end out of bounds
|
||||
assert_eq!(buf.slice(4..4), None); // valid but empty
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linebuf_this_line() {
|
||||
let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line";
|
||||
let mut buf = LineBuf::new().with_initial(initial, 57);
|
||||
let (start,end) = buf.this_line();
|
||||
assert_eq!(buf.slice(start..end), Some("This is the third line"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linebuf_prev_line() {
|
||||
let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line";
|
||||
let mut buf = LineBuf::new().with_initial(initial, 57);
|
||||
let (start,end) = buf.prev_line().unwrap();
|
||||
assert_eq!(buf.slice(start..end), Some("This is the second line"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linebuf_next_line() {
|
||||
let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line";
|
||||
let mut buf = LineBuf::new().with_initial(initial, 57);
|
||||
let (start,end) = buf.next_line().unwrap();
|
||||
assert_eq!(buf.slice(start..end), Some("This is the fourth line"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linebuf_cursor_motion() {
|
||||
let mut buf = LineBuf::new().with_initial("Thé quíck 🦊 bröwn fóx jumpś óver the 💤 lázy dóg 🐶", 0);
|
||||
|
||||
buf.update_graphemes_lazy();
|
||||
let total = buf.grapheme_indices.as_ref().unwrap().len();
|
||||
|
||||
for i in 0..total {
|
||||
buf.cursor.set(i);
|
||||
|
||||
let expected_to = buf.buffer.get(..buf.grapheme_indices_owned()[i]).unwrap_or("").to_string();
|
||||
let expected_from = if i + 1 < total {
|
||||
buf.buffer.get(buf.grapheme_indices_owned()[i]..).unwrap_or("").to_string()
|
||||
} else {
|
||||
// last grapheme, ends at buffer end
|
||||
buf.buffer.get(buf.grapheme_indices_owned()[i]..).unwrap_or("").to_string()
|
||||
};
|
||||
|
||||
let expected_at = {
|
||||
let start = buf.grapheme_indices_owned()[i];
|
||||
let end = buf.grapheme_indices_owned().get(i + 1).copied().unwrap_or(buf.buffer.len());
|
||||
buf.buffer.get(start..end).map(|slice| slice.to_string())
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
buf.slice_to_cursor(),
|
||||
Some(expected_to.as_str()),
|
||||
"Failed at cursor position {i}: slice_to_cursor"
|
||||
);
|
||||
assert_eq!(
|
||||
buf.slice_from_cursor(),
|
||||
Some(expected_from.as_str()),
|
||||
"Failed at cursor position {i}: slice_from_cursor"
|
||||
);
|
||||
assert_eq!(
|
||||
buf.grapheme_at(i).map(|slice| slice.to_string()),
|
||||
expected_at,
|
||||
"Failed at cursor position {i}: grapheme_at"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user