Bump to 0.6.1: add Ctrl-D/U half-screen scrolling, Ctrl-G position info, and status messages
This commit is contained in:
@@ -734,7 +734,7 @@ pub trait Completer {
|
||||
let (s, e) = self.token_span();
|
||||
orig.get(s..e).unwrap_or(orig)
|
||||
}
|
||||
fn draw(&mut self, writer: &mut TermWriter) -> ShResult<()>;
|
||||
fn draw(&mut self, writer: &mut TermWriter) -> ShResult<usize>;
|
||||
fn clear(&mut self, _writer: &mut TermWriter) -> ShResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
@@ -1122,9 +1122,9 @@ impl FuzzySelector {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw(&mut self, writer: &mut TermWriter) -> ShResult<()> {
|
||||
pub fn draw(&mut self, writer: &mut TermWriter) -> ShResult<usize> {
|
||||
if !self.active {
|
||||
return Ok(());
|
||||
return Ok(0);
|
||||
}
|
||||
let (cols, _) = get_win_size(*TTY_FILENO);
|
||||
|
||||
@@ -1272,7 +1272,7 @@ impl FuzzySelector {
|
||||
writer.flush_write(&buf)?;
|
||||
self.old_layout = Some(new_layout);
|
||||
|
||||
Ok(())
|
||||
Ok(rows as usize)
|
||||
}
|
||||
|
||||
pub fn clear(&mut self, writer: &mut TermWriter) -> ShResult<()> {
|
||||
@@ -1421,7 +1421,7 @@ impl Completer for FuzzyCompleter {
|
||||
fn clear(&mut self, writer: &mut TermWriter) -> ShResult<()> {
|
||||
self.selector.clear(writer)
|
||||
}
|
||||
fn draw(&mut self, writer: &mut TermWriter) -> ShResult<()> {
|
||||
fn draw(&mut self, writer: &mut TermWriter) -> ShResult<usize> {
|
||||
self.selector.draw(writer)
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
@@ -1497,8 +1497,8 @@ impl Completer for SimpleCompleter {
|
||||
self.token_span
|
||||
}
|
||||
|
||||
fn draw(&mut self, _writer: &mut TermWriter) -> ShResult<()> {
|
||||
Ok(())
|
||||
fn draw(&mut self, _writer: &mut TermWriter) -> ShResult<usize> {
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
fn original_input(&self) -> &str {
|
||||
|
||||
@@ -138,6 +138,13 @@ pub fn split_lines_at(lines: &mut Vec<Line>, pos: Pos) -> Vec<Line> {
|
||||
rest
|
||||
}
|
||||
|
||||
pub fn split_lines(mut lines: Vec<Line>, pos: Pos) -> (Vec<Line>,Vec<Line>) {
|
||||
let tail = lines[pos.row].split_off(pos.col);
|
||||
let mut rest: Vec<Line> = lines.drain(pos.row + 1..).collect();
|
||||
rest.insert(0, tail);
|
||||
(lines, rest)
|
||||
}
|
||||
|
||||
pub fn attach_lines(lines: &mut Vec<Line>, other: &mut Vec<Line>) {
|
||||
if other.is_empty() {
|
||||
return;
|
||||
@@ -1860,6 +1867,16 @@ impl LineBuf {
|
||||
let (s, e) = ordered(*s, *e);
|
||||
Some(MotionKind::Block { start: s, end: e })
|
||||
}
|
||||
dir @ (Motion::HalfScreenUp | Motion::HalfScreenDown) => {
|
||||
let off = match dir {
|
||||
Motion::HalfScreenUp => -(self.get_viewport_height() as isize / 2),
|
||||
Motion::HalfScreenDown => self.get_viewport_height() as isize / 2,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let row = self.row();
|
||||
let target_row = self.offset_row(off);
|
||||
Some(MotionKind::Line { start: target_row, end: row, inclusive: false })
|
||||
}
|
||||
Motion::RepeatMotion |
|
||||
Motion::RepeatMotionRev => unreachable!("Repeat motions should have been resolved in readline/mod.rs"),
|
||||
Motion::Global(val) |
|
||||
@@ -1907,7 +1924,6 @@ impl LineBuf {
|
||||
Ok(())
|
||||
}
|
||||
fn extract_range(&mut self, motion: &MotionKind) -> Vec<Line> {
|
||||
log::debug!("Extracting range for motion: {:?}", motion);
|
||||
let extracted = match motion {
|
||||
MotionKind::Char {
|
||||
start,
|
||||
@@ -2484,9 +2500,31 @@ impl LineBuf {
|
||||
}
|
||||
}
|
||||
|
||||
Verb::EndOfFile => {
|
||||
self.lines.clear();
|
||||
}
|
||||
|
||||
Verb::PrintPosition => {
|
||||
let num_lines = self.lines.len();
|
||||
let row = self.row() + 1;
|
||||
let col = self.col() + 1;
|
||||
let total_graphemes = self.count_graphemes();
|
||||
let (left,_) = split_lines(self.lines.clone(), self.cursor.pos);
|
||||
let total_in_left = left.iter().map(|l| l.len()).sum::<usize>();
|
||||
let percentage = if total_graphemes > 0 {
|
||||
(total_in_left as f64 / total_graphemes as f64) * 100.0
|
||||
} else {
|
||||
100.0
|
||||
}.round() as usize;
|
||||
|
||||
let msg = format!("line: {row}/{num_lines}, col: {col} --{percentage}%--");
|
||||
write_meta(|m| {
|
||||
m.post_status_message(msg);
|
||||
})
|
||||
}
|
||||
|
||||
Verb::Complete
|
||||
| Verb::ExMode
|
||||
| Verb::EndOfFile
|
||||
| Verb::InsertMode
|
||||
| Verb::NormalMode
|
||||
| Verb::VisualMode
|
||||
@@ -2600,7 +2638,13 @@ impl LineBuf {
|
||||
}
|
||||
|
||||
pub fn fix_cursor(&mut self) {
|
||||
// we are now going to enforce some invariants and do some bookkeeping
|
||||
if self.lines.is_empty() {
|
||||
// self.lines must always have at least one line
|
||||
self.lines.push(Line::default());
|
||||
}
|
||||
if self.cursor.pos.row >= self.lines.len() {
|
||||
// clamp this now so self.cur_line() cannot panic
|
||||
self.cursor.pos.row = self.lines.len().saturating_sub(1);
|
||||
}
|
||||
if self.cursor.exclusive {
|
||||
@@ -2616,6 +2660,8 @@ impl LineBuf {
|
||||
self.cursor.pos.col = line.len();
|
||||
}
|
||||
}
|
||||
|
||||
// update viewport scroll offset
|
||||
self.update_scroll_offset();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use history::History;
|
||||
use keys::{KeyCode, KeyEvent, ModKeys};
|
||||
use linebuf::{LineBuf, SelectMode};
|
||||
use std::collections::VecDeque;
|
||||
use std::fmt::Write;
|
||||
use term::{KeyReader, Layout, LineWriter, PollReader, TermWriter, get_win_size};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
@@ -16,8 +17,7 @@ use crate::readline::complete::{FuzzyCompleter, SelectorResponse};
|
||||
use crate::readline::term::{Pos, TermReader, calc_str_width};
|
||||
use crate::readline::vimode::{ViEx, ViVerbatim};
|
||||
use crate::state::{
|
||||
AutoCmdKind, ShellParam, Var, VarFlags, VarKind, read_logic, read_shopts, with_vars, write_meta,
|
||||
write_vars,
|
||||
AutoCmdKind, ShellParam, Var, VarFlags, VarKind, read_logic, read_meta, read_shopts, with_vars, write_meta, write_vars
|
||||
};
|
||||
use crate::{
|
||||
libsh::error::ShResult,
|
||||
@@ -272,6 +272,8 @@ pub struct ShedVi {
|
||||
pub ex_history: History,
|
||||
|
||||
pub needs_redraw: bool,
|
||||
pub ctrl_d_warning_counter: usize,
|
||||
pub status_msgs: VecDeque<(String, Instant)>
|
||||
}
|
||||
|
||||
impl ShedVi {
|
||||
@@ -293,6 +295,8 @@ impl ShedVi {
|
||||
history: History::new()?,
|
||||
ex_history: History::empty(),
|
||||
needs_redraw: true,
|
||||
ctrl_d_warning_counter: 0,
|
||||
status_msgs: VecDeque::new()
|
||||
};
|
||||
write_vars(|v| {
|
||||
v.set_var(
|
||||
@@ -325,6 +329,8 @@ impl ShedVi {
|
||||
history: History::empty(),
|
||||
ex_history: History::empty(),
|
||||
needs_redraw: true,
|
||||
ctrl_d_warning_counter: 0,
|
||||
status_msgs: VecDeque::new()
|
||||
};
|
||||
write_vars(|v| {
|
||||
v.set_var(
|
||||
@@ -883,17 +889,10 @@ impl ShedVi {
|
||||
return Ok(Some(ReadlineEvent::Line(buf)));
|
||||
}
|
||||
|
||||
if cmd.verb().is_some_and(|v| v.1 == Verb::EndOfFile) {
|
||||
if self.focused_editor().joined().is_empty() {
|
||||
return Ok(Some(ReadlineEvent::Eof));
|
||||
} else {
|
||||
*self.focused_editor() = LineBuf::new();
|
||||
self.mode = Box::new(ViInsert::new());
|
||||
self.needs_redraw = true;
|
||||
return Ok(None);
|
||||
}
|
||||
} else if cmd.verb().is_some_and(|v| v.1 == Verb::Quit) {
|
||||
return Ok(Some(ReadlineEvent::Eof));
|
||||
if (cmd.verb().is_some_and(|v| v.1 == Verb::EndOfFile)
|
||||
&& self.focused_editor().joined().is_empty())
|
||||
|| cmd.verb().is_some_and(|v| v.1 == Verb::Quit) {
|
||||
return Ok(Some(ReadlineEvent::Eof));
|
||||
}
|
||||
|
||||
// check if it's an edit
|
||||
@@ -902,6 +901,8 @@ impl ShedVi {
|
||||
// this is only used for ringing the bell
|
||||
let has_edit_verb = cmd.verb().is_some_and(|v| v.1.is_edit() && v.1 != Verb::Change);
|
||||
|
||||
let is_ctrl_d_motion = cmd.motion().is_some_and(|m| m.1 == Motion::HalfScreenDown);
|
||||
|
||||
let is_shell_cmd = cmd.verb().is_some_and(|v| matches!(v.1, Verb::ShellCmd(_)));
|
||||
let is_ex_cmd = cmd.flags.contains(CmdFlags::IS_EX_CMD);
|
||||
if is_shell_cmd {
|
||||
@@ -913,6 +914,7 @@ impl ShedVi {
|
||||
}
|
||||
|
||||
let before = self.editor.joined();
|
||||
let before_cursor = self.editor.cursor;
|
||||
|
||||
self.exec_cmd(cmd, false)?;
|
||||
|
||||
@@ -922,6 +924,7 @@ impl ShedVi {
|
||||
}
|
||||
}
|
||||
let after = self.editor.joined();
|
||||
let after_cursor = self.editor.cursor;
|
||||
|
||||
if before != after {
|
||||
self
|
||||
@@ -929,7 +932,18 @@ impl ShedVi {
|
||||
.update_pending_cmd((&self.editor.joined(), self.editor.cursor_to_flat()));
|
||||
} else if before == after && has_edit_verb {
|
||||
self.writer.send_bell().ok();
|
||||
}
|
||||
} else if before_cursor == after_cursor && is_ctrl_d_motion {
|
||||
if self.ctrl_d_warning_counter == 3 || self.editor.is_empty() {
|
||||
// our silly user is spamming ctrl+d for some reason
|
||||
// maybe they want to exit the shell?
|
||||
write_meta(|m| {
|
||||
m.post_status_message("Ctrl+D only quits in insert mode. try ':q' or entering insert mode with 'i'".into())
|
||||
});
|
||||
self.ctrl_d_warning_counter = 0;
|
||||
} else {
|
||||
self.ctrl_d_warning_counter += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let hint = self.history.get_hint();
|
||||
|
||||
@@ -1126,7 +1140,9 @@ impl ShedVi {
|
||||
self.writer.flush_write(&buf)?;
|
||||
|
||||
// Move to end of layout for overlay draws (completer, history search)
|
||||
let has_overlays = self.completer.is_active() || self.focused_history().fuzzy_finder.is_active();
|
||||
let has_overlays = self.completer.is_active()
|
||||
|| self.focused_history().fuzzy_finder.is_active();
|
||||
|
||||
let down = new_layout.end.row.saturating_sub(new_layout.cursor.row);
|
||||
if has_overlays && down > 0 {
|
||||
self.writer.flush_write(&format!("\x1b[{down}B"))?;
|
||||
@@ -1141,9 +1157,11 @@ impl ShedVi {
|
||||
// Without PSR, use the content width on the cursor's row
|
||||
(new_layout.end.col + 1).max(new_layout.cursor.col + 1)
|
||||
};
|
||||
|
||||
let mut fuzzy_window_rows = 0;
|
||||
self.completer
|
||||
.set_prompt_line_context(preceding_width, new_layout.end.col);
|
||||
self.completer.draw(&mut self.writer)?;
|
||||
fuzzy_window_rows += self.completer.draw(&mut self.writer)?;
|
||||
|
||||
{
|
||||
self.focused_history()
|
||||
@@ -1151,10 +1169,37 @@ impl ShedVi {
|
||||
.set_prompt_line_context(preceding_width, new_layout.end.col);
|
||||
|
||||
let mut writer = std::mem::take(&mut self.writer);
|
||||
self.focused_history().fuzzy_finder.draw(&mut writer)?;
|
||||
fuzzy_window_rows += self.focused_history().fuzzy_finder.draw(&mut writer)?;
|
||||
self.writer = writer;
|
||||
}
|
||||
|
||||
while let Some(msg) = write_meta(|m| m.pop_status_message()) {
|
||||
let now = Instant::now();
|
||||
self.status_msgs.push_back((msg,now));
|
||||
}
|
||||
|
||||
while let Some((msg,time)) = self.status_msgs.front() {
|
||||
if time.elapsed().as_secs() < 5 {
|
||||
log::debug!("drawing status message: {msg}");
|
||||
let down = new_layout.end.row - new_layout.cursor.row;
|
||||
log::debug!("status message down: {down}");
|
||||
let fuzzy_rows = fuzzy_window_rows.saturating_sub(1); // the cursor is one row below the top
|
||||
let total = down.saturating_add(fuzzy_rows as u16);
|
||||
let move_down = if total > 0 {
|
||||
format!("\x1b[{total}B")
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let move_up = total + 2;
|
||||
let col = new_layout.cursor.col + 1;
|
||||
self.writer.flush_write(&format!("{move_down}\n\n\x1b7\x1b[2K{msg}\x1b8\x1b[{move_up}A\x1b[{col}G"))?;
|
||||
new_layout.end.row += (2 + msg.chars().filter(|c| *c == '\n').count()) as u16;
|
||||
break
|
||||
} else {
|
||||
self.status_msgs.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
self.old_layout = Some(new_layout);
|
||||
self.needs_redraw = false;
|
||||
|
||||
|
||||
@@ -243,6 +243,7 @@ pub enum Verb {
|
||||
Equalize,
|
||||
AcceptLineOrNewline,
|
||||
EndOfFile,
|
||||
PrintPosition,
|
||||
// Ex-mode verbs
|
||||
ExMode,
|
||||
ShellCmd(String),
|
||||
@@ -335,6 +336,8 @@ pub enum Motion {
|
||||
EndOfBuffer,
|
||||
ToColumn,
|
||||
ToDelimMatch,
|
||||
HalfScreenDown,
|
||||
HalfScreenUp,
|
||||
ToBrace(Direction),
|
||||
ToBracket(Direction),
|
||||
ToParen(Direction),
|
||||
|
||||
@@ -120,7 +120,7 @@ impl ViMode for ViEx {
|
||||
Ok(cmd) => Ok(cmd),
|
||||
Err(e) => {
|
||||
let msg = e.unwrap_or(format!("Not an editor command: {}", &input));
|
||||
write_meta(|m| m.post_system_message(msg.clone()));
|
||||
write_meta(|m| m.post_status_message(msg.clone()));
|
||||
Err(ShErr::simple(ShErrKind::ParseErr, msg))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +116,7 @@ pub fn common_cmds(key: E) -> Option<ViCmd> {
|
||||
E(K::Right, M::NONE) => pending_cmd.set_motion(MotionCmd(1, Motion::ForwardChar)),
|
||||
E(K::Up, M::NONE) => pending_cmd.set_motion(MotionCmd(1, Motion::LineUp)),
|
||||
E(K::Down, M::NONE) => pending_cmd.set_motion(MotionCmd(1, Motion::LineDown)),
|
||||
E(K::Enter, M::SHIFT) => pending_cmd.set_verb(VerbCmd(1, Verb::InsertChar('\n'))),
|
||||
E(K::Enter, M::NONE) => pending_cmd.set_verb(VerbCmd(1, Verb::AcceptLineOrNewline)),
|
||||
E(K::Char('D'), M::CTRL) => pending_cmd.set_verb(VerbCmd(1, Verb::EndOfFile)),
|
||||
E(K::Delete, M::NONE) => {
|
||||
|
||||
@@ -776,6 +776,36 @@ impl ViMode for ViNormal {
|
||||
flags: self.flags(),
|
||||
})
|
||||
}
|
||||
E(K::Char('G'), M::CTRL) => {
|
||||
self.pending_seq.clear();
|
||||
Some(ViCmd {
|
||||
register: Default::default(),
|
||||
verb: Some(VerbCmd(1, Verb::PrintPosition)),
|
||||
motion: None,
|
||||
raw_seq: "".into(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
}
|
||||
E(K::Char('D'), M::CTRL) => {
|
||||
self.pending_seq.clear();
|
||||
Some(ViCmd {
|
||||
register: Default::default(),
|
||||
verb: None,
|
||||
motion: Some(MotionCmd(1, Motion::HalfScreenDown)),
|
||||
raw_seq: "".into(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
}
|
||||
E(K::Char('U'), M::CTRL) => {
|
||||
self.pending_seq.clear();
|
||||
Some(ViCmd {
|
||||
register: Default::default(),
|
||||
verb: None,
|
||||
motion: Some(MotionCmd(1, Motion::HalfScreenUp)),
|
||||
raw_seq: "".into(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
}
|
||||
|
||||
E(K::Char(ch), M::NONE) => self.try_parse(ch),
|
||||
E(K::Backspace, M::NONE) => Some(ViCmd {
|
||||
|
||||
@@ -663,6 +663,36 @@ impl ViMode for ViVisual {
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
}
|
||||
E(K::Char('G'), M::CTRL) => {
|
||||
self.pending_seq.clear();
|
||||
Some(ViCmd {
|
||||
register: Default::default(),
|
||||
verb: Some(VerbCmd(1, Verb::PrintPosition)),
|
||||
motion: None,
|
||||
raw_seq: "".into(),
|
||||
flags: CmdFlags::empty()
|
||||
})
|
||||
}
|
||||
E(K::Char('D'), M::CTRL) => {
|
||||
self.pending_seq.clear();
|
||||
Some(ViCmd {
|
||||
register: Default::default(),
|
||||
verb: None,
|
||||
motion: Some(MotionCmd(1, Motion::HalfScreenDown)),
|
||||
raw_seq: "".into(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
}
|
||||
E(K::Char('U'), M::CTRL) => {
|
||||
self.pending_seq.clear();
|
||||
Some(ViCmd {
|
||||
register: Default::default(),
|
||||
verb: None,
|
||||
motion: Some(MotionCmd(1, Motion::HalfScreenUp)),
|
||||
raw_seq: "".into(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
}
|
||||
E(K::Char('R'), M::CTRL) => {
|
||||
let mut chars = self.pending_seq.chars().peekable();
|
||||
let count = self.parse_count(&mut chars).unwrap_or(1);
|
||||
|
||||
23
src/state.rs
23
src/state.rs
@@ -1369,7 +1369,12 @@ pub struct MetaTab {
|
||||
runtime_stop: Option<Instant>,
|
||||
|
||||
// pending system messages
|
||||
system_msg: Vec<String>,
|
||||
// are drawn above the prompt and survive redraws
|
||||
system_msg: VecDeque<String>,
|
||||
|
||||
// same as system messages,
|
||||
// but they appear under the prompt and are erased on redraw
|
||||
status_msg: VecDeque<String>,
|
||||
|
||||
// pushd/popd stack
|
||||
dir_stack: VecDeque<PathBuf>,
|
||||
@@ -1394,7 +1399,8 @@ impl Default for MetaTab {
|
||||
shell_time: Instant::now(),
|
||||
runtime_start: None,
|
||||
runtime_stop: None,
|
||||
system_msg: vec![],
|
||||
system_msg: VecDeque::new(),
|
||||
status_msg: VecDeque::new(),
|
||||
dir_stack: VecDeque::new(),
|
||||
getopts_offset: 0,
|
||||
old_path: None,
|
||||
@@ -1614,14 +1620,23 @@ impl MetaTab {
|
||||
}
|
||||
}
|
||||
pub fn post_system_message(&mut self, message: String) {
|
||||
self.system_msg.push(message);
|
||||
self.system_msg.push_back(message);
|
||||
}
|
||||
pub fn pop_system_message(&mut self) -> Option<String> {
|
||||
self.system_msg.pop()
|
||||
self.system_msg.pop_front()
|
||||
}
|
||||
pub fn system_msg_pending(&self) -> bool {
|
||||
!self.system_msg.is_empty()
|
||||
}
|
||||
pub fn post_status_message(&mut self, message: String) {
|
||||
self.status_msg.push_back(message);
|
||||
}
|
||||
pub fn pop_status_message(&mut self) -> Option<String> {
|
||||
self.status_msg.pop_front()
|
||||
}
|
||||
pub fn status_msg_pending(&self) -> bool {
|
||||
!self.status_msg.is_empty()
|
||||
}
|
||||
pub fn dir_stack_top(&self) -> Option<&PathBuf> {
|
||||
self.dir_stack.front()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user