implemented Ctrl+R command history searching and command history expansion with '\!'
This commit is contained in:
@@ -81,6 +81,12 @@ in
|
||||
"pre-mode-change"
|
||||
"post-mode-change"
|
||||
"on-exit"
|
||||
"on-history-open"
|
||||
"on-history-close"
|
||||
"on-history-select"
|
||||
"on-completion-start"
|
||||
"on-completion-cancel"
|
||||
"on-completion-select"
|
||||
])) (list: list != []);
|
||||
description = "The events that trigger this autocmd";
|
||||
};
|
||||
|
||||
@@ -111,11 +111,6 @@ impl KeyMap {
|
||||
expand_keymap(&self.action)
|
||||
}
|
||||
pub fn compare(&self, other: &[KeyEvent]) -> KeyMapMatch {
|
||||
log::debug!(
|
||||
"Comparing keymap keys {:?} with input {:?}",
|
||||
self.keys_expanded(),
|
||||
other
|
||||
);
|
||||
let ours = self.keys_expanded();
|
||||
if other == ours {
|
||||
KeyMapMatch::IsExact
|
||||
|
||||
@@ -1076,7 +1076,6 @@ pub fn unescape_str(raw: &str) -> String {
|
||||
}
|
||||
}
|
||||
'$' => {
|
||||
log::debug!("Found ANSI-C quoting");
|
||||
chars.next();
|
||||
while let Some(q_ch) = chars.next() {
|
||||
match q_ch {
|
||||
@@ -1232,7 +1231,6 @@ pub fn unescape_str(raw: &str) -> String {
|
||||
}
|
||||
}
|
||||
'$' if chars.peek() == Some(&'\'') => {
|
||||
log::debug!("Found ANSI-C quoting");
|
||||
chars.next();
|
||||
result.push(markers::SNG_QUOTE);
|
||||
while let Some(q_ch) = chars.next() {
|
||||
@@ -1406,7 +1404,6 @@ impl FromStr for ParamExp {
|
||||
))
|
||||
};
|
||||
|
||||
log::debug!("Parsing parameter expansion: '{:?}'", s);
|
||||
|
||||
// Handle indirect var expansion: ${!var}
|
||||
if let Some(var) = s.strip_prefix('!') {
|
||||
@@ -1423,7 +1420,6 @@ impl FromStr for ParamExp {
|
||||
return Ok(RemShortestPrefix(rest.to_string()));
|
||||
}
|
||||
if let Some(rest) = s.strip_prefix("%%") {
|
||||
log::debug!("Matched longest suffix pattern: '{}'", rest);
|
||||
return Ok(RemLongestSuffix(rest.to_string()));
|
||||
} else if let Some(rest) = s.strip_prefix('%') {
|
||||
return Ok(RemShortestSuffix(rest.to_string()));
|
||||
@@ -2363,7 +2359,7 @@ pub fn parse_key_alias(alias: &str) -> Option<KeyEvent> {
|
||||
}
|
||||
}
|
||||
|
||||
let key = match *key_name.first()? {
|
||||
let key = match key_name.first()?.to_uppercase().as_str() {
|
||||
"CR" => KeyCode::Char('\r'),
|
||||
"ENTER" | "RETURN" => KeyCode::Enter,
|
||||
"ESC" | "ESCAPE" => KeyCode::Esc,
|
||||
@@ -2378,6 +2374,7 @@ pub fn parse_key_alias(alias: &str) -> Option<KeyEvent> {
|
||||
"RIGHT" => KeyCode::Right,
|
||||
"HOME" => KeyCode::Home,
|
||||
"END" => KeyCode::End,
|
||||
"CMD" => KeyCode::ExMode,
|
||||
"PGUP" | "PAGEUP" => KeyCode::PageUp,
|
||||
"PGDN" | "PAGEDOWN" => KeyCode::PageDown,
|
||||
k if k.len() == 1 => KeyCode::Char(k.chars().next().unwrap()),
|
||||
|
||||
@@ -528,6 +528,12 @@ pub enum CompResponse {
|
||||
Consumed, // key was handled, but completion remains active
|
||||
}
|
||||
|
||||
pub enum SelectorResponse {
|
||||
Accept(String),
|
||||
Dismiss,
|
||||
Consumed,
|
||||
}
|
||||
|
||||
pub trait Completer {
|
||||
fn complete(
|
||||
&mut self,
|
||||
@@ -702,23 +708,28 @@ impl QueryEditor {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FuzzyCompleter {
|
||||
completer: SimpleCompleter,
|
||||
pub struct FuzzySelector {
|
||||
query: QueryEditor,
|
||||
filtered: Vec<ScoredCandidate>,
|
||||
candidates: Vec<String>,
|
||||
cursor: ClampedUsize,
|
||||
number_candidates: bool,
|
||||
old_layout: Option<FuzzyLayout>,
|
||||
max_height: usize,
|
||||
scroll_offset: usize,
|
||||
active: bool,
|
||||
/// Context from the prompt: width of the line above the fuzzy window
|
||||
prompt_line_width: u16,
|
||||
/// Context from the prompt: cursor column on the line above the fuzzy window
|
||||
prompt_cursor_col: u16,
|
||||
title: String,
|
||||
}
|
||||
|
||||
impl FuzzyCompleter {
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FuzzyCompleter {
|
||||
completer: SimpleCompleter,
|
||||
pub selector: FuzzySelector,
|
||||
}
|
||||
|
||||
impl FuzzySelector {
|
||||
const BOT_LEFT: &str = "\x1b[90m╰\x1b[0m";
|
||||
const BOT_RIGHT: &str = "\x1b[90m╯\x1b[0m";
|
||||
const TOP_LEFT: &str = "\x1b[90m╭\x1b[0m";
|
||||
@@ -730,29 +741,122 @@ impl FuzzyCompleter {
|
||||
const PROMPT_ARROW: &str = "\x1b[1;36m>\x1b[0m";
|
||||
const TREE_LEFT: &str = "\x1b[90m├\x1b[0m";
|
||||
const TREE_RIGHT: &str = "\x1b[90m┤\x1b[0m";
|
||||
//const TREE_BOT: &str = "\x1b[90m┴\x1b[0m";
|
||||
//const TREE_TOP: &str = "\x1b[90m┬\x1b[0m";
|
||||
//const CROSS: &str = "\x1b[90m┼\x1b[0m";
|
||||
|
||||
pub fn new(title: impl Into<String>) -> Self {
|
||||
Self {
|
||||
max_height: 8,
|
||||
query: QueryEditor::default(),
|
||||
filtered: vec![],
|
||||
candidates: vec![],
|
||||
cursor: ClampedUsize::new(0, 0, true),
|
||||
number_candidates: false,
|
||||
old_layout: None,
|
||||
scroll_offset: 0,
|
||||
active: false,
|
||||
prompt_line_width: 0,
|
||||
prompt_cursor_col: 0,
|
||||
title: title.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn number_candidates(self, enable: bool) -> Self {
|
||||
Self {
|
||||
number_candidates: enable,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn activate(&mut self, candidates: Vec<String>) {
|
||||
self.active = true;
|
||||
self.candidates = candidates;
|
||||
self.score_candidates();
|
||||
}
|
||||
|
||||
pub fn set_query(&mut self, query: String) {
|
||||
self.query.linebuf = LineBuf::new().with_initial(&query, query.len());
|
||||
self.query.update_scroll_offset();
|
||||
self.score_candidates();
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.query.clear();
|
||||
self.filtered.clear();
|
||||
self.candidates.clear();
|
||||
self.cursor = ClampedUsize::new(0, 0, true);
|
||||
self.old_layout = None;
|
||||
self.scroll_offset = 0;
|
||||
self.active = false;
|
||||
}
|
||||
|
||||
pub fn reset_stay_active(&mut self) {
|
||||
if self.active {
|
||||
self.query.clear();
|
||||
self.score_candidates();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.active
|
||||
}
|
||||
|
||||
pub fn selected_candidate(&self) -> Option<String> {
|
||||
self
|
||||
.filtered
|
||||
.get(self.cursor.get())
|
||||
.map(|c| c.content.clone())
|
||||
}
|
||||
|
||||
pub fn set_prompt_line_context(&mut self, line_width: u16, cursor_col: u16) {
|
||||
self.prompt_line_width = line_width;
|
||||
self.prompt_cursor_col = cursor_col;
|
||||
}
|
||||
|
||||
fn candidate_height(&self, idx: usize) -> usize {
|
||||
self.filtered.get(idx)
|
||||
.map(|c| c.content.trim_end().lines().count().max(1))
|
||||
.unwrap_or(1)
|
||||
}
|
||||
|
||||
fn get_window(&mut self) -> &[ScoredCandidate] {
|
||||
let height = self.filtered.len().min(self.max_height);
|
||||
|
||||
self.update_scroll_offset();
|
||||
|
||||
&self.filtered[self.scroll_offset..self.scroll_offset + height]
|
||||
let mut lines = 0;
|
||||
let mut end = self.scroll_offset;
|
||||
while end < self.filtered.len() {
|
||||
if lines >= self.max_height {
|
||||
break;
|
||||
}
|
||||
lines += self.candidate_height(end);
|
||||
end += 1;
|
||||
}
|
||||
|
||||
&self.filtered[self.scroll_offset..end]
|
||||
}
|
||||
|
||||
pub fn update_scroll_offset(&mut self) {
|
||||
let height = self.filtered.len().min(self.max_height);
|
||||
if self.cursor.get() < self.scroll_offset + 1 {
|
||||
self.scroll_offset = self.cursor.ret_sub(1);
|
||||
let cursor = self.cursor.get();
|
||||
|
||||
// Scroll up: cursor above window
|
||||
if cursor < self.scroll_offset {
|
||||
self.scroll_offset = cursor;
|
||||
return;
|
||||
}
|
||||
if self.cursor.get() >= self.scroll_offset + height.saturating_sub(1) {
|
||||
self.scroll_offset = self.cursor.ret_sub(height.saturating_sub(2));
|
||||
|
||||
// Scroll down: ensure all candidates from scroll_offset through cursor
|
||||
// fit within max_height rendered lines
|
||||
loop {
|
||||
let mut lines = 0;
|
||||
let last = cursor.min(self.filtered.len().saturating_sub(1));
|
||||
for idx in self.scroll_offset..=last {
|
||||
lines += self.candidate_height(idx);
|
||||
}
|
||||
self.scroll_offset = self
|
||||
.scroll_offset
|
||||
.min(self.filtered.len().saturating_sub(height));
|
||||
if lines <= self.max_height || self.scroll_offset >= cursor {
|
||||
break;
|
||||
}
|
||||
self.scroll_offset += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn score_candidates(&mut self) {
|
||||
let mut scored: Vec<_> = self
|
||||
.candidates
|
||||
@@ -769,83 +873,13 @@ impl FuzzyCompleter {
|
||||
self.cursor.set_max(scored.len());
|
||||
self.filtered = scored;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FuzzyCompleter {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_height: 8,
|
||||
completer: SimpleCompleter::default(),
|
||||
query: QueryEditor::default(),
|
||||
filtered: vec![],
|
||||
candidates: vec![],
|
||||
cursor: ClampedUsize::new(0, 0, true),
|
||||
old_layout: None,
|
||||
scroll_offset: 0,
|
||||
active: false,
|
||||
prompt_line_width: 0,
|
||||
prompt_cursor_col: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Completer for FuzzyCompleter {
|
||||
fn set_prompt_line_context(&mut self, line_width: u16, cursor_col: u16) {
|
||||
self.prompt_line_width = line_width;
|
||||
self.prompt_cursor_col = cursor_col;
|
||||
}
|
||||
fn reset_stay_active(&mut self) {
|
||||
if self.is_active() {
|
||||
self.query.clear();
|
||||
self.score_candidates();
|
||||
}
|
||||
}
|
||||
fn get_completed_line(&self, _candidate: &str) -> String {
|
||||
log::debug!("Getting completed line for candidate: {}", _candidate);
|
||||
|
||||
let selected = &self.filtered[self.cursor.get()].content;
|
||||
log::debug!("Selected candidate: {}", selected);
|
||||
let (start, end) = self.completer.token_span;
|
||||
log::debug!("Token span: ({}, {})", start, end);
|
||||
let ret = format!(
|
||||
"{}{}{}",
|
||||
&self.completer.original_input[..start],
|
||||
selected,
|
||||
&self.completer.original_input[end..]
|
||||
);
|
||||
log::debug!("Completed line: {}", ret);
|
||||
ret
|
||||
}
|
||||
fn complete(
|
||||
&mut self,
|
||||
line: String,
|
||||
cursor_pos: usize,
|
||||
direction: i32,
|
||||
) -> ShResult<Option<String>> {
|
||||
self.completer.complete(line, cursor_pos, direction)?;
|
||||
let candidates: Vec<_> = self.completer.candidates.clone();
|
||||
if candidates.is_empty() {
|
||||
self.completer.reset();
|
||||
self.active = false;
|
||||
return Ok(None);
|
||||
} else if candidates.len() == 1 {
|
||||
self.filtered = candidates.into_iter().map(ScoredCandidate::from).collect();
|
||||
let completed = self.get_completed_line(&self.filtered[0].content);
|
||||
self.active = false;
|
||||
return Ok(Some(completed));
|
||||
}
|
||||
self.active = true;
|
||||
self.candidates = candidates;
|
||||
self.score_candidates();
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn handle_key(&mut self, key: K) -> ShResult<CompResponse> {
|
||||
pub fn handle_key(&mut self, key: K) -> ShResult<SelectorResponse> {
|
||||
match key {
|
||||
K(C::Char('D'), M::CTRL) | K(C::Esc, M::NONE) => {
|
||||
self.active = false;
|
||||
self.filtered.clear();
|
||||
Ok(CompResponse::Dismiss)
|
||||
Ok(SelectorResponse::Dismiss)
|
||||
}
|
||||
K(C::Enter, M::NONE) => {
|
||||
self.active = false;
|
||||
@@ -854,76 +888,30 @@ impl Completer for FuzzyCompleter {
|
||||
.get(self.cursor.get())
|
||||
.map(|c| c.content.clone())
|
||||
{
|
||||
Ok(CompResponse::Accept(selected))
|
||||
Ok(SelectorResponse::Accept(selected))
|
||||
} else {
|
||||
Ok(CompResponse::Dismiss)
|
||||
Ok(SelectorResponse::Dismiss)
|
||||
}
|
||||
}
|
||||
K(C::Tab, M::SHIFT) | K(C::Up, M::NONE) => {
|
||||
self.cursor.wrap_sub(1);
|
||||
self.update_scroll_offset();
|
||||
Ok(CompResponse::Consumed)
|
||||
Ok(SelectorResponse::Consumed)
|
||||
}
|
||||
K(C::Tab, M::NONE) | K(C::Down, M::NONE) => {
|
||||
self.cursor.wrap_add(1);
|
||||
self.update_scroll_offset();
|
||||
Ok(CompResponse::Consumed)
|
||||
Ok(SelectorResponse::Consumed)
|
||||
}
|
||||
_ => {
|
||||
self.query.handle_key(key)?;
|
||||
self.score_candidates();
|
||||
Ok(CompResponse::Consumed)
|
||||
Ok(SelectorResponse::Consumed)
|
||||
}
|
||||
}
|
||||
}
|
||||
fn clear(&mut self, writer: &mut TermWriter) -> ShResult<()> {
|
||||
if let Some(layout) = self.old_layout.take() {
|
||||
let (new_cols, _) = get_win_size(*TTY_FILENO);
|
||||
// The fuzzy window is one continuous auto-wrapped block (no hard
|
||||
// newlines between rows). After a resize the terminal re-joins
|
||||
// soft wraps and re-wraps as a flat buffer.
|
||||
let total_cells = layout.rows as u32 * layout.cols as u32;
|
||||
let physical_rows = if new_cols > 0 {
|
||||
total_cells.div_ceil(new_cols as u32) as u16
|
||||
} else {
|
||||
layout.rows
|
||||
};
|
||||
let cursor_offset = layout.cols as u32 + layout.cursor_col as u32;
|
||||
let cursor_phys_row = if new_cols > 0 {
|
||||
(cursor_offset / new_cols as u32) as u16
|
||||
} else {
|
||||
1
|
||||
};
|
||||
let lines_below = physical_rows.saturating_sub(cursor_phys_row + 1);
|
||||
|
||||
// The prompt line above the \n may have wrapped (e.g. due to PSR
|
||||
// filling to t_cols). Compute how many extra rows that adds
|
||||
// between the prompt cursor and the fuzzy content.
|
||||
let gap_extra = if new_cols > 0 && layout.preceding_line_width > new_cols {
|
||||
let wrap_rows = (layout.preceding_line_width as u32).div_ceil(new_cols as u32) as u16;
|
||||
let cursor_wrap_row = layout.preceding_cursor_col / new_cols;
|
||||
wrap_rows.saturating_sub(cursor_wrap_row + 1)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let mut buf = String::new();
|
||||
if lines_below > 0 {
|
||||
write!(buf, "\x1b[{}B", lines_below).unwrap();
|
||||
}
|
||||
for _ in 0..physical_rows {
|
||||
buf.push_str("\x1b[2K\x1b[A");
|
||||
}
|
||||
buf.push_str("\x1b[2K");
|
||||
// Clear extra rows from prompt line wrapping (PSR)
|
||||
for _ in 0..gap_extra {
|
||||
buf.push_str("\x1b[A\x1b[2K");
|
||||
}
|
||||
writer.flush_write(&buf)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn draw(&mut self, writer: &mut TermWriter) -> ShResult<()> {
|
||||
pub fn draw(&mut self, writer: &mut TermWriter) -> ShResult<()> {
|
||||
if !self.active {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -939,13 +927,19 @@ impl Completer for FuzzyCompleter {
|
||||
let query = self.query.get_window();
|
||||
let num_filtered = format!("\x1b[33m{}\x1b[0m", self.filtered.len());
|
||||
let num_candidates = format!("\x1b[33m{}\x1b[0m", self.candidates.len());
|
||||
let title = self.title.clone();
|
||||
let title_width = title.len() as u16;
|
||||
let number_candidates = self.number_candidates;
|
||||
let min_pad = self.candidates.len().to_string().len().saturating_add(1).max(6);
|
||||
let max_height = self.max_height;
|
||||
let visible = self.get_window();
|
||||
let mut rows: u16 = 0;
|
||||
let top_bar = format!(
|
||||
"\n{}{} \x1b[1mComplete\x1b[0m {}{}",
|
||||
"\n{}{} \x1b[1m{}\x1b[0m {}{}",
|
||||
Self::TOP_LEFT,
|
||||
Self::HOR_LINE,
|
||||
Self::HOR_LINE.repeat(cols.saturating_sub(13) as usize),
|
||||
title,
|
||||
Self::HOR_LINE.repeat(cols.saturating_sub(title_width + 5) as usize),
|
||||
Self::TOP_RIGHT
|
||||
);
|
||||
buf.push_str(&top_bar);
|
||||
@@ -972,24 +966,51 @@ impl Completer for FuzzyCompleter {
|
||||
buf.push_str(&sep_line_final);
|
||||
rows += 1;
|
||||
|
||||
let mut lines_drawn = 0;
|
||||
for (i, candidate) in visible.iter().enumerate() {
|
||||
if lines_drawn >= max_height {
|
||||
break;
|
||||
}
|
||||
let selector = if i + offset == cursor_pos {
|
||||
Self::SELECTOR_HL
|
||||
} else {
|
||||
Self::SELECTOR_GRAY
|
||||
};
|
||||
let mut content = candidate.content.clone();
|
||||
let col_lim = cols.saturating_sub(3);
|
||||
if calc_str_width(&content) > col_lim {
|
||||
content.truncate(col_lim.saturating_sub(6) as usize); // ui bars + elipses length
|
||||
content.push_str("...");
|
||||
let mut drew_number = false;
|
||||
for line in candidate.content.trim_end().lines() {
|
||||
if lines_drawn >= max_height {
|
||||
break;
|
||||
}
|
||||
let left = format!("{} {}{}\x1b[0m", Self::VERT_LINE, &selector, &content);
|
||||
let mut line = line.trim_end().replace('\t', " ");
|
||||
let col_lim = if number_candidates{
|
||||
cols.saturating_sub(3 + min_pad as u16)
|
||||
} else {
|
||||
cols.saturating_sub(3)
|
||||
};
|
||||
if calc_str_width(&line) > col_lim {
|
||||
line.truncate(col_lim.saturating_sub(6) as usize);
|
||||
line.push_str("...");
|
||||
}
|
||||
let left = if number_candidates {
|
||||
if !drew_number {
|
||||
let this_num = i + offset + 1;
|
||||
let right_pad = " ".repeat(min_pad.saturating_sub(this_num.to_string().len()));
|
||||
format!("{} {}\x1b[33m{}\x1b[39m{right_pad}{}\x1b[0m", Self::VERT_LINE, &selector,i + offset + 1, &line)
|
||||
} else {
|
||||
let right_pad = " ".repeat(min_pad);
|
||||
format!("{} {}{}{}\x1b[0m", Self::VERT_LINE, &selector,right_pad, &line)
|
||||
}
|
||||
} else {
|
||||
format!("{} {}{}\x1b[0m", Self::VERT_LINE, &selector, &line)
|
||||
};
|
||||
let cols_used = calc_str_width(&left);
|
||||
let right_pad = " ".repeat(cols.saturating_sub(cols_used + 1) as usize);
|
||||
let hl_cand_line = format!("{}{}{}", left, right_pad, Self::VERT_LINE);
|
||||
buf.push_str(&hl_cand_line);
|
||||
rows += 1;
|
||||
drew_number = true;
|
||||
lines_drawn += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let bot_bar = format!(
|
||||
@@ -1003,15 +1024,14 @@ impl Completer for FuzzyCompleter {
|
||||
buf.push_str(&bot_bar);
|
||||
rows += 1;
|
||||
|
||||
// Move cursor back up to the prompt line (skip: separator + candidates + bottom border)
|
||||
let lines_below_prompt = rows.saturating_sub(2); // total rows minus top_bar and prompt
|
||||
let lines_below_prompt = rows.saturating_sub(2);
|
||||
let cursor_in_window = self
|
||||
.query
|
||||
.linebuf
|
||||
.cursor
|
||||
.get()
|
||||
.saturating_sub(self.query.scroll_offset);
|
||||
let cursor_col = (cursor_in_window + 4) as u16; // "| > ".len() == 4
|
||||
let cursor_col = (cursor_in_window + 4) as u16;
|
||||
write!(buf, "\x1b[{}A\r\x1b[{}C", lines_below_prompt, cursor_col).unwrap();
|
||||
|
||||
let new_layout = FuzzyLayout {
|
||||
@@ -1026,20 +1046,129 @@ impl Completer for FuzzyCompleter {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn clear(&mut self, writer: &mut TermWriter) -> ShResult<()> {
|
||||
if let Some(layout) = self.old_layout.take() {
|
||||
let (new_cols, _) = get_win_size(*TTY_FILENO);
|
||||
let total_cells = layout.rows as u32 * layout.cols as u32;
|
||||
let physical_rows = if new_cols > 0 {
|
||||
total_cells.div_ceil(new_cols as u32) as u16
|
||||
} else {
|
||||
layout.rows
|
||||
};
|
||||
let cursor_offset = layout.cols as u32 + layout.cursor_col as u32;
|
||||
let cursor_phys_row = if new_cols > 0 {
|
||||
(cursor_offset / new_cols as u32) as u16
|
||||
} else {
|
||||
1
|
||||
};
|
||||
let lines_below = physical_rows.saturating_sub(cursor_phys_row + 1);
|
||||
|
||||
let gap_extra = if new_cols > 0 && layout.preceding_line_width > new_cols {
|
||||
let wrap_rows = (layout.preceding_line_width as u32).div_ceil(new_cols as u32) as u16;
|
||||
let cursor_wrap_row = layout.preceding_cursor_col / new_cols;
|
||||
wrap_rows.saturating_sub(cursor_wrap_row + 1)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let mut buf = String::new();
|
||||
if lines_below > 0 {
|
||||
write!(buf, "\x1b[{}B", lines_below).unwrap();
|
||||
}
|
||||
for _ in 0..physical_rows {
|
||||
buf.push_str("\x1b[2K\x1b[A");
|
||||
}
|
||||
buf.push_str("\x1b[2K");
|
||||
for _ in 0..gap_extra {
|
||||
buf.push_str("\x1b[A\x1b[2K");
|
||||
}
|
||||
writer.flush_write(&buf)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FuzzyCompleter {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
completer: SimpleCompleter::default(),
|
||||
selector: FuzzySelector::new("Complete"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Completer for FuzzyCompleter {
|
||||
fn set_prompt_line_context(&mut self, line_width: u16, cursor_col: u16) {
|
||||
self.selector.set_prompt_line_context(line_width, cursor_col);
|
||||
}
|
||||
fn reset_stay_active(&mut self) {
|
||||
self.selector.reset_stay_active();
|
||||
}
|
||||
fn get_completed_line(&self, _candidate: &str) -> String {
|
||||
log::debug!("Getting completed line for candidate: {}", _candidate);
|
||||
|
||||
let selected = self.selector.selected_candidate().unwrap_or_default();
|
||||
log::debug!("Selected candidate: {}", selected);
|
||||
let (start, end) = self.completer.token_span;
|
||||
log::debug!("Token span: ({}, {})", start, end);
|
||||
let ret = format!(
|
||||
"{}{}{}",
|
||||
&self.completer.original_input[..start],
|
||||
selected,
|
||||
&self.completer.original_input[end..]
|
||||
);
|
||||
log::debug!("Completed line: {}", ret);
|
||||
ret
|
||||
}
|
||||
fn complete(
|
||||
&mut self,
|
||||
line: String,
|
||||
cursor_pos: usize,
|
||||
direction: i32,
|
||||
) -> ShResult<Option<String>> {
|
||||
self.completer.complete(line, cursor_pos, direction)?;
|
||||
let candidates: Vec<_> = self.completer.candidates.clone();
|
||||
if candidates.is_empty() {
|
||||
self.completer.reset();
|
||||
self.selector.active = false;
|
||||
return Ok(None);
|
||||
} else if candidates.len() == 1 {
|
||||
self.selector.filtered = candidates.into_iter().map(ScoredCandidate::from).collect();
|
||||
let selected = self.selector.filtered[0].content.clone();
|
||||
let completed = self.get_completed_line(&selected);
|
||||
self.selector.active = false;
|
||||
return Ok(Some(completed));
|
||||
}
|
||||
self.selector.activate(candidates);
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn handle_key(&mut self, key: K) -> ShResult<CompResponse> {
|
||||
match self.selector.handle_key(key)? {
|
||||
SelectorResponse::Accept(s) => Ok(CompResponse::Accept(s)),
|
||||
SelectorResponse::Dismiss => Ok(CompResponse::Dismiss),
|
||||
SelectorResponse::Consumed => Ok(CompResponse::Consumed),
|
||||
}
|
||||
}
|
||||
fn clear(&mut self, writer: &mut TermWriter) -> ShResult<()> {
|
||||
self.selector.clear(writer)
|
||||
}
|
||||
fn draw(&mut self, writer: &mut TermWriter) -> ShResult<()> {
|
||||
self.selector.draw(writer)
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
*self = Self::default();
|
||||
self.completer.reset();
|
||||
self.selector.reset();
|
||||
}
|
||||
fn token_span(&self) -> (usize, usize) {
|
||||
self.completer.token_span()
|
||||
}
|
||||
fn is_active(&self) -> bool {
|
||||
self.active
|
||||
self.selector.is_active()
|
||||
}
|
||||
fn selected_candidate(&self) -> Option<String> {
|
||||
self
|
||||
.filtered
|
||||
.get(self.cursor.get())
|
||||
.map(|c| c.content.clone())
|
||||
self.selector.selected_candidate()
|
||||
}
|
||||
fn original_input(&self) -> &str {
|
||||
&self.completer.original_input
|
||||
|
||||
@@ -105,14 +105,17 @@ impl Highlighter {
|
||||
self.in_selection = false;
|
||||
}
|
||||
_ if self.only_hl_visual => {
|
||||
if !is_marker(ch) {
|
||||
self.output.push(ch);
|
||||
}
|
||||
}
|
||||
markers::STRING_DQ_END
|
||||
| markers::STRING_SQ_END
|
||||
| markers::VAR_SUB_END
|
||||
| markers::CMD_SUB_END
|
||||
| markers::PROC_SUB_END
|
||||
| markers::SUBSH_END => self.pop_style(),
|
||||
| markers::SUBSH_END
|
||||
| markers::HIST_EXP_END => self.pop_style(),
|
||||
|
||||
markers::CMD_SEP | markers::RESET => self.clear_styles(),
|
||||
|
||||
@@ -276,6 +279,23 @@ impl Highlighter {
|
||||
}
|
||||
self.last_was_reset = false;
|
||||
}
|
||||
markers::HIST_EXP => {
|
||||
let mut hist_exp = String::new();
|
||||
while let Some(ch) = input_chars.peek() {
|
||||
if *ch == markers::HIST_EXP_END {
|
||||
input_chars.next();
|
||||
break;
|
||||
} else if markers::is_marker(*ch) {
|
||||
input_chars.next();
|
||||
continue;
|
||||
}
|
||||
hist_exp.push(*ch);
|
||||
input_chars.next();
|
||||
}
|
||||
self.push_style(Style::Blue);
|
||||
self.output.push_str(&hist_exp);
|
||||
self.pop_style();
|
||||
}
|
||||
markers::VAR_SUB => {
|
||||
let mut var_sub = String::new();
|
||||
while let Some(ch) = input_chars.peek() {
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
env,
|
||||
fmt::{Display, Write},
|
||||
fs::{self, OpenOptions},
|
||||
io::Write as IoWrite,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
cmp::Ordering, collections::HashSet, env, fmt::{Display, Write}, fs::{self, OpenOptions}, io::Write as IoWrite, path::{Path, PathBuf}, str::FromStr, time::{Duration, SystemTime, UNIX_EPOCH}
|
||||
};
|
||||
|
||||
use crate::{
|
||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||
readline::linebuf::LineBuf,
|
||||
readline::{complete::FuzzySelector, linebuf::LineBuf},
|
||||
};
|
||||
|
||||
#[derive(Default, Clone, Copy, Debug)]
|
||||
@@ -207,6 +200,7 @@ pub struct History {
|
||||
pub pending: Option<LineBuf>, // command, cursor_pos
|
||||
entries: Vec<HistEntry>,
|
||||
search_mask: Vec<HistEntry>,
|
||||
pub fuzzy_finder: FuzzySelector,
|
||||
no_matches: bool,
|
||||
pub cursor: usize,
|
||||
//search_direction: Direction,
|
||||
@@ -232,6 +226,7 @@ impl History {
|
||||
Ok(Self {
|
||||
path,
|
||||
entries,
|
||||
fuzzy_finder: FuzzySelector::new("History").number_candidates(true),
|
||||
pending: None,
|
||||
search_mask,
|
||||
no_matches: false,
|
||||
@@ -242,6 +237,20 @@ impl History {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn start_search(&mut self, initial: &str) -> Option<String> {
|
||||
if self.search_mask.is_empty() {
|
||||
None
|
||||
} else if self.search_mask.len() == 1 {
|
||||
Some(self.search_mask[0].command().to_string())
|
||||
} else {
|
||||
self.fuzzy_finder.set_query(initial.to_string());
|
||||
let raw_entries = self.search_mask.clone().into_iter()
|
||||
.map(|ent| ent.command().to_string());
|
||||
self.fuzzy_finder.activate(raw_entries.collect());
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.search_mask = dedupe_entries(&self.entries);
|
||||
self.cursor = self.search_mask.len();
|
||||
@@ -291,6 +300,36 @@ impl History {
|
||||
pub fn last_mut(&mut self) -> Option<&mut HistEntry> {
|
||||
self.entries.last_mut()
|
||||
}
|
||||
pub fn last(&self) -> Option<&HistEntry> {
|
||||
self.entries.last()
|
||||
}
|
||||
|
||||
pub fn resolve_hist_token(&self, token: &str) -> Option<String> {
|
||||
let token = token.strip_prefix('!').unwrap_or(token).to_string();
|
||||
if let Ok(num) = token.parse::<i32>() && num != 0 {
|
||||
match num.cmp(&0) {
|
||||
Ordering::Less => {
|
||||
if num.unsigned_abs() > self.entries.len() as u32 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let rev_idx = self.entries.len() - num.unsigned_abs() as usize;
|
||||
self.entries.get(rev_idx)
|
||||
.map(|e| e.command().to_string())
|
||||
}
|
||||
Ordering::Greater => {
|
||||
self.entries.get(num as usize)
|
||||
.map(|e| e.command().to_string())
|
||||
}
|
||||
_ => unreachable!()
|
||||
}
|
||||
} else {
|
||||
let mut rev_search = self.entries.iter();
|
||||
rev_search
|
||||
.rfind(|e| e.command().starts_with(&token))
|
||||
.map(|e| e.command().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_new_id(&self) -> u32 {
|
||||
let Some(ent) = self.entries.last() else {
|
||||
|
||||
@@ -114,6 +114,10 @@ impl KeyEvent {
|
||||
"Cannot convert unknown escape sequence to Vim key sequence".to_string(),
|
||||
));
|
||||
}
|
||||
KeyCode::ExMode => {
|
||||
seq.push_str("CMD");
|
||||
needs_angle_bracket = true;
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
seq.push_str("BS");
|
||||
needs_angle_bracket = true;
|
||||
@@ -222,6 +226,9 @@ pub enum KeyCode {
|
||||
Right,
|
||||
Tab,
|
||||
Up,
|
||||
|
||||
// weird stuff
|
||||
ExMode, // keycode emitted by the <cmd> byte alias in vim keymaps
|
||||
}
|
||||
|
||||
bitflags::bitflags! {
|
||||
|
||||
@@ -15,13 +15,11 @@ use crate::{
|
||||
libsh::{error::ShResult, guards::var_ctx_guard},
|
||||
parse::{
|
||||
execute::exec_input,
|
||||
lex::{LexFlags, LexStream, Tk, TkFlags, TkRule},
|
||||
lex::{LexFlags, LexStream, QuoteState, Tk, TkFlags, TkRule},
|
||||
},
|
||||
prelude::*,
|
||||
readline::{
|
||||
markers,
|
||||
register::{RegisterContent, write_register},
|
||||
term::RawModeGuard,
|
||||
history::History, markers, register::{RegisterContent, write_register}, term::RawModeGuard
|
||||
},
|
||||
state::{VarFlags, VarKind, read_shopts, write_meta, write_vars},
|
||||
};
|
||||
@@ -299,6 +297,13 @@ impl ClampedUsize {
|
||||
let max = self.upper_bound();
|
||||
self.value = (self.value + value).clamp(0, max)
|
||||
}
|
||||
pub fn add_signed(&mut self, value: isize) {
|
||||
if value.is_negative() {
|
||||
self.sub(value.wrapping_abs() as usize);
|
||||
} else {
|
||||
self.add(value as usize);
|
||||
}
|
||||
}
|
||||
pub fn sub(&mut self, value: usize) {
|
||||
self.value = self.value.saturating_sub(value)
|
||||
}
|
||||
@@ -645,6 +650,10 @@ impl LineBuf {
|
||||
self.buffer.push_str(slice);
|
||||
self.update_graphemes();
|
||||
}
|
||||
pub fn insert_str_at_cursor(&mut self, slice: &str) {
|
||||
let pos = self.index_byte_pos(self.cursor.get());
|
||||
self.insert_str_at(pos, slice);
|
||||
}
|
||||
pub fn insert_at_cursor(&mut self, ch: char) {
|
||||
self.insert_at(self.cursor.get(), ch);
|
||||
}
|
||||
@@ -2893,7 +2902,7 @@ impl LineBuf {
|
||||
}
|
||||
}
|
||||
Verb::Insert(string) => {
|
||||
self.push_str(&string);
|
||||
self.insert_str_at_cursor(&string);
|
||||
let graphemes = string.graphemes(true).count();
|
||||
log::debug!("Inserted string: {string:?}, graphemes: {graphemes}");
|
||||
log::debug!("buffer after insert: {:?}", self.buffer);
|
||||
@@ -3317,6 +3326,73 @@ impl LineBuf {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn attempt_history_expansion(&mut self, hist: &History) -> bool {
|
||||
self.update_graphemes();
|
||||
let mut changes: Vec<(Range<usize>,String)> = vec![];
|
||||
let mut graphemes = self.buffer.grapheme_indices(true);
|
||||
let mut qt_state = QuoteState::default();
|
||||
|
||||
while let Some((i,gr)) = graphemes.next() {
|
||||
match gr {
|
||||
"\\" => {
|
||||
graphemes.next();
|
||||
}
|
||||
"'" => qt_state.toggle_single(),
|
||||
"\"" => qt_state.toggle_double(),
|
||||
"!" if !qt_state.in_single() => {
|
||||
let start = i;
|
||||
match graphemes.next() {
|
||||
Some((_,"!")) => {
|
||||
// we have "!!", which expands to the previous command
|
||||
if let Some(prev) = hist.last() {
|
||||
let raw = prev.command();
|
||||
changes.push((start..start+2, raw.to_string()));
|
||||
}
|
||||
}
|
||||
Some((_,"$")) => {
|
||||
// we have "!$", which expands to the last word of the previous command
|
||||
if let Some(prev) = hist.last() {
|
||||
let raw = prev.command();
|
||||
if let Some(last_word) = raw.split_whitespace().last() {
|
||||
changes.push((start..start+2, last_word.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
Some((j,gr)) if !is_whitespace(gr) => {
|
||||
let mut end = j + gr.len();
|
||||
while let Some((k, gr2)) = graphemes.next() {
|
||||
if is_whitespace(gr2) { break; }
|
||||
end = k + gr2.len();
|
||||
}
|
||||
let token = &self.buffer[j..end];
|
||||
let cmd = hist.resolve_hist_token(token).unwrap_or(token.into());
|
||||
changes.push((start..end, cmd));
|
||||
}
|
||||
_ => { /* not a hist expansion */ }
|
||||
}
|
||||
}
|
||||
_ => { /* carry on */ }
|
||||
}
|
||||
}
|
||||
|
||||
let ret = !changes.is_empty();
|
||||
|
||||
let buf_len = self.grapheme_indices().len();
|
||||
|
||||
for (range,change) in changes.into_iter().rev() {
|
||||
self.buffer.replace_range(range, &change);
|
||||
}
|
||||
self.update_graphemes();
|
||||
|
||||
let new_len = self.grapheme_indices().len();
|
||||
let delta = new_len as isize - buf_len as isize;
|
||||
|
||||
self.cursor.set_max(new_len);
|
||||
self.cursor.add_signed(delta);
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.buffer // FIXME: this will have to be fixed up later
|
||||
}
|
||||
|
||||
@@ -12,11 +12,11 @@ use crate::expand::expand_prompt;
|
||||
use crate::libsh::sys::TTY_FILENO;
|
||||
use crate::libsh::utils::AutoCmdVecUtils;
|
||||
use crate::parse::lex::{LexStream, QuoteState};
|
||||
use crate::readline::complete::FuzzyCompleter;
|
||||
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, VarFlags, VarKind, read_logic, read_shopts, write_meta, write_vars,
|
||||
AutoCmdKind, ShellParam, VarFlags, VarKind, read_logic, read_shopts, with_vars, write_meta, write_vars
|
||||
};
|
||||
use crate::{
|
||||
libsh::error::ShResult,
|
||||
@@ -76,6 +76,8 @@ pub mod markers {
|
||||
pub const STRING_SQ_END: Marker = '\u{e115}';
|
||||
pub const ESCAPE: Marker = '\u{e116}';
|
||||
pub const GLOB: Marker = '\u{e117}';
|
||||
pub const HIST_EXP: Marker = '\u{e11c}';
|
||||
pub const HIST_EXP_END: Marker = '\u{e11d}';
|
||||
|
||||
// other
|
||||
pub const VISUAL_MODE_START: Marker = '\u{e118}';
|
||||
@@ -409,11 +411,51 @@ impl ShedVi {
|
||||
|
||||
// Process all available keys
|
||||
while let Some(key) = self.reader.read_key()? {
|
||||
// If completer is active, delegate input to it
|
||||
if self.completer.is_active() {
|
||||
// If completer or history search are active, delegate input to it
|
||||
if self.history.fuzzy_finder.is_active() {
|
||||
self.print_line(false)?;
|
||||
match self.history.fuzzy_finder.handle_key(key)? {
|
||||
SelectorResponse::Accept(cmd) => {
|
||||
let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnHistorySelect));
|
||||
|
||||
self.editor.set_buffer(cmd.to_string());
|
||||
self.editor.move_cursor_to_end();
|
||||
self.history.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
|
||||
self.editor.set_hint(None);
|
||||
self.history.fuzzy_finder.clear(&mut self.writer)?;
|
||||
self.history.fuzzy_finder.reset();
|
||||
|
||||
with_vars([("_HIST_ENTRY".into(), cmd.clone())], || {
|
||||
post_cmds.exec_with(&cmd);
|
||||
});
|
||||
|
||||
write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str(self.mode.report_mode().to_string()), VarFlags::NONE)).ok();
|
||||
self.prompt.refresh();
|
||||
self.needs_redraw = true;
|
||||
continue;
|
||||
}
|
||||
SelectorResponse::Dismiss => {
|
||||
let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnHistoryClose));
|
||||
post_cmds.exec();
|
||||
|
||||
self.editor.set_hint(None);
|
||||
self.history.fuzzy_finder.clear(&mut self.writer)?;
|
||||
write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str(self.mode.report_mode().to_string()), VarFlags::NONE)).ok();
|
||||
self.prompt.refresh();
|
||||
self.needs_redraw = true;
|
||||
continue;
|
||||
}
|
||||
SelectorResponse::Consumed => {
|
||||
self.needs_redraw = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else if self.completer.is_active() {
|
||||
self.print_line(false)?;
|
||||
match self.completer.handle_key(key.clone())? {
|
||||
CompResponse::Accept(candidate) => {
|
||||
let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnCompletionSelect));
|
||||
|
||||
let span_start = self.completer.token_span().0;
|
||||
let new_cursor = span_start + candidate.len();
|
||||
let line = self.completer.get_completed_line(&candidate);
|
||||
@@ -432,12 +474,22 @@ impl ShedVi {
|
||||
self.completer.clear(&mut self.writer)?;
|
||||
self.needs_redraw = true;
|
||||
self.completer.reset();
|
||||
|
||||
with_vars([("_COMP_CANDIDATE".into(), candidate.clone())], || {
|
||||
post_cmds.exec_with(&candidate);
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
CompResponse::Dismiss => {
|
||||
let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnCompletionCancel));
|
||||
post_cmds.exec();
|
||||
|
||||
let hint = self.history.get_hint();
|
||||
self.editor.set_hint(hint);
|
||||
self.completer.clear(&mut self.writer)?;
|
||||
write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str(self.mode.report_mode().to_string()), VarFlags::NONE)).ok();
|
||||
self.prompt.refresh();
|
||||
self.completer.reset();
|
||||
continue;
|
||||
}
|
||||
@@ -486,6 +538,9 @@ impl ShedVi {
|
||||
return Ok(event);
|
||||
}
|
||||
}
|
||||
if !self.completer.is_active() && !self.history.fuzzy_finder.is_active() {
|
||||
write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str(self.mode.report_mode().to_string()), VarFlags::NONE)).ok();
|
||||
}
|
||||
|
||||
// Redraw if we processed any input
|
||||
if self.needs_redraw {
|
||||
@@ -498,6 +553,7 @@ impl ShedVi {
|
||||
|
||||
pub fn handle_key(&mut self, key: KeyEvent) -> ShResult<Option<ReadlineEvent>> {
|
||||
if self.should_accept_hint(&key) {
|
||||
log::debug!("Accepting hint on key {key:?} in mode {:?}", self.mode.report_mode());
|
||||
self.editor.accept_hint();
|
||||
if !self.history.at_pending() {
|
||||
self.history.reset_to_pending();
|
||||
@@ -510,6 +566,12 @@ impl ShedVi {
|
||||
}
|
||||
|
||||
if let KeyEvent(KeyCode::Tab, mod_keys) = key {
|
||||
if self.editor.attempt_history_expansion(&self.history) {
|
||||
// If history expansion occurred, don't attempt completion yet
|
||||
// allow the user to see the expanded command and accept or edit it before completing
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let direction = match mod_keys {
|
||||
ModKeys::SHIFT => -1,
|
||||
_ => 1,
|
||||
@@ -524,7 +586,14 @@ impl ShedVi {
|
||||
self.old_layout = None;
|
||||
}
|
||||
Ok(Some(line)) => {
|
||||
let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnCompletionSelect));
|
||||
let cand = self.completer.selected_candidate().unwrap_or_default();
|
||||
with_vars([("_COMP_CANDIDATE".into(), cand.clone())], || {
|
||||
post_cmds.exec_with(&cand);
|
||||
});
|
||||
|
||||
let span_start = self.completer.token_span().0;
|
||||
|
||||
let new_cursor = span_start
|
||||
+ self
|
||||
.completer
|
||||
@@ -532,7 +601,7 @@ impl ShedVi {
|
||||
.map(|c| c.len())
|
||||
.unwrap_or_default();
|
||||
|
||||
self.editor.set_buffer(line);
|
||||
self.editor.set_buffer(line.clone());
|
||||
self.editor.cursor.set(new_cursor);
|
||||
|
||||
if !self.history.at_pending() {
|
||||
@@ -543,10 +612,19 @@ impl ShedVi {
|
||||
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
|
||||
let hint = self.history.get_hint();
|
||||
self.editor.set_hint(hint);
|
||||
|
||||
|
||||
}
|
||||
Ok(None) => {
|
||||
let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnCompletionStart));
|
||||
|
||||
post_cmds.exec();
|
||||
|
||||
self.writer.send_bell().ok();
|
||||
if self.completer.is_active() {
|
||||
write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str("COMPLETE".to_string()), VarFlags::NONE)).ok();
|
||||
self.prompt.refresh();
|
||||
self.needs_redraw = true;
|
||||
self.editor.set_hint(None);
|
||||
}
|
||||
}
|
||||
@@ -554,6 +632,33 @@ impl ShedVi {
|
||||
|
||||
self.needs_redraw = true;
|
||||
return Ok(None);
|
||||
} else if let KeyEvent(KeyCode::Char('R'), ModKeys::CTRL) = key {
|
||||
let initial = self.editor.as_str();
|
||||
match self.history.start_search(initial) {
|
||||
Some(entry) => {
|
||||
let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnHistorySelect));
|
||||
with_vars([("_HIST_ENTRY".into(), entry.clone())], || {
|
||||
post_cmds.exec_with(&entry);
|
||||
});
|
||||
|
||||
self.editor.set_buffer(entry);
|
||||
self.editor.move_cursor_to_end();
|
||||
self.history.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
|
||||
self.editor.set_hint(None);
|
||||
}
|
||||
None => {
|
||||
let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnHistoryOpen));
|
||||
post_cmds.exec();
|
||||
|
||||
self.writer.send_bell().ok();
|
||||
if self.history.fuzzy_finder.is_active() {
|
||||
write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str("SEARCH".to_string()), VarFlags::NONE)).ok();
|
||||
self.prompt.refresh();
|
||||
self.needs_redraw = true;
|
||||
self.editor.set_hint(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let KeyEvent(KeyCode::Char('\\'), ModKeys::NONE) = key
|
||||
@@ -586,6 +691,12 @@ impl ShedVi {
|
||||
&& !self.editor.buffer.ends_with('\\')
|
||||
&& (self.should_submit()? || !read_shopts(|o| o.prompt.linebreak_on_incomplete))
|
||||
{
|
||||
if self.editor.attempt_history_expansion(&self.history) {
|
||||
// If history expansion occurred, don't submit yet
|
||||
// allow the user to see the expanded command and accept or edit it before submitting
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
self.editor.set_hint(None);
|
||||
self.editor.cursor.set(self.editor.cursor_max());
|
||||
self.print_line(true)?;
|
||||
@@ -747,6 +858,7 @@ impl ShedVi {
|
||||
let one_line = new_layout.end.row == 0;
|
||||
|
||||
self.completer.clear(&mut self.writer)?;
|
||||
self.history.fuzzy_finder.clear(&mut self.writer)?;
|
||||
|
||||
if let Some(layout) = self.old_layout.as_ref() {
|
||||
self.writer.clear_rows(layout)?;
|
||||
@@ -837,6 +949,9 @@ impl ShedVi {
|
||||
.set_prompt_line_context(preceding_width, new_layout.cursor.col);
|
||||
self.completer.draw(&mut self.writer)?;
|
||||
|
||||
self.history.fuzzy_finder.set_prompt_line_context(preceding_width, new_layout.cursor.col);
|
||||
self.history.fuzzy_finder.draw(&mut self.writer)?;
|
||||
|
||||
self.old_layout = Some(new_layout);
|
||||
self.needs_redraw = false;
|
||||
|
||||
@@ -1529,6 +1644,35 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
|
||||
insertions.push((span_start + index, markers::GLOB));
|
||||
}
|
||||
}
|
||||
'!' if !qt_state.in_single() && cmd_sub_depth == 0 && proc_sub_depth == 0 => {
|
||||
let bang_pos = index;
|
||||
token_chars.next(); // consume the '!'
|
||||
if let Some((_, next_ch)) = token_chars.peek() {
|
||||
match next_ch {
|
||||
'!' | '$' => {
|
||||
// !! or !$
|
||||
token_chars.next();
|
||||
insertions.push((span_start + bang_pos, markers::HIST_EXP));
|
||||
insertions.push((span_start + bang_pos + 2, markers::HIST_EXP_END));
|
||||
}
|
||||
c if c.is_ascii_alphanumeric() || *c == '-' => {
|
||||
// !word, !-N, !N
|
||||
let mut end_pos = bang_pos + 1;
|
||||
while let Some((cur_i, wch)) = token_chars.peek() {
|
||||
if wch.is_ascii_alphanumeric() || *wch == '_' || *wch == '-' {
|
||||
end_pos = *cur_i + 1;
|
||||
token_chars.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
insertions.push((span_start + bang_pos, markers::HIST_EXP));
|
||||
insertions.push((span_start + end_pos, markers::HIST_EXP_END));
|
||||
}
|
||||
_ => { /* lone ! before non-expansion char, ignore */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
token_chars.next(); // consume the char with no special handling
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ pub fn get_win_size(fd: RawFd) -> (Col, Row) {
|
||||
}
|
||||
}
|
||||
|
||||
fn enumerate_lines(s: &str, left_pad: usize) -> String {
|
||||
fn enumerate_lines(s: &str, left_pad: usize, show_numbers: bool) -> String {
|
||||
let total_lines = s.lines().count();
|
||||
let max_num_len = total_lines.to_string().len();
|
||||
s.lines()
|
||||
@@ -84,23 +84,15 @@ fn enumerate_lines(s: &str, left_pad: usize) -> String {
|
||||
// " 2 | " — num + padding + " | "
|
||||
let prefix_len = max_num_len + 3; // "N | "
|
||||
let trail_pad = left_pad.saturating_sub(prefix_len);
|
||||
if i == total_lines - 1 {
|
||||
// Don't add a newline to the last line
|
||||
write!(
|
||||
acc,
|
||||
"\x1b[0m\x1b[90m{}{num} |\x1b[0m {}{ln}",
|
||||
" ".repeat(num_pad),
|
||||
" ".repeat(trail_pad),
|
||||
)
|
||||
.unwrap();
|
||||
let prefix = if show_numbers {
|
||||
format!("\x1b[0m\x1b[90m{}{num} |\x1b[0m ", " ".repeat(num_pad))
|
||||
} else {
|
||||
writeln!(
|
||||
acc,
|
||||
"\x1b[0m\x1b[90m{}{num} |\x1b[0m {}{ln}",
|
||||
" ".repeat(num_pad),
|
||||
" ".repeat(trail_pad),
|
||||
)
|
||||
.unwrap();
|
||||
" ".repeat(prefix_len + 1).to_string()
|
||||
};
|
||||
if i == total_lines - 1 {
|
||||
write!(acc, "{prefix}{}{ln}", " ".repeat(trail_pad)).unwrap();
|
||||
} else {
|
||||
writeln!(acc, "{prefix}{}{ln}", " ".repeat(trail_pad)).unwrap();
|
||||
}
|
||||
}
|
||||
acc
|
||||
@@ -1013,7 +1005,8 @@ impl LineWriter for TermWriter {
|
||||
let multiline = line.contains('\n');
|
||||
if multiline {
|
||||
let prompt_end = Layout::calc_pos(self.t_cols, prompt, Pos { col: 0, row: 0 }, 0, false);
|
||||
let display_line = enumerate_lines(line, prompt_end.col as usize);
|
||||
let show_numbers = read_shopts(|o| o.prompt.line_numbers);
|
||||
let display_line = enumerate_lines(line, prompt_end.col as usize, show_numbers);
|
||||
self.buffer.push_str(&display_line);
|
||||
} else {
|
||||
self.buffer.push_str(line);
|
||||
|
||||
@@ -54,6 +54,15 @@ impl ViMode for ViInsert {
|
||||
.set_motion(MotionCmd(1, Motion::ForwardChar));
|
||||
self.register_and_return()
|
||||
}
|
||||
E(K::ExMode, _) => {
|
||||
Some(ViCmd {
|
||||
register: Default::default(),
|
||||
verb: Some(VerbCmd(1, Verb::ExMode)),
|
||||
motion: None,
|
||||
raw_seq: String::new(),
|
||||
flags: Default::default(),
|
||||
})
|
||||
}
|
||||
E(K::Char('W'), M::CTRL) => {
|
||||
self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete));
|
||||
self.pending_cmd.set_motion(MotionCmd(
|
||||
|
||||
@@ -756,6 +756,15 @@ impl ViMode for ViNormal {
|
||||
raw_seq: "".into(),
|
||||
flags: self.flags(),
|
||||
}),
|
||||
E(K::ExMode, _) => {
|
||||
return Some(ViCmd {
|
||||
register: Default::default(),
|
||||
verb: Some(VerbCmd(1, Verb::ExMode)),
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
});
|
||||
}
|
||||
E(K::Char('A'), M::CTRL) => {
|
||||
let count = self
|
||||
.parse_count(&mut self.pending_seq.chars().peekable())
|
||||
|
||||
@@ -41,6 +41,15 @@ impl ViMode for ViReplace {
|
||||
.set_motion(MotionCmd(1, Motion::ForwardChar));
|
||||
self.register_and_return()
|
||||
}
|
||||
E(K::ExMode, _) => {
|
||||
Some(ViCmd {
|
||||
register: Default::default(),
|
||||
verb: Some(VerbCmd(1, Verb::ExMode)),
|
||||
motion: None,
|
||||
raw_seq: String::new(),
|
||||
flags: Default::default(),
|
||||
})
|
||||
}
|
||||
E(K::Char('W'), M::CTRL) => {
|
||||
self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete));
|
||||
self.pending_cmd.set_motion(MotionCmd(
|
||||
|
||||
@@ -614,6 +614,15 @@ impl ViMode for ViVisual {
|
||||
raw_seq: "".into(),
|
||||
flags: CmdFlags::empty(),
|
||||
}),
|
||||
E(K::ExMode, _) => {
|
||||
return Some(ViCmd {
|
||||
register: Default::default(),
|
||||
verb: Some(VerbCmd(1, Verb::ExMode)),
|
||||
motion: None,
|
||||
raw_seq: String::new(),
|
||||
flags: Default::default(),
|
||||
});
|
||||
}
|
||||
E(K::Char('A'), M::CTRL) => {
|
||||
let count = self
|
||||
.parse_count(&mut self.pending_seq.chars().peekable())
|
||||
|
||||
18
src/state.rs
18
src/state.rs
@@ -550,6 +550,12 @@ pub enum AutoCmdKind {
|
||||
PostPrompt,
|
||||
PreModeChange,
|
||||
PostModeChange,
|
||||
OnHistoryOpen,
|
||||
OnHistoryClose,
|
||||
OnHistorySelect,
|
||||
OnCompletionStart,
|
||||
OnCompletionCancel,
|
||||
OnCompletionSelect,
|
||||
OnExit,
|
||||
}
|
||||
|
||||
@@ -565,6 +571,12 @@ impl Display for AutoCmdKind {
|
||||
Self::PostPrompt => write!(f, "post-prompt"),
|
||||
Self::PreModeChange => write!(f, "pre-mode-change"),
|
||||
Self::PostModeChange => write!(f, "post-mode-change"),
|
||||
Self::OnHistoryOpen => write!(f, "on-history-open"),
|
||||
Self::OnHistoryClose => write!(f, "on-history-close"),
|
||||
Self::OnHistorySelect => write!(f, "on-history-select"),
|
||||
Self::OnCompletionStart => write!(f, "on-completion-start"),
|
||||
Self::OnCompletionCancel => write!(f, "on-completion-cancel"),
|
||||
Self::OnCompletionSelect => write!(f, "on-completion-select"),
|
||||
Self::OnExit => write!(f, "on-exit"),
|
||||
}
|
||||
}
|
||||
@@ -583,6 +595,12 @@ impl FromStr for AutoCmdKind {
|
||||
"post-prompt" => Ok(Self::PostPrompt),
|
||||
"pre-mode-change" => Ok(Self::PreModeChange),
|
||||
"post-mode-change" => Ok(Self::PostModeChange),
|
||||
"on-history-open" => Ok(Self::OnHistoryOpen),
|
||||
"on-history-close" => Ok(Self::OnHistoryClose),
|
||||
"on-history-select" => Ok(Self::OnHistorySelect),
|
||||
"on-completion-start" => Ok(Self::OnCompletionStart),
|
||||
"on-completion-cancel" => Ok(Self::OnCompletionCancel),
|
||||
"on-completion-select" => Ok(Self::OnCompletionSelect),
|
||||
"on-exit" => Ok(Self::OnExit),
|
||||
_ => Err(ShErr::simple(
|
||||
ShErrKind::ParseErr,
|
||||
|
||||
Reference in New Issue
Block a user