Improved logical accuracy of Ctrl+W in insert mode
Moved test libraries to dev-dependencies Implemented some more motion types Implemented ToLower, ToUpper, JoinLines, Indent, Undo, and Redo verbs 'O' and 'o' operators now behave correctly Added many more unit tests for the readline module
This commit is contained in:
@@ -4,10 +4,11 @@ use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use super::{term::Layout, vicmd::{Anchor, Dest, Direction, Motion, MotionBehavior, MotionCmd, RegisterName, To, Verb, ViCmd, Word}};
|
||||
use crate::{libsh::error::ShResult, prelude::*};
|
||||
use crate::{libsh::error::{ShErr, ShErrKind, ShResult}, prelude::*};
|
||||
|
||||
#[derive(PartialEq,Eq,Debug,Clone,Copy)]
|
||||
#[derive(Default,PartialEq,Eq,Debug,Clone,Copy)]
|
||||
pub enum CharClass {
|
||||
#[default]
|
||||
Alphanum,
|
||||
Symbol,
|
||||
Whitespace,
|
||||
@@ -40,6 +41,14 @@ impl From<&str> for CharClass {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<char> for CharClass {
|
||||
fn from(value: char) -> Self {
|
||||
let mut buf = [0u8; 4]; // max UTF-8 char size
|
||||
let slice = value.encode_utf8(&mut buf); // get str slice
|
||||
CharClass::from(slice as &str)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_whitespace(a: &str) -> bool {
|
||||
CharClass::from(a) == CharClass::Whitespace
|
||||
}
|
||||
@@ -98,6 +107,9 @@ pub enum MotionKind {
|
||||
Null
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum MotionRange {}
|
||||
|
||||
impl MotionKind {
|
||||
pub fn inclusive(range: RangeInclusive<usize>) -> Self {
|
||||
Self::Inclusive((*range.start(),*range.end()))
|
||||
@@ -265,6 +277,7 @@ pub struct LineBuf {
|
||||
pub select_range: Option<(usize,usize)>,
|
||||
pub last_selection: Option<(usize,usize)>,
|
||||
|
||||
pub insert_mode_start_pos: Option<usize>,
|
||||
pub saved_col: Option<usize>,
|
||||
|
||||
pub undo_stack: Vec<Edit>,
|
||||
@@ -337,6 +350,12 @@ impl LineBuf {
|
||||
pub fn grapheme_at_cursor(&mut self) -> Option<&str> {
|
||||
self.grapheme_at(self.cursor.get())
|
||||
}
|
||||
pub fn mark_insert_mode_start_pos(&mut self) {
|
||||
self.insert_mode_start_pos = Some(self.cursor.get())
|
||||
}
|
||||
pub fn clear_insert_mode_start_pos(&mut self) {
|
||||
self.insert_mode_start_pos = None
|
||||
}
|
||||
pub fn slice(&mut self, range: Range<usize>) -> Option<&str> {
|
||||
self.update_graphemes_lazy();
|
||||
let start_index = self.grapheme_indices().get(range.start).copied()?;
|
||||
@@ -380,9 +399,17 @@ impl LineBuf {
|
||||
pub fn slice_to_cursor(&mut self) -> Option<&str> {
|
||||
self.slice_to(self.cursor.get())
|
||||
}
|
||||
pub fn slice_to_cursor_inclusive(&mut self) -> Option<&str> {
|
||||
self.slice_to(self.cursor.ret_add(1))
|
||||
}
|
||||
pub fn slice_from_cursor(&mut self) -> Option<&str> {
|
||||
self.slice_from(self.cursor.get())
|
||||
}
|
||||
pub fn remove(&mut self, pos: usize) {
|
||||
let idx = self.index_byte_pos(pos);
|
||||
self.buffer.remove(idx);
|
||||
self.update_graphemes();
|
||||
}
|
||||
pub fn drain(&mut self, start: usize, end: usize) -> String {
|
||||
let drained = if end == self.grapheme_indices().len() {
|
||||
if start == self.grapheme_indices().len() {
|
||||
@@ -588,7 +615,26 @@ impl LineBuf {
|
||||
To::Start => {
|
||||
match dir {
|
||||
Direction::Forward => self.start_of_word_forward_or_end_of_word_backward_from(pos.get(), word, dir),
|
||||
Direction::Backward => self.end_of_word_forward_or_start_of_word_backward_from(pos.get(), word, dir)
|
||||
Direction::Backward => 'backward: {
|
||||
// We also need to handle insert mode's Ctrl+W behaviors here
|
||||
let target = self.end_of_word_forward_or_start_of_word_backward_from(pos.get(), word, dir);
|
||||
|
||||
// Check to see if we are in insert mode
|
||||
let Some(start_pos) = self.insert_mode_start_pos else {
|
||||
break 'backward target
|
||||
};
|
||||
// If we are in front of start_pos, and we would cross start_pos to reach target
|
||||
// then stop at start_pos
|
||||
if start_pos > target && self.cursor.get() > start_pos {
|
||||
return start_pos
|
||||
} else {
|
||||
// We are behind start_pos, now we just reset it
|
||||
if self.cursor.get() < start_pos {
|
||||
self.clear_insert_mode_start_pos();
|
||||
}
|
||||
break 'backward target
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
To::End => {
|
||||
@@ -865,6 +911,15 @@ impl LineBuf {
|
||||
pub fn replace_at_cursor(&mut self, new: &str) {
|
||||
self.replace_at(self.cursor.get(), new);
|
||||
}
|
||||
pub fn force_replace_at(&mut self, pos: usize, new: &str) {
|
||||
let Some(gr) = self.grapheme_at(pos).map(|gr| gr.to_string()) else {
|
||||
self.buffer.push_str(new);
|
||||
return
|
||||
};
|
||||
let start = self.index_byte_pos(pos);
|
||||
let end = start + gr.len();
|
||||
self.buffer.replace_range(start..end, new);
|
||||
}
|
||||
pub fn replace_at(&mut self, pos: usize, new: &str) {
|
||||
let Some(gr) = self.grapheme_at(pos).map(|gr| gr.to_string()) else {
|
||||
self.buffer.push_str(new);
|
||||
@@ -1112,10 +1167,10 @@ impl LineBuf {
|
||||
MotionCmd(count,Motion::FirstGraphicalOnScreenLine) => todo!(),
|
||||
MotionCmd(count,Motion::HalfOfScreen) => todo!(),
|
||||
MotionCmd(count,Motion::HalfOfScreenLineText) => todo!(),
|
||||
MotionCmd(count,Motion::WholeBuffer) => todo!(),
|
||||
MotionCmd(count,Motion::BeginningOfBuffer) => todo!(),
|
||||
MotionCmd(count,Motion::EndOfBuffer) => todo!(),
|
||||
MotionCmd(count,Motion::ToColumn(col)) => todo!(),
|
||||
MotionCmd(_count,Motion::WholeBuffer) => MotionKind::Exclusive((0,self.grapheme_indices().len())),
|
||||
MotionCmd(_count,Motion::BeginningOfBuffer) => MotionKind::On(0),
|
||||
MotionCmd(_count,Motion::EndOfBuffer) => MotionKind::To(self.grapheme_indices().len()),
|
||||
MotionCmd(_count,Motion::ToColumn) => todo!(),
|
||||
MotionCmd(count,Motion::ToDelimMatch) => todo!(),
|
||||
MotionCmd(count,Motion::ToBrace(direction)) => todo!(),
|
||||
MotionCmd(count,Motion::ToBracket(direction)) => todo!(),
|
||||
@@ -1233,6 +1288,7 @@ impl LineBuf {
|
||||
};
|
||||
Some(range)
|
||||
}
|
||||
#[allow(clippy::unnecessary_to_owned)]
|
||||
pub fn exec_verb(&mut self, verb: Verb, motion: MotionKind, register: RegisterName) -> ShResult<()> {
|
||||
match verb {
|
||||
Verb::Delete |
|
||||
@@ -1318,16 +1374,101 @@ impl LineBuf {
|
||||
self.replace_at(i,new);
|
||||
}
|
||||
}
|
||||
Verb::ToLower => todo!(),
|
||||
Verb::ToUpper => todo!(),
|
||||
Verb::Complete => todo!(),
|
||||
Verb::CompleteBackward => todo!(),
|
||||
Verb::Undo => todo!(),
|
||||
Verb::Redo => todo!(),
|
||||
Verb::ToLower => {
|
||||
let Some((start,end)) = self.range_from_motion(&motion) else {
|
||||
return Ok(())
|
||||
};
|
||||
for i in start..end {
|
||||
let Some(gr) = self.grapheme_at(i) else {
|
||||
continue
|
||||
};
|
||||
if gr.len() > 1 || gr.is_empty() {
|
||||
continue
|
||||
}
|
||||
let ch = gr.chars().next().unwrap();
|
||||
if !ch.is_alphabetic() {
|
||||
continue
|
||||
}
|
||||
let mut buf = [0u8;4];
|
||||
let new = if ch.is_ascii_uppercase() {
|
||||
ch.to_ascii_lowercase().encode_utf8(&mut buf)
|
||||
} else {
|
||||
ch.encode_utf8(&mut buf)
|
||||
};
|
||||
self.replace_at(i,new);
|
||||
}
|
||||
}
|
||||
Verb::ToUpper => {
|
||||
let Some((start,end)) = self.range_from_motion(&motion) else {
|
||||
return Ok(())
|
||||
};
|
||||
for i in start..end {
|
||||
let Some(gr) = self.grapheme_at(i) else {
|
||||
continue
|
||||
};
|
||||
if gr.len() > 1 || gr.is_empty() {
|
||||
continue
|
||||
}
|
||||
let ch = gr.chars().next().unwrap();
|
||||
if !ch.is_alphabetic() {
|
||||
continue
|
||||
}
|
||||
let mut buf = [0u8;4];
|
||||
let new = if ch.is_ascii_lowercase() {
|
||||
ch.to_ascii_uppercase().encode_utf8(&mut buf)
|
||||
} else {
|
||||
ch.encode_utf8(&mut buf)
|
||||
};
|
||||
self.replace_at(i,new);
|
||||
}
|
||||
}
|
||||
Verb::Redo |
|
||||
Verb::Undo => {
|
||||
let (edit_provider,edit_receiver) = match verb {
|
||||
Verb::Redo => (&mut self.redo_stack, &mut self.undo_stack),
|
||||
Verb::Undo => (&mut self.undo_stack, &mut self.redo_stack),
|
||||
_ => unreachable!()
|
||||
};
|
||||
let Some(edit) = edit_provider.pop() else { return Ok(()) };
|
||||
let Edit { pos, cursor_pos, old, new, merging: _ } = edit;
|
||||
|
||||
self.buffer.replace_range(pos..pos + new.len(), &old);
|
||||
let new_cursor_pos = self.cursor.get();
|
||||
let in_insert_mode = !self.cursor.exclusive;
|
||||
|
||||
if in_insert_mode {
|
||||
self.cursor.set(cursor_pos)
|
||||
}
|
||||
let new_edit = Edit { pos, cursor_pos: new_cursor_pos, old: new, new: old, merging: false };
|
||||
edit_receiver.push(new_edit);
|
||||
self.update_graphemes();
|
||||
}
|
||||
Verb::RepeatLast => todo!(),
|
||||
Verb::Put(anchor) => todo!(),
|
||||
Verb::SwapVisualAnchor => todo!(),
|
||||
Verb::JoinLines => todo!(),
|
||||
Verb::JoinLines => {
|
||||
let start = self.start_of_line();
|
||||
let Some((_,mut end)) = self.nth_next_line(1) else {
|
||||
return Ok(())
|
||||
};
|
||||
end = end.saturating_sub(1); // exclude the last newline
|
||||
let mut last_was_whitespace = false;
|
||||
for i in start..end {
|
||||
let Some(gr) = self.grapheme_at(i) else {
|
||||
continue
|
||||
};
|
||||
if gr == "\n" {
|
||||
if last_was_whitespace {
|
||||
self.remove(i);
|
||||
} else {
|
||||
self.force_replace_at(i, " ");
|
||||
}
|
||||
last_was_whitespace = false;
|
||||
continue
|
||||
}
|
||||
last_was_whitespace = is_whitespace(gr);
|
||||
}
|
||||
}
|
||||
Verb::InsertChar(ch) => {
|
||||
self.insert_at_cursor(ch);
|
||||
self.cursor.add(1);
|
||||
@@ -1338,19 +1479,56 @@ impl LineBuf {
|
||||
self.cursor.add(graphemes);
|
||||
}
|
||||
Verb::Breakline(anchor) => todo!(),
|
||||
Verb::Indent => todo!(),
|
||||
Verb::Dedent => todo!(),
|
||||
Verb::Indent => {
|
||||
let Some((start,end)) = self.range_from_motion(&motion) else {
|
||||
return Ok(())
|
||||
};
|
||||
self.insert_at(start, '\t');
|
||||
let mut range_indices = self.grapheme_indices()[start..end].to_vec().into_iter();
|
||||
while let Some(idx) = range_indices.next() {
|
||||
let gr = self.grapheme_at(idx).unwrap();
|
||||
if gr == "\n" {
|
||||
let Some(idx) = range_indices.next() else {
|
||||
self.push('\t');
|
||||
break
|
||||
};
|
||||
self.insert_at(idx, '\t');
|
||||
}
|
||||
}
|
||||
|
||||
match motion {
|
||||
MotionKind::ExclusiveWithTargetCol((_,_),pos) |
|
||||
MotionKind::InclusiveWithTargetCol((_,_),pos) => {
|
||||
self.cursor.set(start);
|
||||
let end = self.end_of_line();
|
||||
self.cursor.add(end.min(pos));
|
||||
}
|
||||
_ => self.cursor.set(start),
|
||||
}
|
||||
}
|
||||
Verb::Dedent => {
|
||||
let (start,end) = self.this_line();
|
||||
|
||||
}
|
||||
Verb::Equalize => todo!(),
|
||||
Verb::InsertModeLineBreak(anchor) => {
|
||||
let end = self.end_of_line();
|
||||
self.insert_at(end,'\n');
|
||||
self.cursor.set(end);
|
||||
let (mut start,end) = self.this_line();
|
||||
// We want the position of the newline, or start of buffer
|
||||
start = start.saturating_sub(1).min(self.cursor.max);
|
||||
match anchor {
|
||||
Anchor::After => self.cursor.add(2),
|
||||
Anchor::Before => { /* Do nothing */ }
|
||||
Anchor::After => {
|
||||
self.cursor.set(end);
|
||||
self.insert_at_cursor('\n');
|
||||
}
|
||||
Anchor::Before => {
|
||||
self.cursor.set(start);
|
||||
self.insert_at_cursor('\n');
|
||||
self.cursor.add(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Verb::Complete |
|
||||
Verb::EndOfFile |
|
||||
Verb::InsertMode |
|
||||
Verb::NormalMode |
|
||||
@@ -1358,6 +1536,7 @@ impl LineBuf {
|
||||
Verb::ReplaceMode |
|
||||
Verb::VisualModeLine |
|
||||
Verb::VisualModeBlock |
|
||||
Verb::CompleteBackward |
|
||||
Verb::AcceptLineOrNewline |
|
||||
Verb::VisualModeSelectLast => self.apply_motion(motion), // Already handled logic for these
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use keys::{KeyCode, KeyEvent, ModKeys};
|
||||
use linebuf::{LineBuf, SelectAnchor, SelectMode};
|
||||
use nix::libc::STDOUT_FILENO;
|
||||
use term::{Layout, LineWriter, TermReader};
|
||||
use term::{get_win_size, raw_mode, KeyReader, Layout, LineWriter, TermReader, TermWriter};
|
||||
use vicmd::{Motion, MotionCmd, RegisterName, Verb, VerbCmd, ViCmd};
|
||||
use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual};
|
||||
|
||||
use crate::libsh::{error::ShResult, sys::sh_quit, term::{Style, Styled}};
|
||||
use crate::libsh::{error::{ShErr, ShErrKind, ShResult}, sys::sh_quit, term::{Style, Styled}};
|
||||
use crate::prelude::*;
|
||||
|
||||
pub mod term;
|
||||
@@ -21,20 +21,19 @@ pub trait Readline {
|
||||
}
|
||||
|
||||
pub struct FernVi {
|
||||
reader: TermReader,
|
||||
writer: LineWriter,
|
||||
prompt: String,
|
||||
mode: Box<dyn ViMode>,
|
||||
old_layout: Option<Layout>,
|
||||
repeat_action: Option<CmdReplay>,
|
||||
repeat_motion: Option<MotionCmd>,
|
||||
editor: LineBuf
|
||||
pub reader: Box<dyn KeyReader>,
|
||||
pub writer: Box<dyn LineWriter>,
|
||||
pub prompt: String,
|
||||
pub mode: Box<dyn ViMode>,
|
||||
pub old_layout: Option<Layout>,
|
||||
pub repeat_action: Option<CmdReplay>,
|
||||
pub repeat_motion: Option<MotionCmd>,
|
||||
pub editor: LineBuf
|
||||
}
|
||||
|
||||
impl Readline for FernVi {
|
||||
fn readline(&mut self) -> ShResult<String> {
|
||||
self.editor = LineBuf::new().with_initial("\nThe quick brown fox jumps over\n the lazy dogThe quick\nbrown fox jumps over the a", 1004);
|
||||
let raw_mode_guard = self.reader.raw_mode(); // Restores termios state on drop
|
||||
let raw_mode_guard = raw_mode(); // Restores termios state on drop
|
||||
|
||||
loop {
|
||||
let new_layout = self.get_layout();
|
||||
@@ -42,7 +41,11 @@ impl Readline for FernVi {
|
||||
self.writer.clear_rows(layout)?;
|
||||
}
|
||||
raw_mode_guard.disable_for(|| self.print_line(new_layout))?;
|
||||
let key = self.reader.read_key()?;
|
||||
let Some(key) = self.reader.read_key() else {
|
||||
raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?;
|
||||
std::mem::drop(raw_mode_guard);
|
||||
return Err(ShErr::simple(ShErrKind::ReadlineErr, "EOF"))
|
||||
};
|
||||
flog!(DEBUG, key);
|
||||
|
||||
let Some(mut cmd) = self.mode.handle_key(key) else {
|
||||
@@ -52,6 +55,7 @@ impl Readline for FernVi {
|
||||
|
||||
if cmd.should_submit() {
|
||||
raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?;
|
||||
std::mem::drop(raw_mode_guard);
|
||||
return Ok(std::mem::take(&mut self.editor.buffer))
|
||||
}
|
||||
|
||||
@@ -81,21 +85,28 @@ impl Default for FernVi {
|
||||
impl FernVi {
|
||||
pub fn new(prompt: Option<String>) -> Self {
|
||||
Self {
|
||||
reader: TermReader::new(),
|
||||
writer: LineWriter::new(STDOUT_FILENO),
|
||||
reader: Box::new(TermReader::new()),
|
||||
writer: Box::new(TermWriter::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()
|
||||
editor: LineBuf::new().with_initial("\nThe quick brown fox jumps over\n the lazy dogThe quick\nbrown fox jumps over the a", 1004)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_layout(&mut self) -> Layout {
|
||||
let line = self.editor.as_str().to_string();
|
||||
let to_cursor = self.editor.slice_to_cursor().unwrap_or_default();
|
||||
self.writer.get_layout_from_parts(&self.prompt, to_cursor, &line)
|
||||
let (cols,_) = get_win_size(STDIN_FILENO);
|
||||
Layout::from_parts(
|
||||
/*tab_stop:*/ 8,
|
||||
cols,
|
||||
&self.prompt,
|
||||
to_cursor,
|
||||
&line
|
||||
)
|
||||
}
|
||||
|
||||
pub fn print_line(&mut self, new_layout: Layout) -> ShResult<()> {
|
||||
@@ -114,14 +125,20 @@ impl FernVi {
|
||||
|
||||
pub fn exec_cmd(&mut self, mut cmd: ViCmd) -> ShResult<()> {
|
||||
let mut selecting = false;
|
||||
let mut is_insert_mode = 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::InsertMode => {
|
||||
is_insert_mode = true;
|
||||
Box::new(ViInsert::new().with_count(count as u16))
|
||||
}
|
||||
|
||||
Verb::NormalMode => Box::new(ViNormal::new()),
|
||||
Verb::NormalMode => {
|
||||
Box::new(ViNormal::new())
|
||||
}
|
||||
|
||||
Verb::ReplaceMode => Box::new(ViReplace::new()),
|
||||
|
||||
@@ -157,6 +174,11 @@ impl FernVi {
|
||||
} else {
|
||||
self.editor.stop_selecting();
|
||||
}
|
||||
if is_insert_mode {
|
||||
self.editor.mark_insert_mode_start_pos();
|
||||
} else {
|
||||
self.editor.clear_insert_mode_start_pos();
|
||||
}
|
||||
return Ok(())
|
||||
} else if cmd.is_cmd_repeat() {
|
||||
let Some(replay) = self.repeat_action.clone() else {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{env, fmt::{Debug, Write}, io::{BufRead, BufReader, Read}, os::fd::{AsFd, BorrowedFd, RawFd}};
|
||||
use std::{env, fmt::{Debug, Write}, io::{BufRead, BufReader, Read}, iter::Peekable, os::fd::{AsFd, BorrowedFd, RawFd}, str::Chars};
|
||||
|
||||
use nix::{errno::Errno, libc::{self, STDIN_FILENO}, poll::{self, PollFlags, PollTimeout}, sys::termios, unistd::isatty};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
@@ -9,6 +9,15 @@ use crate::prelude::*;
|
||||
|
||||
use super::{keys::KeyEvent, linebuf::LineBuf};
|
||||
|
||||
pub fn raw_mode() -> RawModeGuard {
|
||||
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: STDIN_FILENO }
|
||||
}
|
||||
|
||||
pub type Row = u16;
|
||||
pub type Col = u16;
|
||||
|
||||
@@ -135,6 +144,21 @@ pub trait WidthCalculator {
|
||||
fn width(&self, text: &str) -> usize;
|
||||
}
|
||||
|
||||
pub trait KeyReader {
|
||||
fn read_key(&mut self) -> Option<KeyEvent>;
|
||||
}
|
||||
|
||||
pub trait LineWriter {
|
||||
fn clear_rows(&mut self, layout: &Layout) -> ShResult<()>;
|
||||
fn redraw(
|
||||
&mut self,
|
||||
prompt: &str,
|
||||
line: &LineBuf,
|
||||
new_layout: &Layout,
|
||||
) -> ShResult<()>;
|
||||
fn flush_write(&mut self, buf: &str) -> ShResult<()>;
|
||||
}
|
||||
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
pub struct UnicodeWidth;
|
||||
|
||||
@@ -256,15 +280,6 @@ 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
|
||||
///
|
||||
@@ -303,33 +318,7 @@ impl TermReader {
|
||||
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];
|
||||
@@ -409,6 +398,38 @@ impl TermReader {
|
||||
_ => Ok(KeyEvent(KeyCode::Esc, ModKeys::empty())),
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl KeyReader for TermReader {
|
||||
fn read_key(&mut self) -> Option<KeyEvent> {
|
||||
use core::str;
|
||||
|
||||
let mut collected = Vec::with_capacity(4);
|
||||
|
||||
loop {
|
||||
let byte = self.next_byte().ok()?;
|
||||
flog!(DEBUG, "read byte: {:?}",byte as char);
|
||||
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).ok()? {
|
||||
return self.parse_esc_seq().ok();
|
||||
}
|
||||
|
||||
// Try parse as valid UTF-8
|
||||
if let Ok(s) = str::from_utf8(&collected) {
|
||||
return Some(KeyEvent::new(s, ModKeys::empty()));
|
||||
}
|
||||
|
||||
// UTF-8 max 4 bytes — if it’s invalid at this point, bail
|
||||
if collected.len() >= 4 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl AsFd for TermReader {
|
||||
@@ -444,6 +465,46 @@ impl Layout {
|
||||
end: Pos::default(),
|
||||
}
|
||||
}
|
||||
pub fn from_parts(
|
||||
tab_stop: u16,
|
||||
term_width: u16,
|
||||
prompt: &str,
|
||||
to_cursor: &str,
|
||||
to_end: &str,
|
||||
) -> Self {
|
||||
flog!(DEBUG,to_cursor);
|
||||
let prompt_end = Self::calc_pos(tab_stop, term_width, prompt, Pos { col: 0, row: 0 });
|
||||
let cursor = Self::calc_pos(tab_stop, term_width, to_cursor, prompt_end);
|
||||
let end = Self::calc_pos(tab_stop, term_width, to_end, prompt_end);
|
||||
Layout { w_calc: width_calculator(), prompt_end, cursor, end }
|
||||
}
|
||||
|
||||
pub fn calc_pos(tab_stop: u16, term_width: u16, s: &str, orig: Pos) -> Pos {
|
||||
let mut pos = orig;
|
||||
let mut esc_seq = 0;
|
||||
for c in s.graphemes(true) {
|
||||
if c == "\n" {
|
||||
pos.row += 1;
|
||||
pos.col = 0;
|
||||
}
|
||||
let c_width = if c == "\t" {
|
||||
tab_stop - (pos.col % tab_stop)
|
||||
} else {
|
||||
width(c, &mut esc_seq)
|
||||
};
|
||||
pos.col += c_width;
|
||||
if pos.col > term_width {
|
||||
pos.row += 1;
|
||||
pos.col = c_width;
|
||||
}
|
||||
}
|
||||
if pos.col >= term_width {
|
||||
pos.row += 1;
|
||||
pos.col = 0;
|
||||
}
|
||||
|
||||
pos
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Layout {
|
||||
@@ -452,7 +513,7 @@ impl Default for Layout {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LineWriter {
|
||||
pub struct TermWriter {
|
||||
out: RawFd,
|
||||
t_cols: Col, // terminal width
|
||||
buffer: String,
|
||||
@@ -460,7 +521,7 @@ pub struct LineWriter {
|
||||
tab_stop: u16,
|
||||
}
|
||||
|
||||
impl LineWriter {
|
||||
impl TermWriter {
|
||||
pub fn new(out: RawFd) -> Self {
|
||||
let w_calc = width_calculator();
|
||||
let (t_cols,_) = get_win_size(out);
|
||||
@@ -472,28 +533,6 @@ impl LineWriter {
|
||||
tab_stop: 8 // TODO: add a way to configure this
|
||||
}
|
||||
}
|
||||
pub fn flush_write(&mut self, buf: &str) -> ShResult<()> {
|
||||
write_all(self.out, buf)?;
|
||||
Ok(())
|
||||
}
|
||||
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;
|
||||
|
||||
let cursor_motion = rows_to_clear.saturating_sub(cursor_row);
|
||||
if cursor_motion > 0 {
|
||||
write!(self.buffer, "\x1b[{cursor_motion}B").unwrap()
|
||||
}
|
||||
|
||||
for _ in 0..rows_to_clear {
|
||||
self.buffer.push_str("\x1b[2K\x1b[A");
|
||||
}
|
||||
self.buffer.push_str("\x1b[2K");
|
||||
write_all(self.out,self.buffer.as_str())?;
|
||||
self.buffer.clear();
|
||||
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");
|
||||
@@ -543,67 +582,7 @@ impl LineWriter {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn redraw(
|
||||
&mut self,
|
||||
prompt: &str,
|
||||
line: &LineBuf,
|
||||
new_layout: &Layout,
|
||||
) -> ShResult<()> {
|
||||
let err = |_| ShErr::simple(ShErrKind::InternalErr, "Failed to write to LineWriter internal buffer");
|
||||
self.buffer.clear();
|
||||
|
||||
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 && !self.buffer.ends_with('\n') {
|
||||
// The line has wrapped. We need to use our own line break.
|
||||
self.buffer.push('\n');
|
||||
}
|
||||
|
||||
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;
|
||||
for c in s.graphemes(true) {
|
||||
if c == "\n" {
|
||||
pos.row += 1;
|
||||
pos.col = 0;
|
||||
}
|
||||
let c_width = if c == "\t" {
|
||||
self.tab_stop - (pos.col % self.tab_stop)
|
||||
} else {
|
||||
width(c, &mut esc_seq)
|
||||
};
|
||||
pos.col += c_width;
|
||||
if pos.col > self.t_cols {
|
||||
pos.row += 1;
|
||||
pos.col = c_width;
|
||||
}
|
||||
}
|
||||
if pos.col >= self.t_cols {
|
||||
pos.row += 1;
|
||||
pos.col = 0;
|
||||
}
|
||||
|
||||
pos
|
||||
}
|
||||
|
||||
pub fn update_t_cols(&mut self) {
|
||||
let (t_cols,_) = get_win_size(self.out);
|
||||
@@ -656,3 +635,56 @@ impl LineWriter {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl LineWriter for TermWriter {
|
||||
fn clear_rows(&mut self, layout: &Layout) -> ShResult<()> {
|
||||
self.buffer.clear();
|
||||
let rows_to_clear = layout.end.row;
|
||||
let cursor_row = layout.cursor.row;
|
||||
|
||||
let cursor_motion = rows_to_clear.saturating_sub(cursor_row);
|
||||
if cursor_motion > 0 {
|
||||
write!(self.buffer, "\x1b[{cursor_motion}B").unwrap()
|
||||
}
|
||||
|
||||
for _ in 0..rows_to_clear {
|
||||
self.buffer.push_str("\x1b[2K\x1b[A");
|
||||
}
|
||||
self.buffer.push_str("\x1b[2K");
|
||||
write_all(self.out,self.buffer.as_str())?;
|
||||
self.buffer.clear();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn redraw(
|
||||
&mut self,
|
||||
prompt: &str,
|
||||
line: &LineBuf,
|
||||
new_layout: &Layout,
|
||||
) -> ShResult<()> {
|
||||
let err = |_| ShErr::simple(ShErrKind::InternalErr, "Failed to write to LineWriter internal buffer");
|
||||
self.buffer.clear();
|
||||
|
||||
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 && !self.buffer.ends_with('\n') {
|
||||
// The line has wrapped. We need to use our own line break.
|
||||
self.buffer.push('\n');
|
||||
}
|
||||
|
||||
let movement = self.get_cursor_movement(end, cursor)?;
|
||||
write!(self.buffer, "{}", &movement).map_err(err)?;
|
||||
|
||||
write_all(self.out, self.buffer.as_str())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn flush_write(&mut self, buf: &str) -> ShResult<()> {
|
||||
write_all(self.out, buf)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,7 +279,7 @@ pub enum Motion {
|
||||
WholeBuffer,
|
||||
BeginningOfBuffer,
|
||||
EndOfBuffer,
|
||||
ToColumn(usize),
|
||||
ToColumn,
|
||||
ToDelimMatch,
|
||||
ToBrace(Direction),
|
||||
ToBracket(Direction),
|
||||
@@ -317,7 +317,7 @@ impl Motion {
|
||||
Self::LineUpCharwise |
|
||||
Self::ScreenLineUpCharwise |
|
||||
Self::ScreenLineDownCharwise |
|
||||
Self::ToColumn(_) |
|
||||
Self::ToColumn |
|
||||
Self::TextObj(TextObj::ForwardSentence,_) |
|
||||
Self::TextObj(TextObj::BackwardSentence,_) |
|
||||
Self::TextObj(TextObj::ForwardParagraph,_) |
|
||||
|
||||
@@ -5,6 +5,7 @@ use nix::NixPath;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use super::keys::{KeyCode as K, KeyEvent as E, ModKeys as M};
|
||||
use super::linebuf::CharClass;
|
||||
use super::vicmd::{Anchor, Bound, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb, VerbCmd, ViCmd, Word};
|
||||
use crate::prelude::*;
|
||||
|
||||
@@ -111,15 +112,9 @@ impl ViMode for ViInsert {
|
||||
self.register_and_return()
|
||||
}
|
||||
E(K::Char('W'), M::CTRL) => {
|
||||
if self.ctrl_w_is_undo() {
|
||||
self.pending_cmd.set_verb(VerbCmd(1,Verb::Undo));
|
||||
self.cmds.clear();
|
||||
Some(self.take_cmd())
|
||||
} else {
|
||||
self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete));
|
||||
self.pending_cmd.set_motion(MotionCmd(1, Motion::WordMotion(To::Start, Word::Normal, Direction::Backward)));
|
||||
self.register_and_return()
|
||||
}
|
||||
self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete));
|
||||
self.pending_cmd.set_motion(MotionCmd(1, Motion::WordMotion(To::Start, Word::Normal, Direction::Backward)));
|
||||
self.register_and_return()
|
||||
}
|
||||
E(K::Char('H'), M::CTRL) |
|
||||
E(K::Backspace, M::NONE) => {
|
||||
@@ -297,6 +292,7 @@ impl ViNormal {
|
||||
pub fn take_cmd(&mut self) -> String {
|
||||
std::mem::take(&mut self.pending_seq)
|
||||
}
|
||||
#[allow(clippy::unnecessary_unwrap)]
|
||||
fn validate_combination(&self, verb: Option<&Verb>, motion: Option<&Motion>) -> CmdState {
|
||||
if verb.is_none() {
|
||||
match motion {
|
||||
@@ -663,7 +659,9 @@ impl ViNormal {
|
||||
let Some(ch) = chars_clone.next() else {
|
||||
break 'motion_parse None
|
||||
};
|
||||
// Double inputs like 'dd' and 'cc', and some special cases
|
||||
match (ch, &verb) {
|
||||
// Double inputs
|
||||
('?', Some(VerbCmd(_,Verb::Rot13))) |
|
||||
('d', Some(VerbCmd(_,Verb::Delete))) |
|
||||
('c', Some(VerbCmd(_,Verb::Change))) |
|
||||
@@ -674,7 +672,20 @@ impl ViNormal {
|
||||
('~', Some(VerbCmd(_,Verb::ToggleCaseRange))) |
|
||||
('>', Some(VerbCmd(_,Verb::Indent))) |
|
||||
('<', Some(VerbCmd(_,Verb::Dedent))) => break 'motion_parse Some(MotionCmd(count, Motion::WholeLine)),
|
||||
_ => {}
|
||||
|
||||
// Exception cases
|
||||
('w', Some(VerbCmd(_, Verb::Change))) => {
|
||||
// 'w' usually means 'to the start of the next word'
|
||||
// e.g. 'dw' deleted to the start of the next word
|
||||
// however, 'cw' only changes to the end of the current word
|
||||
// 'caw' is used to change to the start of the next word
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Normal, Direction::Forward)));
|
||||
}
|
||||
('W', Some(VerbCmd(_, Verb::Change))) => {
|
||||
// Same with 'W'
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Big, Direction::Forward)));
|
||||
}
|
||||
_ => { /* Nothing weird, so let's continue */ }
|
||||
}
|
||||
match ch {
|
||||
'g' => {
|
||||
@@ -761,7 +772,7 @@ impl ViNormal {
|
||||
}
|
||||
'|' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(1, Motion::ToColumn(count)));
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::ToColumn));
|
||||
}
|
||||
'^' => {
|
||||
chars = chars_clone;
|
||||
@@ -956,6 +967,8 @@ impl ViVisual {
|
||||
pub fn take_cmd(&mut self) -> String {
|
||||
std::mem::take(&mut self.pending_seq)
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_unwrap)]
|
||||
fn validate_combination(&self, verb: Option<&Verb>, motion: Option<&Motion>) -> CmdState {
|
||||
if verb.is_none() {
|
||||
match motion {
|
||||
@@ -964,7 +977,7 @@ impl ViVisual {
|
||||
None => return CmdState::Pending
|
||||
}
|
||||
}
|
||||
if verb.is_some() && motion.is_none() {
|
||||
if motion.is_none() && verb.is_some() {
|
||||
match verb.unwrap() {
|
||||
Verb::Put(_) => CmdState::Complete,
|
||||
_ => CmdState::Pending
|
||||
@@ -1307,28 +1320,28 @@ impl ViVisual {
|
||||
break 'motion_parse None
|
||||
};
|
||||
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Forward, Dest::On, (*ch).into())))
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Forward, Dest::On, *ch)))
|
||||
}
|
||||
'F' => {
|
||||
let Some(ch) = chars_clone.peek() else {
|
||||
break 'motion_parse None
|
||||
};
|
||||
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Backward, Dest::On, (*ch).into())))
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Backward, Dest::On, *ch)))
|
||||
}
|
||||
't' => {
|
||||
let Some(ch) = chars_clone.peek() else {
|
||||
break 'motion_parse None
|
||||
};
|
||||
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Forward, Dest::Before, (*ch).into())))
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Forward, Dest::Before, *ch)))
|
||||
}
|
||||
'T' => {
|
||||
let Some(ch) = chars_clone.peek() else {
|
||||
break 'motion_parse None
|
||||
};
|
||||
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Backward, Dest::Before, (*ch).into())))
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Backward, Dest::Before, *ch)))
|
||||
}
|
||||
';' => {
|
||||
chars = chars_clone;
|
||||
@@ -1340,7 +1353,7 @@ impl ViVisual {
|
||||
}
|
||||
'|' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(1, Motion::ToColumn(count)));
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::ToColumn));
|
||||
}
|
||||
'0' => {
|
||||
chars = chars_clone;
|
||||
@@ -1427,15 +1440,14 @@ impl ViVisual {
|
||||
|
||||
match self.validate_combination(verb_ref, motion_ref) {
|
||||
CmdState::Complete => {
|
||||
let cmd = Some(
|
||||
Some(
|
||||
ViCmd {
|
||||
register,
|
||||
verb,
|
||||
motion,
|
||||
raw_seq: std::mem::take(&mut self.pending_seq)
|
||||
}
|
||||
);
|
||||
cmd
|
||||
)
|
||||
}
|
||||
CmdState::Pending => {
|
||||
None
|
||||
|
||||
Reference in New Issue
Block a user