diff --git a/src/prompt/readline/line.rs b/src/prompt/readline/line.rs index 2f9c7e6..de3c310 100644 --- a/src/prompt/readline/line.rs +++ b/src/prompt/readline/line.rs @@ -1,6 +1,6 @@ use std::ops::Range; -use crate::{libsh::error::ShResult, prompt::readline::linecmd::Anchor}; +use crate::{libsh::{error::ShResult, term::{Style, Styled}}, prompt::readline::linecmd::Anchor}; use super::linecmd::{At, CharSearch, MoveCmd, Movement, Verb, VerbCmd, Word}; @@ -19,16 +19,91 @@ impl LineBuf { self.buffer = init.to_string().chars().collect(); self } + pub fn display_lines(&self) -> Vec { + 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 { + 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::() + } 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 @@ -46,14 +121,17 @@ impl LineBuf { if pos < self.cursor() { self.cursor = self.cursor.saturating_sub(1) } - if self.cursor() >= self.buffer.len() { - self.cursor = self.buffer.len().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() { @@ -108,11 +186,55 @@ impl LineBuf { 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) -> char { - self.buffer[self.cursor] + 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 { + if self.cursor() == 0 { + None + } else { + Some(self.get_char(self.cursor() - 1)) + } + } + pub fn next_char(&self) -> Option { + 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 bool>(&self, mut start: usize, cond: F) -> usize { while start > 0 && !cond(start) { @@ -126,7 +248,7 @@ impl LineBuf { } start } - pub fn calc_range(&self, movement: &Movement) -> Range { + pub fn calc_range(&mut self, movement: &Movement) -> Range { let mut start = self.cursor(); let mut end = self.cursor(); @@ -149,28 +271,73 @@ impl LineBuf { end = self.forward_until(end, |pos| self.buffer[pos] == '\n'); } Movement::BackwardWord(word) => { - let cur_char = self.cursor_char(); 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()); - start += 1; + if self.get_char(start).is_whitespace() { + start += 1; + } } Word::Normal => { - if cur_char.is_alphanumeric() || cur_char == '_' { - start = self.backward_until(start, |pos| !(self.buffer[pos].is_alphanumeric() || self.buffer[pos] == '_')); - start += 1; - } else { - start = self.backward_until(start, |pos| (self.buffer[pos].is_alphanumeric() || self.buffer[pos] == '_')); + 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 cur_char = self.cursor_char(); + 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(); @@ -178,38 +345,68 @@ impl LineBuf { Word::Big => { if cur_char.is_whitespace() { end = self.forward_until(end, not_ws); - } else { - end = self.forward_until(end, is_ws); - 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 => {/* Done */} + 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); - end = end.saturating_sub(1); + if self.buffer.get(end).is_some_and(|ch| ch.is_whitespace()) { + end = end.saturating_sub(1); + } } } } Word::Normal => { - let ch_class = CharClass::from(self.buffer[end]); if cur_char.is_whitespace() { end = self.forward_until(end, not_ws); - } else { - end = self.forward_until(end, |pos| ch_class.is_opposite(self.buffer[pos])) + } + + 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 => {/* Done */ } + 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])); - end = end.saturating_sub(1); + if self.buffer.get(end).is_some_and(|ch| ch.is_whitespace()) { + end = end.saturating_sub(1); + } } } } @@ -225,51 +422,58 @@ impl LineBuf { Movement::CharSearch(char_search) => { match char_search { CharSearch::FindFwd(ch) => { - let search = self.forward_until(end, |pos| self.buffer[pos] == *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[search] == *ch { + if self.buffer.get(search).is_some_and(|&s_ch| s_ch == ch) { end = search; } } CharSearch::FwdTo(ch) => { - let search = self.forward_until(end, |pos| self.buffer[pos] == *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[search] == *ch { + if self.buffer.get(search).is_some_and(|&s_ch| s_ch == ch) { end = search.saturating_sub(1); } } CharSearch::FindBkwd(ch) => { - let search = self.forward_until(start, |pos| self.buffer[pos] == *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[search] == *ch { + if self.buffer.get(search).is_some_and(|&s_ch| s_ch == ch) { start = search; } } CharSearch::BkwdTo(ch) => { - let search = self.forward_until(start, |pos| self.buffer[pos] == *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[search] == *ch { + if self.buffer.get(search).is_some_and(|&s_ch| s_ch == ch) { start = search.saturating_add(1); } } } } - Movement::ViFirstPrint => todo!(), Movement::LineUp => todo!(), Movement::LineDown => todo!(), Movement::WholeBuffer => { start = 0; - end = self.len().saturating_sub(1); + end = self.len_minus_one(); } Movement::BeginningOfBuffer => { start = 0; } Movement::EndOfBuffer => { - end = self.len().saturating_sub(1); + end = self.len_minus_one(); } Movement::Null => {/* nothing */} } @@ -299,6 +503,22 @@ impl LineBuf { } } } + 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) => self.insert_at_cursor(ch), Verb::InsertMode => todo!(), Verb::JoinLines => todo!(), @@ -308,8 +528,22 @@ impl LineBuf { Verb::Put(_) => todo!(), Verb::Undo => todo!(), Verb::RepeatLast => todo!(), - Verb::Dedent => todo!(), - Verb::Indent => 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!() } @@ -333,10 +567,21 @@ impl LineBuf { 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::DeleteOne(anchor) => todo!(), - Verb::Change => todo!(), + Verb::Breakline(anchor) => todo!(), Verb::Yank => todo!(), Verb::ReplaceChar(_) => todo!(), Verb::Substitute => todo!(), @@ -358,13 +603,13 @@ impl LineBuf { #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum CharClass { AlphaNum, - Symbol + Symbol, } impl CharClass { - pub fn is_opposite(&self, ch: char) -> bool { - let opp_class = CharClass::from(ch); - opp_class != *self + pub fn is_opposite(&self, other: char) -> bool { + let other_class = CharClass::from(other); + other_class != *self } } @@ -379,7 +624,7 @@ impl From for CharClass { } -pub fn strip_ansi_codes(s: &str) -> String { +pub fn strip_ansi_codes_and_escapes(s: &str) -> String { let mut out = String::with_capacity(s.len()); let mut chars = s.chars().peekable(); @@ -395,7 +640,11 @@ pub fn strip_ansi_codes(s: &str) -> String { chars.next(); // consume intermediate characters } } else { - out.push(c); + match c { + '\n' | + '\r' => { /* Continue */ } + _ => out.push(c) + } } } out diff --git a/src/prompt/readline/linecmd.rs b/src/prompt/readline/linecmd.rs index c8ed00d..e3058bb 100644 --- a/src/prompt/readline/linecmd.rs +++ b/src/prompt/readline/linecmd.rs @@ -34,6 +34,18 @@ impl ViCmdBuilder { let Self { verb_count, verb, move_count, movement: _ } = self; Self { verb_count, verb, move_count, movement: Some(movement) } } + pub fn verb_count(&self) -> Option { + self.verb_count + } + pub fn move_count(&self) -> Option { + 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; @@ -128,6 +140,7 @@ pub enum Verb { /// `J` — join lines JoinLines, InsertChar(char), + Breakline(Anchor), Indent, Dedent } @@ -147,6 +160,7 @@ impl Verb { Verb::Dedent | Verb::Indent | Verb::InsertChar(_) | + Verb::Breakline(_) | Verb::ReplaceChar(_) => false, Verb::Delete | Verb::Change | @@ -171,8 +185,6 @@ pub enum Movement { ForwardWord(At, Word), // Forward until start/end of word /// character-search, character-search-backward, vi-char-search CharSearch(CharSearch), - /// vi-first-print - ViFirstPrint, /// backward-char BackwardChar, /// forward-char @@ -200,7 +212,6 @@ impl Movement { Self::BackwardWord(_) | Self::ForwardWord(_, _) | Self::CharSearch(_) | - Self::ViFirstPrint | Self::BackwardChar | Self::ForwardChar | Self::LineUp | @@ -334,10 +345,10 @@ pub enum Anchor { #[derive(Debug, Clone, Eq, PartialEq, Copy)] pub enum CharSearch { - FindFwd(char), - FwdTo(char), - FindBkwd(char), - BkwdTo(char) + FindFwd(Option), + FwdTo(Option), + FindBkwd(Option), + BkwdTo(Option), } #[derive(Debug, Clone, Eq, PartialEq, Copy)] @@ -347,13 +358,6 @@ pub enum Word { } -const fn repeat_count(previous: RepeatCount, new: Option) -> RepeatCount { - match new { - Some(n) => n, - None => previous, - } -} - #[derive(Default,Debug,Clone,Copy,PartialEq,Eq,PartialOrd,Ord)] pub enum InputMode { Normal, diff --git a/src/prompt/readline/mod.rs b/src/prompt/readline/mod.rs index f0dd33d..afaf32b 100644 --- a/src/prompt/readline/mod.rs +++ b/src/prompt/readline/mod.rs @@ -1,8 +1,8 @@ use std::{arch::asm, os::fd::BorrowedFd}; use keys::KeyEvent; -use line::{strip_ansi_codes, LineBuf}; -use linecmd::{Anchor, At, InputMode, LineCmd, MoveCmd, Movement, Verb, VerbCmd, ViCmd, ViCmdBuilder, Word}; +use line::{strip_ansi_codes_and_escapes, LineBuf}; +use linecmd::{Anchor, At, CharSearch, InputMode, LineCmd, MoveCmd, Movement, Verb, VerbCmd, ViCmd, ViCmdBuilder, Word}; use nix::{libc::STDIN_FILENO, sys::termios::{self, Termios}, unistd::read}; use term::Terminal; use unicode_width::UnicodeWidthStr; @@ -13,6 +13,33 @@ pub mod line; pub mod keys; pub mod linecmd; +/// Add a verb to a specified ViCmdBuilder, then build it +/// +/// Returns the built value as a LineCmd::ViCmd +macro_rules! build_verb { + ($cmd:expr,$verb:expr) => {{ + $cmd.with_verb($verb).build().map(|cmd| LineCmd::ViCmd(cmd)) + }} +} + +/// Add a movement to a specified ViCmdBuilder, then build it +/// +/// Returns the built value as a LineCmd::ViCmd +macro_rules! build_movement { + ($cmd:expr,$move:expr) => {{ + $cmd.with_movement($move).build().map(|cmd| LineCmd::ViCmd(cmd)) + }} +} + +/// 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, @@ -70,19 +97,50 @@ impl FernReader { } let mut prompt_lines = self.prompt.lines().peekable(); let mut last_line_len = 0; + let lines = self.line.display_lines(); while let Some(line) = prompt_lines.next() { if prompt_lines.peek().is_none() { - last_line_len = strip_ansi_codes(line).width(); + last_line_len = strip_ansi_codes_and_escapes(line).width(); self.term.write(line); } else { self.term.writeln(line); } } - let line = self.pack_line(); - self.term.write(&line); + let num_lines = lines.len(); + let mut lines_iter = lines.into_iter().peekable(); - let cursor_offset = self.line.cursor() + last_line_len; - self.term.write(&format!("\r\x1b[{}C", cursor_offset)); + while let Some(line) = lines_iter.next() { + if lines_iter.peek().is_some() { + self.term.writeln(&line); + } else { + self.term.write(&line); + } + } + + if num_lines == 1 { + let cursor_offset = self.line.cursor() + last_line_len; + self.term.write(&format!("\r\x1b[{}C", cursor_offset)); + } else { + let (x, y) = self.line.cursor_display_coords(); + // Y-axis movements are 1-indexed and must move up from the bottom + // Therefore, add 1 to Y and subtract that number from the number of lines + // to find the number of times we have to push the cursor upward + let y = num_lines.saturating_sub(y+1); + 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 next_cmd(&mut self) -> ShResult { let vi_cmd = ViCmdBuilder::new(); @@ -97,12 +155,7 @@ impl FernReader { 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) => { - let cmd = pending_cmd - .with_verb(Verb::InsertChar(ch)) - .build()?; - LineCmd::ViCmd(cmd) - } + E(K::Char(ch), M::NONE) => build_verb!(pending_cmd, Verb::InsertChar(ch))?, E(K::Char('H'), M::CTRL) | E(K::Backspace, M::NONE) => LineCmd::backspace(), @@ -114,10 +167,7 @@ impl FernReader { E(K::Esc, M::NONE) => { self.edit_mode = InputMode::Normal; - let cmd = pending_cmd - .with_movement(Movement::BackwardChar) - .build()?; - LineCmd::ViCmd(cmd) + build_movement!(pending_cmd, Movement::BackwardChar)? } E(K::Char('D'), M::CTRL) => LineCmd::EndOfFile, _ => { @@ -131,6 +181,31 @@ impl FernReader { pub fn get_normal_cmd(&mut self, mut pending_cmd: ViCmdBuilder) -> ShResult { 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 { pending_cmd.append_digit(digit); return self.get_normal_cmd(pending_cmd); @@ -144,55 +219,91 @@ impl FernReader { } E(K::Char('j'), M::NONE) => LineCmd::LineDownOrNextHistory, E(K::Char('k'), M::NONE) => LineCmd::LineUpOrPreviousHistory, - E(K::Char('l'), M::NONE) => { - let cmd = pending_cmd - .with_movement(Movement::ForwardChar) - .build()?; - LineCmd::ViCmd(cmd) + 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.edit_mode = InputMode::Insert; + build_verb!(pending_cmd,Verb::Breakline(Anchor::After))? } - E(K::Char('w'), M::NONE) => { - let cmd = pending_cmd - .with_movement(Movement::ForwardWord(At::Start, Word::Normal)) - .build()?; - LineCmd::ViCmd(cmd) - } - E(K::Char('W'), M::NONE) => { - let cmd = pending_cmd - .with_movement(Movement::ForwardWord(At::Start, Word::Big)) - .build()?; - LineCmd::ViCmd(cmd) - } - E(K::Char('b'), M::NONE) => { - let cmd = pending_cmd - .with_movement(Movement::BackwardWord(Word::Normal)) - .build()?; - LineCmd::ViCmd(cmd) - } - E(K::Char('B'), M::NONE) => { - let cmd = pending_cmd - .with_movement(Movement::BackwardWord(Word::Big)) - .build()?; - LineCmd::ViCmd(cmd) - } - E(K::Char('x'), M::NONE) => { - let cmd = pending_cmd - .with_verb(Verb::DeleteOne(Anchor::After)) - .build()?; - LineCmd::ViCmd(cmd) + E(K::Char('O'), M::NONE) => { + self.edit_mode = InputMode::Insert; + build_verb!(pending_cmd,Verb::Breakline(Anchor::Before))? } E(K::Char('i'), M::NONE) => { self.edit_mode = InputMode::Insert; - let cmd = pending_cmd - .with_movement(Movement::BackwardChar) - .build()?; - LineCmd::ViCmd(cmd) + LineCmd::Null } E(K::Char('I'), M::NONE) => { self.edit_mode = InputMode::Insert; - let cmd = pending_cmd - .with_movement(Movement::BeginningOfFirstWord) - .build()?; - LineCmd::ViCmd(cmd) + build_movement!(pending_cmd,Movement::BeginningOfFirstWord)? + } + E(K::Char('a'), M::NONE) => { + self.edit_mode = InputMode::Insert; + build_movement!(pending_cmd,Movement::ForwardChar)? + } + E(K::Char('A'), M::NONE) => { + self.edit_mode = InputMode::Insert; + 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..."); @@ -205,44 +316,18 @@ impl FernReader { pub fn common_cmd(&mut self, key: KeyEvent, pending_cmd: ViCmdBuilder) -> ShResult { use keys::{KeyEvent as E, KeyCode as K, ModKeys as M}; match key { - E(K::Home, M::NONE) => { - let cmd = pending_cmd - .with_movement(Movement::BeginningOfLine) - .build()?; - Ok(LineCmd::ViCmd(cmd)) - } - E(K::End, M::NONE) => { - let cmd = pending_cmd - .with_movement(Movement::EndOfLine) - .build()?; - Ok(LineCmd::ViCmd(cmd)) - } - E(K::Left, M::NONE) => { - let cmd = pending_cmd - .with_movement(Movement::BackwardChar) - .build()?; - Ok(LineCmd::ViCmd(cmd)) - } - E(K::Right, M::NONE) => { - let cmd = pending_cmd - .with_movement(Movement::ForwardChar) - .build()?; - Ok(LineCmd::ViCmd(cmd)) - } - E(K::Delete, M::NONE) => { - let cmd = pending_cmd - .with_movement(Movement::ForwardChar) - .with_verb(Verb::Delete) - .build()?; - Ok(LineCmd::ViCmd(cmd)) - } + E(K::Home, M::NONE) => build_movement!(pending_cmd,Movement::BeginningOfLine), + E(K::End, M::NONE) => build_movement!(pending_cmd,Movement::EndOfLine), + E(K::Left, M::NONE) => build_movement!(pending_cmd,Movement::BackwardChar), + E(K::Right, M::NONE) => build_movement!(pending_cmd,Movement::ForwardChar), + E(K::Delete, M::NONE) => build_moveverb!(pending_cmd,Verb::Delete,Movement::ForwardChar), + E(K::Up, M::NONE) => Ok(LineCmd::LineUpOrPreviousHistory), + E(K::Down, M::NONE) => Ok(LineCmd::LineDownOrNextHistory), + E(K::Enter, M::NONE) => Ok(LineCmd::AcceptLine), E(K::Backspace, M::NONE) | E(K::Char('h'), M::CTRL) => { Ok(LineCmd::backspace()) } - E(K::Up, M::NONE) => Ok(LineCmd::LineUpOrPreviousHistory), - E(K::Down, M::NONE) => Ok(LineCmd::LineDownOrNextHistory), - E(K::Enter, M::NONE) => Ok(LineCmd::AcceptLine), _ => Err(ShErr::simple(ShErrKind::ReadlineErr,format!("Unhandled common key event: {key:?}"))) } } @@ -251,10 +336,14 @@ impl FernReader { ViCmd::MoveVerb(verb_cmd, move_cmd) => { self.last_effect = Some(verb_cmd.clone()); self.last_movement = Some(move_cmd.clone()); + let VerbCmd { verb_count, verb } = verb_cmd; for _ in 0..verb_count { self.line.exec_vi_cmd(Some(verb.clone()), Some(move_cmd.clone()))?; } + if verb == Verb::Change { + self.edit_mode = InputMode::Insert + } } ViCmd::Verb(verb_cmd) => { self.last_effect = Some(verb_cmd.clone()); @@ -318,3 +407,9 @@ impl FernReader { } } +impl Drop for FernReader { + fn drop(&mut self) { + self.term.write("\x1b[2 q"); + } +} + diff --git a/src/prompt/readline/term.rs b/src/prompt/readline/term.rs index 2dfe977..a402bfd 100644 --- a/src/prompt/readline/term.rs +++ b/src/prompt/readline/term.rs @@ -92,6 +92,25 @@ impl Terminal { } KeyEvent(KeyCode::Null, ModKeys::empty()) } + + pub fn cursor_pos(&self) -> (usize, usize) { + self.write("\x1b[6n"); + let mut buf = [0u8;32]; + let n = self.read_byte(&mut buf); + + + let response = std::str::from_utf8(&buf[..n]).unwrap_or(""); + let mut row = 0; + let mut col = 0; + if let Some(caps) = response.strip_prefix("\x1b[").and_then(|s| s.strip_suffix("R")) { + let mut parts = caps.split(';'); + if let (Some(rowstr), Some(colstr)) = (parts.next(), parts.next()) { + row = rowstr.parse().unwrap_or(1); + col = colstr.parse().unwrap_or(1); + } + } + (row,col) + } } impl Default for Terminal {