fuzzy completion now looks good and works good
This commit is contained in:
@@ -278,7 +278,8 @@ fn shed_interactive() -> ShResult<()> {
|
|||||||
let command_run_time = start.elapsed();
|
let command_run_time = start.elapsed();
|
||||||
log::info!("Command executed in {:.2?}", command_run_time);
|
log::info!("Command executed in {:.2?}", command_run_time);
|
||||||
write_meta(|m| m.stop_timer());
|
write_meta(|m| m.stop_timer());
|
||||||
readline.writer.flush_write("\n")?;
|
readline.fix_column()?;
|
||||||
|
readline.writer.flush_write("\n\r")?;
|
||||||
|
|
||||||
// Reset for next command with fresh prompt
|
// Reset for next command with fresh prompt
|
||||||
readline.reset(true)?;
|
readline.reset(true)?;
|
||||||
|
|||||||
@@ -8,16 +8,14 @@ use unicode_width::UnicodeWidthStr;
|
|||||||
use crate::{
|
use crate::{
|
||||||
builtin::complete::{CompFlags, CompOptFlags, CompOpts},
|
builtin::complete::{CompFlags, CompOptFlags, CompOpts},
|
||||||
libsh::{
|
libsh::{
|
||||||
error::ShResult,
|
error::ShResult, guards::var_ctx_guard, sys::TTY_FILENO, utils::TkVecUtils
|
||||||
guards::var_ctx_guard,
|
|
||||||
utils::TkVecUtils,
|
|
||||||
},
|
},
|
||||||
parse::{
|
parse::{
|
||||||
execute::exec_input,
|
execute::exec_input,
|
||||||
lex::{self, LexFlags, Tk, TkRule, ends_with_unescaped},
|
lex::{self, LexFlags, Tk, TkRule, ends_with_unescaped},
|
||||||
},
|
},
|
||||||
readline::{
|
readline::{
|
||||||
Marker, annotate_input_recursive, keys::{KeyCode as C, KeyEvent as K, ModKeys as M}, linebuf::{ClampedUsize, LineBuf}, markers::{self, is_marker}, term::{LineWriter, TermWriter}, vimode::{ViInsert, ViMode}
|
Marker, annotate_input_recursive, keys::{KeyCode as C, KeyEvent as K, ModKeys as M}, linebuf::{ClampedUsize, LineBuf}, markers::{self, is_marker}, term::{LineWriter, TermWriter, calc_str_width, get_win_size}, vimode::{ViInsert, ViMode}
|
||||||
},
|
},
|
||||||
state::{VarFlags, VarKind, read_jobs, read_logic, read_meta, read_vars, write_vars},
|
state::{VarFlags, VarKind, read_jobs, read_logic, read_meta, read_vars, write_vars},
|
||||||
};
|
};
|
||||||
@@ -529,11 +527,7 @@ pub trait Completer {
|
|||||||
fn draw(&mut self, writer: &mut TermWriter) -> ShResult<()>;
|
fn draw(&mut self, writer: &mut TermWriter) -> ShResult<()>;
|
||||||
fn clear(&mut self, _writer: &mut TermWriter) -> ShResult<()> { Ok(()) }
|
fn clear(&mut self, _writer: &mut TermWriter) -> ShResult<()> { Ok(()) }
|
||||||
fn handle_key(&mut self, key: K) -> ShResult<CompResponse>;
|
fn handle_key(&mut self, key: K) -> ShResult<CompResponse>;
|
||||||
fn get_completed_line(&self, candidate: &str) -> String {
|
fn get_completed_line(&self, candidate: &str) -> String;
|
||||||
let (start, end) = self.token_span();
|
|
||||||
let orig = self.original_input();
|
|
||||||
format!("{}{}{}", &orig[..start], candidate, &orig[end..])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone)]
|
#[derive(Default, Debug, Clone)]
|
||||||
@@ -622,13 +616,41 @@ pub struct FuzzyLayout {
|
|||||||
#[derive(Default, Debug, Clone)]
|
#[derive(Default, Debug, Clone)]
|
||||||
pub struct QueryEditor {
|
pub struct QueryEditor {
|
||||||
mode: ViInsert,
|
mode: ViInsert,
|
||||||
|
scroll_offset: usize,
|
||||||
|
available_width: usize,
|
||||||
linebuf: LineBuf
|
linebuf: LineBuf
|
||||||
}
|
}
|
||||||
|
|
||||||
impl QueryEditor {
|
impl QueryEditor {
|
||||||
pub fn clear(&mut self) {
|
pub fn clear(&mut self) {
|
||||||
self.linebuf = LineBuf::default();
|
self.linebuf = LineBuf::new();
|
||||||
self.mode = ViInsert::default();
|
self.mode = ViInsert::default();
|
||||||
|
self.scroll_offset = 0;
|
||||||
|
}
|
||||||
|
pub fn set_available_width(&mut self, width: usize) {
|
||||||
|
self.available_width = width;
|
||||||
|
}
|
||||||
|
pub fn update_scroll_offset(&mut self) {
|
||||||
|
self.linebuf.update_graphemes();
|
||||||
|
let cursor_pos = self.linebuf.cursor.get();
|
||||||
|
if cursor_pos < self.scroll_offset + 1 {
|
||||||
|
self.scroll_offset = self.linebuf.cursor.ret_sub(1);
|
||||||
|
}
|
||||||
|
if cursor_pos >= self.scroll_offset + self.available_width.saturating_sub(1) {
|
||||||
|
self.scroll_offset = self.linebuf.cursor.ret_sub(self.available_width.saturating_sub(1));
|
||||||
|
}
|
||||||
|
let max_offset = self.linebuf.grapheme_indices().len().saturating_sub(self.available_width);
|
||||||
|
self.scroll_offset = self.scroll_offset.min(max_offset);
|
||||||
|
}
|
||||||
|
pub fn get_window(&mut self) -> String {
|
||||||
|
self.linebuf.update_graphemes();
|
||||||
|
let buf_len = self.linebuf.grapheme_indices().len();
|
||||||
|
if buf_len <= self.available_width {
|
||||||
|
return self.linebuf.as_str().to_string();
|
||||||
|
}
|
||||||
|
let start = self.scroll_offset.min(buf_len.saturating_sub(self.available_width));
|
||||||
|
let end = (start + self.available_width).min(buf_len);
|
||||||
|
self.linebuf.slice(start..end).unwrap_or("").to_string()
|
||||||
}
|
}
|
||||||
pub fn handle_key(&mut self, key: K) -> ShResult<()> {
|
pub fn handle_key(&mut self, key: K) -> ShResult<()> {
|
||||||
let Some(cmd) = self.mode.handle_key(key) else {
|
let Some(cmd) = self.mode.handle_key(key) else {
|
||||||
@@ -653,6 +675,22 @@ pub struct FuzzyCompleter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FuzzyCompleter {
|
impl FuzzyCompleter {
|
||||||
|
const BOT_LEFT: &str = "\x1b[90m╰\x1b[0m";
|
||||||
|
const BOT_RIGHT: &str = "\x1b[90m╯\x1b[0m";
|
||||||
|
const TOP_LEFT: &str = "\x1b[90m╭\x1b[0m";
|
||||||
|
const TOP_RIGHT: &str = "\x1b[90m╮\x1b[0m";
|
||||||
|
const HOR_LINE: &str = "\x1b[90m─\x1b[0m";
|
||||||
|
const VERT_LINE: &str = "\x1b[90m│\x1b[0m";
|
||||||
|
const SELECTOR_GRAY: &str = "\x1b[90m▌\x1b[0m";
|
||||||
|
const SELECTOR_HL: &str = "\x1b[38;2;200;0;120m▌\x1b[1;39;48;5;237m";
|
||||||
|
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";
|
||||||
|
|
||||||
|
|
||||||
fn get_window(&mut self) -> &[ScoredCandidate] {
|
fn get_window(&mut self) -> &[ScoredCandidate] {
|
||||||
let height = self.filtered.len().min(self.max_height);
|
let height = self.filtered.len().min(self.max_height);
|
||||||
|
|
||||||
@@ -707,6 +745,22 @@ impl Default for FuzzyCompleter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Completer for FuzzyCompleter {
|
impl Completer for FuzzyCompleter {
|
||||||
|
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>> {
|
fn complete(&mut self, line: String, cursor_pos: usize, direction: i32) -> ShResult<Option<String>> {
|
||||||
self.completer.complete(line, cursor_pos, direction)?;
|
self.completer.complete(line, cursor_pos, direction)?;
|
||||||
let candidates: Vec<_> = self.completer.candidates.clone();
|
let candidates: Vec<_> = self.completer.candidates.clone();
|
||||||
@@ -714,12 +768,16 @@ impl Completer for FuzzyCompleter {
|
|||||||
self.completer.reset();
|
self.completer.reset();
|
||||||
self.active = false;
|
self.active = false;
|
||||||
return Ok(None);
|
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.active = true;
|
||||||
self.candidates = candidates;
|
self.candidates = candidates;
|
||||||
self.score_candidates();
|
self.score_candidates();
|
||||||
self.completer.reset();
|
Ok(None)
|
||||||
Ok(None) // FuzzyCompleter itself doesn't directly return a completed line, it manages the state of the filtered candidates and selection
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_key(&mut self, key: K) -> ShResult<CompResponse> {
|
fn handle_key(&mut self, key: K) -> ShResult<CompResponse> {
|
||||||
@@ -730,13 +788,11 @@ impl Completer for FuzzyCompleter {
|
|||||||
Ok(CompResponse::Dismiss)
|
Ok(CompResponse::Dismiss)
|
||||||
}
|
}
|
||||||
K(C::Enter, M::NONE) => {
|
K(C::Enter, M::NONE) => {
|
||||||
|
self.active = false;
|
||||||
if let Some(selected) = self.filtered.get(self.cursor.get()).map(|c| c.content.clone()) {
|
if let Some(selected) = self.filtered.get(self.cursor.get()).map(|c| c.content.clone()) {
|
||||||
self.active = false;
|
|
||||||
self.query.clear();
|
|
||||||
self.filtered.clear();
|
|
||||||
Ok(CompResponse::Accept(selected))
|
Ok(CompResponse::Accept(selected))
|
||||||
} else {
|
} else {
|
||||||
Ok(CompResponse::Passthrough)
|
Ok(CompResponse::Dismiss)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
K(C::Tab, M::SHIFT) |
|
K(C::Tab, M::SHIFT) |
|
||||||
@@ -761,16 +817,17 @@ impl Completer for FuzzyCompleter {
|
|||||||
fn clear(&mut self, writer: &mut TermWriter) -> ShResult<()> {
|
fn clear(&mut self, writer: &mut TermWriter) -> ShResult<()> {
|
||||||
if let Some(layout) = self.old_layout.take() {
|
if let Some(layout) = self.old_layout.take() {
|
||||||
let mut buf = String::new();
|
let mut buf = String::new();
|
||||||
// Cursor is on the query line. Move down to the last candidate.
|
// Cursor is on the prompt line. Move down to the bottom border.
|
||||||
if layout.rows > 0 {
|
let lines_below_prompt = layout.rows.saturating_sub(2);
|
||||||
write!(buf, "\x1b[{}B", layout.rows).unwrap();
|
if lines_below_prompt > 0 {
|
||||||
|
write!(buf, "\x1b[{}B", lines_below_prompt).unwrap();
|
||||||
}
|
}
|
||||||
// Erase each line and move up, back to the query line
|
// Erase each line moving up, back to the top border
|
||||||
for _ in 0..layout.rows {
|
for _ in 0..layout.rows {
|
||||||
buf.push_str("\x1b[2K\x1b[A");
|
buf.push_str("\x1b[2K\x1b[A");
|
||||||
}
|
}
|
||||||
// Erase the query line, then move up to the prompt line
|
// Erase the top border line
|
||||||
buf.push_str("\x1b[2K\x1b[A");
|
buf.push_str("\x1b[2K");
|
||||||
writer.flush_write(&buf)?;
|
writer.flush_write(&buf)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -779,31 +836,89 @@ impl Completer for FuzzyCompleter {
|
|||||||
if !self.active {
|
if !self.active {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
let (cols,_) = get_win_size(*TTY_FILENO);
|
||||||
|
|
||||||
let mut buf = String::new();
|
let mut buf = String::new();
|
||||||
let cursor_pos = self.cursor.get();
|
let cursor_pos = self.cursor.get();
|
||||||
let offset = self.scroll_offset;
|
let offset = self.scroll_offset;
|
||||||
let query = self.query.linebuf.as_str().to_string();
|
self.query.set_available_width(cols.saturating_sub(6) as usize);
|
||||||
|
self.query.update_scroll_offset();
|
||||||
|
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 visible = self.get_window();
|
let visible = self.get_window();
|
||||||
buf.push_str("\n\r> ");
|
let mut rows = 0;
|
||||||
buf.push_str(&query);
|
let top_bar = format!("\n{}{}{}",
|
||||||
|
Self::TOP_LEFT,
|
||||||
|
Self::HOR_LINE.to_string().repeat(cols.saturating_sub(2) as usize),
|
||||||
|
Self::TOP_RIGHT
|
||||||
|
);
|
||||||
|
buf.push_str(&top_bar);
|
||||||
|
rows += 1;
|
||||||
|
for _ in 0..rows {
|
||||||
|
}
|
||||||
|
|
||||||
|
let prompt = format!("{} {} {}", Self::VERT_LINE, Self::PROMPT_ARROW, &query);
|
||||||
|
let cols_used = calc_str_width(&prompt);
|
||||||
|
let right_pad = " ".repeat(cols.saturating_sub(cols_used + 1) as usize);
|
||||||
|
let prompt_line_final = format!("{}{}{}", prompt, right_pad, Self::VERT_LINE);
|
||||||
|
buf.push_str(&prompt_line_final);
|
||||||
|
rows += 1;
|
||||||
|
|
||||||
|
let sep_line_left = format!("{}{}{}/{}",
|
||||||
|
Self::TREE_LEFT,
|
||||||
|
Self::HOR_LINE.repeat(2),
|
||||||
|
&num_filtered,
|
||||||
|
&num_candidates
|
||||||
|
);
|
||||||
|
let cols_used = calc_str_width(&sep_line_left);
|
||||||
|
let right_pad = Self::HOR_LINE.repeat(cols.saturating_sub(cols_used + 1) as usize);
|
||||||
|
let sep_line_final = format!("{}{}{}", sep_line_left, right_pad, Self::TREE_RIGHT);
|
||||||
|
buf.push_str(&sep_line_final);
|
||||||
|
rows += 1;
|
||||||
|
|
||||||
|
|
||||||
for (i, candidate) in visible.iter().enumerate() {
|
for (i, candidate) in visible.iter().enumerate() {
|
||||||
buf.push_str("\n\r");
|
let selector = if i + offset == cursor_pos {
|
||||||
if i + offset == cursor_pos {
|
Self::SELECTOR_HL
|
||||||
buf.push_str("\x1b[7m");
|
|
||||||
buf.push_str(&candidate.content);
|
|
||||||
buf.push_str("\x1b[0m");
|
|
||||||
} else {
|
} else {
|
||||||
buf.push_str(&candidate.content);
|
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 left = format!("{} {}{}\x1b[0m",
|
||||||
|
Self::VERT_LINE,
|
||||||
|
&selector,
|
||||||
|
&content
|
||||||
|
);
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let bot_bar = format!("{}{}{}",
|
||||||
|
Self::BOT_LEFT,
|
||||||
|
Self::HOR_LINE.to_string().repeat(cols.saturating_sub(2) as usize),
|
||||||
|
Self::BOT_RIGHT
|
||||||
|
);
|
||||||
|
buf.push_str(&bot_bar);
|
||||||
|
rows += 1;
|
||||||
|
|
||||||
let new_layout = FuzzyLayout {
|
let new_layout = FuzzyLayout {
|
||||||
rows: visible.len() as u16, // +1 for the query line
|
rows, // +1 for the query line
|
||||||
};
|
};
|
||||||
|
|
||||||
// Move cursor back up to the query line and position after "> " + query text
|
// Move cursor back up to the prompt line (skip: separator + candidates + bottom border)
|
||||||
write!(buf, "\x1b[{}A\r\x1b[{}C", new_layout.rows, self.query.linebuf.as_str().width() + 2).unwrap();
|
let lines_below_prompt = new_layout.rows.saturating_sub(2); // total rows minus top_bar and prompt
|
||||||
|
let cursor_in_window = self.query.linebuf.cursor.get().saturating_sub(self.query.scroll_offset);
|
||||||
|
let cursor_col = cursor_in_window + 4; // "| > ".len() == 4
|
||||||
|
write!(buf, "\x1b[{}A\r\x1b[{}C", lines_below_prompt, cursor_col).unwrap();
|
||||||
writer.flush_write(&buf)?;
|
writer.flush_write(&buf)?;
|
||||||
self.old_layout = Some(new_layout);
|
self.old_layout = Some(new_layout);
|
||||||
|
|
||||||
@@ -838,6 +953,9 @@ pub struct SimpleCompleter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Completer for SimpleCompleter {
|
impl Completer for SimpleCompleter {
|
||||||
|
fn get_completed_line(&self, _candidate: &str) -> String {
|
||||||
|
self.get_completed_line()
|
||||||
|
}
|
||||||
fn complete(&mut self, line: String, cursor_pos: usize, direction: i32) -> ShResult<Option<String>> {
|
fn complete(&mut self, line: String, cursor_pos: usize, direction: i32) -> ShResult<Option<String>> {
|
||||||
if self.active {
|
if self.active {
|
||||||
Ok(Some(self.cycle_completion(direction)))
|
Ok(Some(self.cycle_completion(direction)))
|
||||||
@@ -1046,7 +1164,7 @@ impl SimpleCompleter {
|
|||||||
|
|
||||||
let cword = if let Some(pos) = relevant
|
let cword = if let Some(pos) = relevant
|
||||||
.iter()
|
.iter()
|
||||||
.position(|tk| cursor_pos >= tk.span.range().start && cursor_pos < tk.span.range().end)
|
.position(|tk| cursor_pos >= tk.span.range().start && cursor_pos <= tk.span.range().end)
|
||||||
{
|
{
|
||||||
pos
|
pos
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -338,7 +338,12 @@ pub struct LineBuf {
|
|||||||
|
|
||||||
impl LineBuf {
|
impl LineBuf {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
let mut new = Self {
|
||||||
|
grapheme_indices: Some(vec![]), // We know the buffer is empty, so this keeps us safe from unwrapping None
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
new.update_graphemes();
|
||||||
|
new
|
||||||
}
|
}
|
||||||
/// Only update self.grapheme_indices if it is None
|
/// Only update self.grapheme_indices if it is None
|
||||||
pub fn update_graphemes_lazy(&mut self) {
|
pub fn update_graphemes_lazy(&mut self) {
|
||||||
@@ -418,7 +423,17 @@ impl LineBuf {
|
|||||||
self.cursor.set_max(indices.len());
|
self.cursor.set_max(indices.len());
|
||||||
self.grapheme_indices = Some(indices)
|
self.grapheme_indices = Some(indices)
|
||||||
}
|
}
|
||||||
|
#[track_caller]
|
||||||
pub fn grapheme_indices(&self) -> &[usize] {
|
pub fn grapheme_indices(&self) -> &[usize] {
|
||||||
|
if self.grapheme_indices.is_none() {
|
||||||
|
let caller = std::panic::Location::caller();
|
||||||
|
panic!(
|
||||||
|
"grapheme_indices is None. This likely means you forgot to call update_graphemes() before calling a method that relies on grapheme_indices, or you called a method that relies on grapheme_indices from another method that also relies on grapheme_indices without updating graphemes in between. Caller: {}:{}:{}",
|
||||||
|
caller.file(),
|
||||||
|
caller.line(),
|
||||||
|
caller.column(),
|
||||||
|
);
|
||||||
|
}
|
||||||
self.grapheme_indices.as_ref().unwrap()
|
self.grapheme_indices.as_ref().unwrap()
|
||||||
}
|
}
|
||||||
pub fn grapheme_indices_owned(&self) -> Vec<usize> {
|
pub fn grapheme_indices_owned(&self) -> Vec<usize> {
|
||||||
@@ -777,6 +792,19 @@ impl LineBuf {
|
|||||||
}
|
}
|
||||||
Some(self.line_bounds(line_no))
|
Some(self.line_bounds(line_no))
|
||||||
}
|
}
|
||||||
|
pub fn this_word(&mut self, word: Word) -> (usize, usize) {
|
||||||
|
let start = if self.is_word_bound(self.cursor.get(), word, Direction::Backward) {
|
||||||
|
self.cursor.get()
|
||||||
|
} else {
|
||||||
|
self.start_of_word_backward(self.cursor.get(), word)
|
||||||
|
};
|
||||||
|
let end = if self.is_word_bound(self.cursor.get(), word, Direction::Forward) {
|
||||||
|
self.cursor.get()
|
||||||
|
} else {
|
||||||
|
self.end_of_word_forward(self.cursor.get(), word)
|
||||||
|
};
|
||||||
|
(start, end)
|
||||||
|
}
|
||||||
pub fn this_line_exclusive(&mut self) -> (usize, usize) {
|
pub fn this_line_exclusive(&mut self) -> (usize, usize) {
|
||||||
let line_no = self.cursor_line_number();
|
let line_no = self.cursor_line_number();
|
||||||
let (start, mut end) = self.line_bounds(line_no);
|
let (start, mut end) = self.line_bounds(line_no);
|
||||||
@@ -2983,6 +3011,55 @@ impl LineBuf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Verb::IncrementNumber(n) |
|
||||||
|
Verb::DecrementNumber(n) => {
|
||||||
|
let inc = if matches!(verb, Verb::IncrementNumber(_)) { n as i64 } else { -(n as i64) };
|
||||||
|
let (s, e) = self.this_word(Word::Normal);
|
||||||
|
let end = (e + 1).min(self.grapheme_indices().len()); // inclusive → exclusive, capped at buffer len
|
||||||
|
let word = self.slice(s..end).unwrap_or_default().to_lowercase();
|
||||||
|
|
||||||
|
let byte_start = self.index_byte_pos(s);
|
||||||
|
let byte_end = if end >= self.grapheme_indices().len() {
|
||||||
|
self.buffer.len()
|
||||||
|
} else {
|
||||||
|
self.index_byte_pos(end)
|
||||||
|
};
|
||||||
|
|
||||||
|
if word.starts_with("0x") {
|
||||||
|
let body = word.strip_prefix("0x").unwrap();
|
||||||
|
let width = body.len();
|
||||||
|
if let Ok(num) = i64::from_str_radix(body, 16) {
|
||||||
|
let new_num = num + inc;
|
||||||
|
self.buffer.replace_range(byte_start..byte_end, &format!("0x{new_num:0>width$x}"));
|
||||||
|
self.update_graphemes();
|
||||||
|
self.cursor.set(s);
|
||||||
|
}
|
||||||
|
} else if word.starts_with("0b") {
|
||||||
|
let body = word.strip_prefix("0b").unwrap();
|
||||||
|
let width = body.len();
|
||||||
|
if let Ok(num) = i64::from_str_radix(body, 2) {
|
||||||
|
let new_num = num + inc;
|
||||||
|
self.buffer.replace_range(byte_start..byte_end, &format!("0b{new_num:0>width$b}"));
|
||||||
|
self.update_graphemes();
|
||||||
|
self.cursor.set(s);
|
||||||
|
}
|
||||||
|
} else if word.starts_with("0o") {
|
||||||
|
let body = word.strip_prefix("0o").unwrap();
|
||||||
|
let width = body.len();
|
||||||
|
if let Ok(num) = i64::from_str_radix(body, 8) {
|
||||||
|
let new_num = num + inc;
|
||||||
|
self.buffer.replace_range(byte_start..byte_end, &format!("0o{new_num:0>width$o}"));
|
||||||
|
self.update_graphemes();
|
||||||
|
self.cursor.set(s);
|
||||||
|
}
|
||||||
|
} else if let Ok(num) = word.parse::<i64>() {
|
||||||
|
let width = word.len();
|
||||||
|
let new_num = num + inc;
|
||||||
|
self.buffer.replace_range(byte_start..byte_end, &format!("{new_num:0>width$}"));
|
||||||
|
self.update_graphemes();
|
||||||
|
self.cursor.set(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Verb::Complete
|
Verb::Complete
|
||||||
| Verb::EndOfFile
|
| Verb::EndOfFile
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use crate::libsh::sys::TTY_FILENO;
|
|||||||
use crate::parse::lex::{LexStream, QuoteState};
|
use crate::parse::lex::{LexStream, QuoteState};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::readline::complete::FuzzyCompleter;
|
use crate::readline::complete::FuzzyCompleter;
|
||||||
use crate::readline::term::{Pos, calc_str_width};
|
use crate::readline::term::{Pos, TermReader, calc_str_width};
|
||||||
use crate::state::{ShellParam, read_shopts};
|
use crate::state::{ShellParam, read_shopts};
|
||||||
use crate::{
|
use crate::{
|
||||||
libsh::error::ShResult,
|
libsh::error::ShResult,
|
||||||
@@ -259,6 +259,10 @@ impl ShedVi {
|
|||||||
self.needs_redraw = true;
|
self.needs_redraw = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn fix_column(&mut self) -> ShResult<()> {
|
||||||
|
self.writer.fix_cursor_column(&mut TermReader::new(*TTY_FILENO))
|
||||||
|
}
|
||||||
|
|
||||||
/// Reset readline state for a new prompt
|
/// Reset readline state for a new prompt
|
||||||
pub fn reset(&mut self, full_redraw: bool) -> ShResult<()> {
|
pub fn reset(&mut self, full_redraw: bool) -> ShResult<()> {
|
||||||
// Clear old display before resetting state — old_layout must survive
|
// Clear old display before resetting state — old_layout must survive
|
||||||
@@ -327,6 +331,8 @@ impl ShedVi {
|
|||||||
let span_start = self.completer.token_span().0;
|
let span_start = self.completer.token_span().0;
|
||||||
let new_cursor = span_start + candidate.len();
|
let new_cursor = span_start + candidate.len();
|
||||||
let line = self.completer.get_completed_line(&candidate);
|
let line = self.completer.get_completed_line(&candidate);
|
||||||
|
log::debug!("Completer accepted candidate: {candidate}");
|
||||||
|
log::debug!("New line after completion: {line}");
|
||||||
self.editor.set_buffer(line);
|
self.editor.set_buffer(line);
|
||||||
self.editor.cursor.set(new_cursor);
|
self.editor.cursor.set(new_cursor);
|
||||||
// Don't reset yet — clear() needs old_layout to erase the selector.
|
// Don't reset yet — clear() needs old_layout to erase the selector.
|
||||||
@@ -341,12 +347,12 @@ impl ShedVi {
|
|||||||
self.editor.set_hint(hint);
|
self.editor.set_hint(hint);
|
||||||
self.completer.clear(&mut self.writer)?;
|
self.completer.clear(&mut self.writer)?;
|
||||||
self.needs_redraw = true;
|
self.needs_redraw = true;
|
||||||
|
self.completer.reset();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
CompResponse::Dismiss => {
|
CompResponse::Dismiss => {
|
||||||
self.completer.clear(&mut self.writer)?;
|
self.completer.clear(&mut self.writer)?;
|
||||||
// Don't reset yet — clear() needs old_layout to erase the selector.
|
self.completer.reset();
|
||||||
// The next print_line() will call clear(), then we can reset.
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
CompResponse::Consumed => {
|
CompResponse::Consumed => {
|
||||||
|
|||||||
@@ -828,6 +828,48 @@ impl TermWriter {
|
|||||||
self.t_cols = t_cols;
|
self.t_cols = t_cols;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Called before the prompt is drawn. If we are not on column 1, push a vid-inverted '%' and then a '\n\r'.
|
||||||
|
///
|
||||||
|
/// Aping zsh with this but it's a nice feature.
|
||||||
|
pub fn fix_cursor_column(&mut self, rdr: &mut TermReader) -> ShResult<()> {
|
||||||
|
let Some((_,c)) = self.get_cursor_pos(rdr)? else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
if c != 1 {
|
||||||
|
self.flush_write("\x1b[7m%\x1b[0m\n\r")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_cursor_pos(&mut self, rdr: &mut TermReader) -> ShResult<Option<(usize, usize)>> {
|
||||||
|
// Ping the cursor's position
|
||||||
|
self.flush_write("\x1b[6n")?;
|
||||||
|
|
||||||
|
if !rdr.poll(PollTimeout::from(255u8))? {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
if rdr.next_byte()? as char != '\x1b' {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
if rdr.next_byte()? as char != '[' {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let row = read_digits_until(rdr, ';')?;
|
||||||
|
|
||||||
|
let col = read_digits_until(rdr, 'R')?;
|
||||||
|
let pos = if let Some(row) = row && let Some(col) = col {
|
||||||
|
Some((row as usize, col as usize))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(pos)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn move_cursor_at_leftmost(
|
pub fn move_cursor_at_leftmost(
|
||||||
&mut self,
|
&mut self,
|
||||||
rdr: &mut TermReader,
|
rdr: &mut TermReader,
|
||||||
|
|||||||
@@ -218,6 +218,8 @@ pub enum Verb {
|
|||||||
ReplaceCharInplace(char, u16), // char to replace with, number of chars to replace
|
ReplaceCharInplace(char, u16), // char to replace with, number of chars to replace
|
||||||
ToggleCaseInplace(u16), // Number of chars to toggle
|
ToggleCaseInplace(u16), // Number of chars to toggle
|
||||||
ToggleCaseRange,
|
ToggleCaseRange,
|
||||||
|
IncrementNumber(u16),
|
||||||
|
DecrementNumber(u16),
|
||||||
ToLower,
|
ToLower,
|
||||||
ToUpper,
|
ToUpper,
|
||||||
Complete,
|
Complete,
|
||||||
|
|||||||
@@ -1025,6 +1025,29 @@ impl ViMode for ViNormal {
|
|||||||
raw_seq: "".into(),
|
raw_seq: "".into(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
}),
|
}),
|
||||||
|
E(K::Char('A'), M::CTRL) => {
|
||||||
|
let count = self.parse_count(&mut self.pending_seq.chars().peekable()).unwrap_or(1) as u16;
|
||||||
|
self.pending_seq.clear();
|
||||||
|
Some(ViCmd {
|
||||||
|
register: Default::default(),
|
||||||
|
verb: Some(VerbCmd(1, Verb::IncrementNumber(count))),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: "".into(),
|
||||||
|
flags: self.flags(),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
E(K::Char('X'), M::CTRL) => {
|
||||||
|
let count = self.parse_count(&mut self.pending_seq.chars().peekable()).unwrap_or(1) as u16;
|
||||||
|
self.pending_seq.clear();
|
||||||
|
Some(ViCmd {
|
||||||
|
register: Default::default(),
|
||||||
|
verb: Some(VerbCmd(1, Verb::DecrementNumber(count))),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: "".into(),
|
||||||
|
flags: self.flags(),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
E(K::Char(ch), M::NONE) => self.try_parse(ch),
|
E(K::Char(ch), M::NONE) => self.try_parse(ch),
|
||||||
E(K::Backspace, M::NONE) => Some(ViCmd {
|
E(K::Backspace, M::NONE) => Some(ViCmd {
|
||||||
register: Default::default(),
|
register: Default::default(),
|
||||||
|
|||||||
Reference in New Issue
Block a user