Compare commits
2 Commits
0e3f2afe99
...
397c9cae52
| Author | SHA1 | Date | |
|---|---|---|---|
| 397c9cae52 | |||
| 9223e4848d |
@@ -29,13 +29,13 @@ The prompt string supports escape sequences for dynamic content:
|
|||||||
| `\t`, `\T` | Last command runtime (milliseconds / human-readable) |
|
| `\t`, `\T` | Last command runtime (milliseconds / human-readable) |
|
||||||
| `\s` | Shell name |
|
| `\s` | Shell name |
|
||||||
| `\e[...` | ANSI escape sequences for colors and styling |
|
| `\e[...` | ANSI escape sequences for colors and styling |
|
||||||
| `\!name` | Execute a shell function and embed its output |
|
| `\@name` | Execute a shell function and embed its output |
|
||||||
|
|
||||||
The `\!` escape is particularly useful. It lets you embed the output of any shell function directly in your prompt. Define a function that prints something, then reference it in your prompt string:
|
The `\@` escape is particularly useful. It lets you embed the output of any shell function directly in your prompt. Define a function that prints something, then reference it in your prompt string:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
gitbranch() { git branch --show-current 2>/dev/null; }
|
gitbranch() { git branch --show-current 2>/dev/null; }
|
||||||
export PS1='\u@\h \W \!gitbranch \$ '
|
export PS1='\u@\h \W \@gitbranch \$ '
|
||||||
```
|
```
|
||||||
|
|
||||||
Additionally, `echo` now has a `-p` flag that expands prompt escape sequences, similar to how the `-e` flag expands conventional escape sequences.
|
Additionally, `echo` now has a `-p` flag that expands prompt escape sequences, similar to how the `-e` flag expands conventional escape sequences.
|
||||||
|
|||||||
@@ -81,6 +81,12 @@ in
|
|||||||
"pre-mode-change"
|
"pre-mode-change"
|
||||||
"post-mode-change"
|
"post-mode-change"
|
||||||
"on-exit"
|
"on-exit"
|
||||||
|
"on-history-open"
|
||||||
|
"on-history-close"
|
||||||
|
"on-history-select"
|
||||||
|
"on-completion-start"
|
||||||
|
"on-completion-cancel"
|
||||||
|
"on-completion-select"
|
||||||
])) (list: list != []);
|
])) (list: list != []);
|
||||||
description = "The events that trigger this autocmd";
|
description = "The events that trigger this autocmd";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -111,11 +111,6 @@ impl KeyMap {
|
|||||||
expand_keymap(&self.action)
|
expand_keymap(&self.action)
|
||||||
}
|
}
|
||||||
pub fn compare(&self, other: &[KeyEvent]) -> KeyMapMatch {
|
pub fn compare(&self, other: &[KeyEvent]) -> KeyMapMatch {
|
||||||
log::debug!(
|
|
||||||
"Comparing keymap keys {:?} with input {:?}",
|
|
||||||
self.keys_expanded(),
|
|
||||||
other
|
|
||||||
);
|
|
||||||
let ours = self.keys_expanded();
|
let ours = self.keys_expanded();
|
||||||
if other == ours {
|
if other == ours {
|
||||||
KeyMapMatch::IsExact
|
KeyMapMatch::IsExact
|
||||||
|
|||||||
@@ -1033,7 +1033,7 @@ pub fn unescape_str(raw: &str) -> String {
|
|||||||
'\\' => {
|
'\\' => {
|
||||||
if let Some(next_ch) = chars.next() {
|
if let Some(next_ch) = chars.next() {
|
||||||
match next_ch {
|
match next_ch {
|
||||||
'"' | '\\' | '`' | '$' => {
|
'"' | '\\' | '`' | '$' | '!' => {
|
||||||
// discard the backslash
|
// discard the backslash
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
@@ -1076,7 +1076,6 @@ pub fn unescape_str(raw: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
'$' => {
|
'$' => {
|
||||||
log::debug!("Found ANSI-C quoting");
|
|
||||||
chars.next();
|
chars.next();
|
||||||
while let Some(q_ch) = chars.next() {
|
while let Some(q_ch) = chars.next() {
|
||||||
match q_ch {
|
match q_ch {
|
||||||
@@ -1232,7 +1231,6 @@ pub fn unescape_str(raw: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
'$' if chars.peek() == Some(&'\'') => {
|
'$' if chars.peek() == Some(&'\'') => {
|
||||||
log::debug!("Found ANSI-C quoting");
|
|
||||||
chars.next();
|
chars.next();
|
||||||
result.push(markers::SNG_QUOTE);
|
result.push(markers::SNG_QUOTE);
|
||||||
while let Some(q_ch) = chars.next() {
|
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}
|
// Handle indirect var expansion: ${!var}
|
||||||
if let Some(var) = s.strip_prefix('!') {
|
if let Some(var) = s.strip_prefix('!') {
|
||||||
@@ -1423,7 +1420,6 @@ impl FromStr for ParamExp {
|
|||||||
return Ok(RemShortestPrefix(rest.to_string()));
|
return Ok(RemShortestPrefix(rest.to_string()));
|
||||||
}
|
}
|
||||||
if let Some(rest) = s.strip_prefix("%%") {
|
if let Some(rest) = s.strip_prefix("%%") {
|
||||||
log::debug!("Matched longest suffix pattern: '{}'", rest);
|
|
||||||
return Ok(RemLongestSuffix(rest.to_string()));
|
return Ok(RemLongestSuffix(rest.to_string()));
|
||||||
} else if let Some(rest) = s.strip_prefix('%') {
|
} else if let Some(rest) = s.strip_prefix('%') {
|
||||||
return Ok(RemShortestSuffix(rest.to_string()));
|
return Ok(RemShortestSuffix(rest.to_string()));
|
||||||
@@ -2041,7 +2037,7 @@ fn tokenize_prompt(raw: &str) -> Vec<PromptTk> {
|
|||||||
'\'' => tokens.push(PromptTk::Text("'".into())),
|
'\'' => tokens.push(PromptTk::Text("'".into())),
|
||||||
'(' => tokens.push(PromptTk::VisGroupOpen),
|
'(' => tokens.push(PromptTk::VisGroupOpen),
|
||||||
')' => tokens.push(PromptTk::VisGroupClose),
|
')' => tokens.push(PromptTk::VisGroupClose),
|
||||||
'!' => {
|
'@' => {
|
||||||
let mut func_name = String::new();
|
let mut func_name = String::new();
|
||||||
let is_braced = chars.peek() == Some(&'{');
|
let is_braced = chars.peek() == Some(&'{');
|
||||||
let mut handled = false;
|
let mut handled = false;
|
||||||
@@ -2060,14 +2056,14 @@ fn tokenize_prompt(raw: &str) -> Vec<PromptTk> {
|
|||||||
handled = true;
|
handled = true;
|
||||||
if is_braced {
|
if is_braced {
|
||||||
// Invalid character in braced function name
|
// Invalid character in braced function name
|
||||||
tokens.push(PromptTk::Text(format!("\\!{{{func_name}")));
|
tokens.push(PromptTk::Text(format!("\\@{{{func_name}")));
|
||||||
} else {
|
} else {
|
||||||
// End of unbraced function name
|
// End of unbraced function name
|
||||||
let func_exists = read_logic(|l| l.get_func(&func_name).is_some());
|
let func_exists = read_logic(|l| l.get_func(&func_name).is_some());
|
||||||
if func_exists {
|
if func_exists {
|
||||||
tokens.push(PromptTk::Function(func_name.clone()));
|
tokens.push(PromptTk::Function(func_name.clone()));
|
||||||
} else {
|
} else {
|
||||||
tokens.push(PromptTk::Text(format!("\\!{func_name}")));
|
tokens.push(PromptTk::Text(format!("\\@{func_name}")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -2080,7 +2076,7 @@ fn tokenize_prompt(raw: &str) -> Vec<PromptTk> {
|
|||||||
if func_exists {
|
if func_exists {
|
||||||
tokens.push(PromptTk::Function(func_name));
|
tokens.push(PromptTk::Function(func_name));
|
||||||
} else {
|
} else {
|
||||||
tokens.push(PromptTk::Text(format!("\\!{func_name}")));
|
tokens.push(PromptTk::Text(format!("\\@{func_name}")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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'),
|
"CR" => KeyCode::Char('\r'),
|
||||||
"ENTER" | "RETURN" => KeyCode::Enter,
|
"ENTER" | "RETURN" => KeyCode::Enter,
|
||||||
"ESC" | "ESCAPE" => KeyCode::Esc,
|
"ESC" | "ESCAPE" => KeyCode::Esc,
|
||||||
@@ -2378,6 +2374,7 @@ pub fn parse_key_alias(alias: &str) -> Option<KeyEvent> {
|
|||||||
"RIGHT" => KeyCode::Right,
|
"RIGHT" => KeyCode::Right,
|
||||||
"HOME" => KeyCode::Home,
|
"HOME" => KeyCode::Home,
|
||||||
"END" => KeyCode::End,
|
"END" => KeyCode::End,
|
||||||
|
"CMD" => KeyCode::ExMode,
|
||||||
"PGUP" | "PAGEUP" => KeyCode::PageUp,
|
"PGUP" | "PAGEUP" => KeyCode::PageUp,
|
||||||
"PGDN" | "PAGEDOWN" => KeyCode::PageDown,
|
"PGDN" | "PAGEDOWN" => KeyCode::PageDown,
|
||||||
k if k.len() == 1 => KeyCode::Char(k.chars().next().unwrap()),
|
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
|
Consumed, // key was handled, but completion remains active
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum SelectorResponse {
|
||||||
|
Accept(String),
|
||||||
|
Dismiss,
|
||||||
|
Consumed,
|
||||||
|
}
|
||||||
|
|
||||||
pub trait Completer {
|
pub trait Completer {
|
||||||
fn complete(
|
fn complete(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -702,23 +708,28 @@ impl QueryEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct FuzzyCompleter {
|
pub struct FuzzySelector {
|
||||||
completer: SimpleCompleter,
|
|
||||||
query: QueryEditor,
|
query: QueryEditor,
|
||||||
filtered: Vec<ScoredCandidate>,
|
filtered: Vec<ScoredCandidate>,
|
||||||
candidates: Vec<String>,
|
candidates: Vec<String>,
|
||||||
cursor: ClampedUsize,
|
cursor: ClampedUsize,
|
||||||
|
number_candidates: bool,
|
||||||
old_layout: Option<FuzzyLayout>,
|
old_layout: Option<FuzzyLayout>,
|
||||||
max_height: usize,
|
max_height: usize,
|
||||||
scroll_offset: usize,
|
scroll_offset: usize,
|
||||||
active: bool,
|
active: bool,
|
||||||
/// Context from the prompt: width of the line above the fuzzy window
|
|
||||||
prompt_line_width: u16,
|
prompt_line_width: u16,
|
||||||
/// Context from the prompt: cursor column on the line above the fuzzy window
|
|
||||||
prompt_cursor_col: u16,
|
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_LEFT: &str = "\x1b[90m╰\x1b[0m";
|
||||||
const BOT_RIGHT: &str = "\x1b[90m╯\x1b[0m";
|
const BOT_RIGHT: &str = "\x1b[90m╯\x1b[0m";
|
||||||
const TOP_LEFT: &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 PROMPT_ARROW: &str = "\x1b[1;36m>\x1b[0m";
|
||||||
const TREE_LEFT: &str = "\x1b[90m├\x1b[0m";
|
const TREE_LEFT: &str = "\x1b[90m├\x1b[0m";
|
||||||
const TREE_RIGHT: &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";
|
pub fn new(title: impl Into<String>) -> Self {
|
||||||
//const CROSS: &str = "\x1b[90m┼\x1b[0m";
|
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] {
|
fn get_window(&mut self) -> &[ScoredCandidate] {
|
||||||
let height = self.filtered.len().min(self.max_height);
|
|
||||||
|
|
||||||
self.update_scroll_offset();
|
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) {
|
pub fn update_scroll_offset(&mut self) {
|
||||||
let height = self.filtered.len().min(self.max_height);
|
let cursor = self.cursor.get();
|
||||||
if self.cursor.get() < self.scroll_offset + 1 {
|
|
||||||
self.scroll_offset = self.cursor.ret_sub(1);
|
// 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
|
if lines <= self.max_height || self.scroll_offset >= cursor {
|
||||||
.scroll_offset
|
break;
|
||||||
.min(self.filtered.len().saturating_sub(height));
|
|
||||||
}
|
}
|
||||||
|
self.scroll_offset += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn score_candidates(&mut self) {
|
pub fn score_candidates(&mut self) {
|
||||||
let mut scored: Vec<_> = self
|
let mut scored: Vec<_> = self
|
||||||
.candidates
|
.candidates
|
||||||
@@ -769,83 +873,13 @@ impl FuzzyCompleter {
|
|||||||
self.cursor.set_max(scored.len());
|
self.cursor.set_max(scored.len());
|
||||||
self.filtered = scored;
|
self.filtered = scored;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for FuzzyCompleter {
|
pub fn handle_key(&mut self, key: K) -> ShResult<SelectorResponse> {
|
||||||
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> {
|
|
||||||
match key {
|
match key {
|
||||||
K(C::Char('D'), M::CTRL) | K(C::Esc, M::NONE) => {
|
K(C::Char('D'), M::CTRL) | K(C::Esc, M::NONE) => {
|
||||||
self.active = false;
|
self.active = false;
|
||||||
self.filtered.clear();
|
self.filtered.clear();
|
||||||
Ok(CompResponse::Dismiss)
|
Ok(SelectorResponse::Dismiss)
|
||||||
}
|
}
|
||||||
K(C::Enter, M::NONE) => {
|
K(C::Enter, M::NONE) => {
|
||||||
self.active = false;
|
self.active = false;
|
||||||
@@ -854,76 +888,30 @@ impl Completer for FuzzyCompleter {
|
|||||||
.get(self.cursor.get())
|
.get(self.cursor.get())
|
||||||
.map(|c| c.content.clone())
|
.map(|c| c.content.clone())
|
||||||
{
|
{
|
||||||
Ok(CompResponse::Accept(selected))
|
Ok(SelectorResponse::Accept(selected))
|
||||||
} else {
|
} else {
|
||||||
Ok(CompResponse::Dismiss)
|
Ok(SelectorResponse::Dismiss)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
K(C::Tab, M::SHIFT) | K(C::Up, M::NONE) => {
|
K(C::Tab, M::SHIFT) | K(C::Up, M::NONE) => {
|
||||||
self.cursor.wrap_sub(1);
|
self.cursor.wrap_sub(1);
|
||||||
self.update_scroll_offset();
|
self.update_scroll_offset();
|
||||||
Ok(CompResponse::Consumed)
|
Ok(SelectorResponse::Consumed)
|
||||||
}
|
}
|
||||||
K(C::Tab, M::NONE) | K(C::Down, M::NONE) => {
|
K(C::Tab, M::NONE) | K(C::Down, M::NONE) => {
|
||||||
self.cursor.wrap_add(1);
|
self.cursor.wrap_add(1);
|
||||||
self.update_scroll_offset();
|
self.update_scroll_offset();
|
||||||
Ok(CompResponse::Consumed)
|
Ok(SelectorResponse::Consumed)
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
self.query.handle_key(key)?;
|
self.query.handle_key(key)?;
|
||||||
self.score_candidates();
|
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
|
pub fn draw(&mut self, writer: &mut TermWriter) -> ShResult<()> {
|
||||||
// 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<()> {
|
|
||||||
if !self.active {
|
if !self.active {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -939,13 +927,19 @@ impl Completer for FuzzyCompleter {
|
|||||||
let query = self.query.get_window();
|
let query = self.query.get_window();
|
||||||
let num_filtered = format!("\x1b[33m{}\x1b[0m", self.filtered.len());
|
let num_filtered = format!("\x1b[33m{}\x1b[0m", self.filtered.len());
|
||||||
let num_candidates = format!("\x1b[33m{}\x1b[0m", self.candidates.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 visible = self.get_window();
|
||||||
let mut rows: u16 = 0;
|
let mut rows: u16 = 0;
|
||||||
let top_bar = format!(
|
let top_bar = format!(
|
||||||
"\n{}{} \x1b[1mComplete\x1b[0m {}{}",
|
"\n{}{} \x1b[1m{}\x1b[0m {}{}",
|
||||||
Self::TOP_LEFT,
|
Self::TOP_LEFT,
|
||||||
Self::HOR_LINE,
|
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
|
Self::TOP_RIGHT
|
||||||
);
|
);
|
||||||
buf.push_str(&top_bar);
|
buf.push_str(&top_bar);
|
||||||
@@ -972,24 +966,51 @@ impl Completer for FuzzyCompleter {
|
|||||||
buf.push_str(&sep_line_final);
|
buf.push_str(&sep_line_final);
|
||||||
rows += 1;
|
rows += 1;
|
||||||
|
|
||||||
|
let mut lines_drawn = 0;
|
||||||
for (i, candidate) in visible.iter().enumerate() {
|
for (i, candidate) in visible.iter().enumerate() {
|
||||||
|
if lines_drawn >= max_height {
|
||||||
|
break;
|
||||||
|
}
|
||||||
let selector = if i + offset == cursor_pos {
|
let selector = if i + offset == cursor_pos {
|
||||||
Self::SELECTOR_HL
|
Self::SELECTOR_HL
|
||||||
} else {
|
} else {
|
||||||
Self::SELECTOR_GRAY
|
Self::SELECTOR_GRAY
|
||||||
};
|
};
|
||||||
let mut content = candidate.content.clone();
|
let mut drew_number = false;
|
||||||
let col_lim = cols.saturating_sub(3);
|
for line in candidate.content.trim_end().lines() {
|
||||||
if calc_str_width(&content) > col_lim {
|
if lines_drawn >= max_height {
|
||||||
content.truncate(col_lim.saturating_sub(6) as usize); // ui bars + elipses length
|
break;
|
||||||
content.push_str("...");
|
|
||||||
}
|
}
|
||||||
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 cols_used = calc_str_width(&left);
|
||||||
let right_pad = " ".repeat(cols.saturating_sub(cols_used + 1) as usize);
|
let right_pad = " ".repeat(cols.saturating_sub(cols_used + 1) as usize);
|
||||||
let hl_cand_line = format!("{}{}{}", left, right_pad, Self::VERT_LINE);
|
let hl_cand_line = format!("{}{}{}", left, right_pad, Self::VERT_LINE);
|
||||||
buf.push_str(&hl_cand_line);
|
buf.push_str(&hl_cand_line);
|
||||||
rows += 1;
|
rows += 1;
|
||||||
|
drew_number = true;
|
||||||
|
lines_drawn += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let bot_bar = format!(
|
let bot_bar = format!(
|
||||||
@@ -1003,15 +1024,14 @@ impl Completer for FuzzyCompleter {
|
|||||||
buf.push_str(&bot_bar);
|
buf.push_str(&bot_bar);
|
||||||
rows += 1;
|
rows += 1;
|
||||||
|
|
||||||
// Move cursor back up to the prompt line (skip: separator + candidates + bottom border)
|
let lines_below_prompt = rows.saturating_sub(2);
|
||||||
let lines_below_prompt = rows.saturating_sub(2); // total rows minus top_bar and prompt
|
|
||||||
let cursor_in_window = self
|
let cursor_in_window = self
|
||||||
.query
|
.query
|
||||||
.linebuf
|
.linebuf
|
||||||
.cursor
|
.cursor
|
||||||
.get()
|
.get()
|
||||||
.saturating_sub(self.query.scroll_offset);
|
.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();
|
write!(buf, "\x1b[{}A\r\x1b[{}C", lines_below_prompt, cursor_col).unwrap();
|
||||||
|
|
||||||
let new_layout = FuzzyLayout {
|
let new_layout = FuzzyLayout {
|
||||||
@@ -1026,20 +1046,129 @@ impl Completer for FuzzyCompleter {
|
|||||||
|
|
||||||
Ok(())
|
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) {
|
fn reset(&mut self) {
|
||||||
*self = Self::default();
|
self.completer.reset();
|
||||||
|
self.selector.reset();
|
||||||
}
|
}
|
||||||
fn token_span(&self) -> (usize, usize) {
|
fn token_span(&self) -> (usize, usize) {
|
||||||
self.completer.token_span()
|
self.completer.token_span()
|
||||||
}
|
}
|
||||||
fn is_active(&self) -> bool {
|
fn is_active(&self) -> bool {
|
||||||
self.active
|
self.selector.is_active()
|
||||||
}
|
}
|
||||||
fn selected_candidate(&self) -> Option<String> {
|
fn selected_candidate(&self) -> Option<String> {
|
||||||
self
|
self.selector.selected_candidate()
|
||||||
.filtered
|
|
||||||
.get(self.cursor.get())
|
|
||||||
.map(|c| c.content.clone())
|
|
||||||
}
|
}
|
||||||
fn original_input(&self) -> &str {
|
fn original_input(&self) -> &str {
|
||||||
&self.completer.original_input
|
&self.completer.original_input
|
||||||
|
|||||||
@@ -105,14 +105,17 @@ impl Highlighter {
|
|||||||
self.in_selection = false;
|
self.in_selection = false;
|
||||||
}
|
}
|
||||||
_ if self.only_hl_visual => {
|
_ if self.only_hl_visual => {
|
||||||
|
if !is_marker(ch) {
|
||||||
self.output.push(ch);
|
self.output.push(ch);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
markers::STRING_DQ_END
|
markers::STRING_DQ_END
|
||||||
| markers::STRING_SQ_END
|
| markers::STRING_SQ_END
|
||||||
| markers::VAR_SUB_END
|
| markers::VAR_SUB_END
|
||||||
| markers::CMD_SUB_END
|
| markers::CMD_SUB_END
|
||||||
| markers::PROC_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(),
|
markers::CMD_SEP | markers::RESET => self.clear_styles(),
|
||||||
|
|
||||||
@@ -276,6 +279,23 @@ impl Highlighter {
|
|||||||
}
|
}
|
||||||
self.last_was_reset = false;
|
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 => {
|
markers::VAR_SUB => {
|
||||||
let mut var_sub = String::new();
|
let mut var_sub = String::new();
|
||||||
while let Some(ch) = input_chars.peek() {
|
while let Some(ch) = input_chars.peek() {
|
||||||
|
|||||||
@@ -1,17 +1,10 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
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}
|
||||||
env,
|
|
||||||
fmt::{Display, Write},
|
|
||||||
fs::{self, OpenOptions},
|
|
||||||
io::Write as IoWrite,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
str::FromStr,
|
|
||||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||||
readline::linebuf::LineBuf,
|
readline::{complete::FuzzySelector, linebuf::LineBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Default, Clone, Copy, Debug)]
|
#[derive(Default, Clone, Copy, Debug)]
|
||||||
@@ -207,6 +200,7 @@ pub struct History {
|
|||||||
pub pending: Option<LineBuf>, // command, cursor_pos
|
pub pending: Option<LineBuf>, // command, cursor_pos
|
||||||
entries: Vec<HistEntry>,
|
entries: Vec<HistEntry>,
|
||||||
search_mask: Vec<HistEntry>,
|
search_mask: Vec<HistEntry>,
|
||||||
|
pub fuzzy_finder: FuzzySelector,
|
||||||
no_matches: bool,
|
no_matches: bool,
|
||||||
pub cursor: usize,
|
pub cursor: usize,
|
||||||
//search_direction: Direction,
|
//search_direction: Direction,
|
||||||
@@ -232,6 +226,7 @@ impl History {
|
|||||||
Ok(Self {
|
Ok(Self {
|
||||||
path,
|
path,
|
||||||
entries,
|
entries,
|
||||||
|
fuzzy_finder: FuzzySelector::new("History").number_candidates(true),
|
||||||
pending: None,
|
pending: None,
|
||||||
search_mask,
|
search_mask,
|
||||||
no_matches: false,
|
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) {
|
pub fn reset(&mut self) {
|
||||||
self.search_mask = dedupe_entries(&self.entries);
|
self.search_mask = dedupe_entries(&self.entries);
|
||||||
self.cursor = self.search_mask.len();
|
self.cursor = self.search_mask.len();
|
||||||
@@ -291,6 +300,36 @@ impl History {
|
|||||||
pub fn last_mut(&mut self) -> Option<&mut HistEntry> {
|
pub fn last_mut(&mut self) -> Option<&mut HistEntry> {
|
||||||
self.entries.last_mut()
|
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 {
|
pub fn get_new_id(&self) -> u32 {
|
||||||
let Some(ent) = self.entries.last() else {
|
let Some(ent) = self.entries.last() else {
|
||||||
|
|||||||
@@ -114,6 +114,10 @@ impl KeyEvent {
|
|||||||
"Cannot convert unknown escape sequence to Vim key sequence".to_string(),
|
"Cannot convert unknown escape sequence to Vim key sequence".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
KeyCode::ExMode => {
|
||||||
|
seq.push_str("CMD");
|
||||||
|
needs_angle_bracket = true;
|
||||||
|
}
|
||||||
KeyCode::Backspace => {
|
KeyCode::Backspace => {
|
||||||
seq.push_str("BS");
|
seq.push_str("BS");
|
||||||
needs_angle_bracket = true;
|
needs_angle_bracket = true;
|
||||||
@@ -222,6 +226,9 @@ pub enum KeyCode {
|
|||||||
Right,
|
Right,
|
||||||
Tab,
|
Tab,
|
||||||
Up,
|
Up,
|
||||||
|
|
||||||
|
// weird stuff
|
||||||
|
ExMode, // keycode emitted by the <cmd> byte alias in vim keymaps
|
||||||
}
|
}
|
||||||
|
|
||||||
bitflags::bitflags! {
|
bitflags::bitflags! {
|
||||||
|
|||||||
@@ -15,13 +15,11 @@ use crate::{
|
|||||||
libsh::{error::ShResult, guards::var_ctx_guard},
|
libsh::{error::ShResult, guards::var_ctx_guard},
|
||||||
parse::{
|
parse::{
|
||||||
execute::exec_input,
|
execute::exec_input,
|
||||||
lex::{LexFlags, LexStream, Tk, TkFlags, TkRule},
|
lex::{LexFlags, LexStream, QuoteState, Tk, TkFlags, TkRule},
|
||||||
},
|
},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
readline::{
|
readline::{
|
||||||
markers,
|
history::History, markers, register::{RegisterContent, write_register}, term::RawModeGuard
|
||||||
register::{RegisterContent, write_register},
|
|
||||||
term::RawModeGuard,
|
|
||||||
},
|
},
|
||||||
state::{VarFlags, VarKind, read_shopts, write_meta, write_vars},
|
state::{VarFlags, VarKind, read_shopts, write_meta, write_vars},
|
||||||
};
|
};
|
||||||
@@ -299,6 +297,13 @@ impl ClampedUsize {
|
|||||||
let max = self.upper_bound();
|
let max = self.upper_bound();
|
||||||
self.value = (self.value + value).clamp(0, max)
|
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) {
|
pub fn sub(&mut self, value: usize) {
|
||||||
self.value = self.value.saturating_sub(value)
|
self.value = self.value.saturating_sub(value)
|
||||||
}
|
}
|
||||||
@@ -645,6 +650,10 @@ impl LineBuf {
|
|||||||
self.buffer.push_str(slice);
|
self.buffer.push_str(slice);
|
||||||
self.update_graphemes();
|
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) {
|
pub fn insert_at_cursor(&mut self, ch: char) {
|
||||||
self.insert_at(self.cursor.get(), ch);
|
self.insert_at(self.cursor.get(), ch);
|
||||||
}
|
}
|
||||||
@@ -2893,7 +2902,7 @@ impl LineBuf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Verb::Insert(string) => {
|
Verb::Insert(string) => {
|
||||||
self.push_str(&string);
|
self.insert_str_at_cursor(&string);
|
||||||
let graphemes = string.graphemes(true).count();
|
let graphemes = string.graphemes(true).count();
|
||||||
log::debug!("Inserted string: {string:?}, graphemes: {graphemes}");
|
log::debug!("Inserted string: {string:?}, graphemes: {graphemes}");
|
||||||
log::debug!("buffer after insert: {:?}", self.buffer);
|
log::debug!("buffer after insert: {:?}", self.buffer);
|
||||||
@@ -3317,6 +3326,73 @@ impl LineBuf {
|
|||||||
|
|
||||||
Ok(())
|
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 {
|
pub fn as_str(&self) -> &str {
|
||||||
&self.buffer // FIXME: this will have to be fixed up later
|
&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::sys::TTY_FILENO;
|
||||||
use crate::libsh::utils::AutoCmdVecUtils;
|
use crate::libsh::utils::AutoCmdVecUtils;
|
||||||
use crate::parse::lex::{LexStream, QuoteState};
|
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::term::{Pos, TermReader, calc_str_width};
|
||||||
use crate::readline::vimode::{ViEx, ViVerbatim};
|
use crate::readline::vimode::{ViEx, ViVerbatim};
|
||||||
use crate::state::{
|
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::{
|
use crate::{
|
||||||
libsh::error::ShResult,
|
libsh::error::ShResult,
|
||||||
@@ -76,6 +76,8 @@ pub mod markers {
|
|||||||
pub const STRING_SQ_END: Marker = '\u{e115}';
|
pub const STRING_SQ_END: Marker = '\u{e115}';
|
||||||
pub const ESCAPE: Marker = '\u{e116}';
|
pub const ESCAPE: Marker = '\u{e116}';
|
||||||
pub const GLOB: Marker = '\u{e117}';
|
pub const GLOB: Marker = '\u{e117}';
|
||||||
|
pub const HIST_EXP: Marker = '\u{e11c}';
|
||||||
|
pub const HIST_EXP_END: Marker = '\u{e11d}';
|
||||||
|
|
||||||
// other
|
// other
|
||||||
pub const VISUAL_MODE_START: Marker = '\u{e118}';
|
pub const VISUAL_MODE_START: Marker = '\u{e118}';
|
||||||
@@ -409,11 +411,51 @@ impl ShedVi {
|
|||||||
|
|
||||||
// Process all available keys
|
// Process all available keys
|
||||||
while let Some(key) = self.reader.read_key()? {
|
while let Some(key) = self.reader.read_key()? {
|
||||||
// If completer is active, delegate input to it
|
// If completer or history search are active, delegate input to it
|
||||||
if self.completer.is_active() {
|
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)?;
|
self.print_line(false)?;
|
||||||
match self.completer.handle_key(key.clone())? {
|
match self.completer.handle_key(key.clone())? {
|
||||||
CompResponse::Accept(candidate) => {
|
CompResponse::Accept(candidate) => {
|
||||||
|
let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnCompletionSelect));
|
||||||
|
|
||||||
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);
|
||||||
@@ -432,12 +474,22 @@ impl ShedVi {
|
|||||||
self.completer.clear(&mut self.writer)?;
|
self.completer.clear(&mut self.writer)?;
|
||||||
self.needs_redraw = true;
|
self.needs_redraw = true;
|
||||||
self.completer.reset();
|
self.completer.reset();
|
||||||
|
|
||||||
|
with_vars([("_COMP_CANDIDATE".into(), candidate.clone())], || {
|
||||||
|
post_cmds.exec_with(&candidate);
|
||||||
|
});
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
CompResponse::Dismiss => {
|
CompResponse::Dismiss => {
|
||||||
|
let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnCompletionCancel));
|
||||||
|
post_cmds.exec();
|
||||||
|
|
||||||
let hint = self.history.get_hint();
|
let hint = self.history.get_hint();
|
||||||
self.editor.set_hint(hint);
|
self.editor.set_hint(hint);
|
||||||
self.completer.clear(&mut self.writer)?;
|
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();
|
self.completer.reset();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -486,6 +538,9 @@ impl ShedVi {
|
|||||||
return Ok(event);
|
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
|
// Redraw if we processed any input
|
||||||
if self.needs_redraw {
|
if self.needs_redraw {
|
||||||
@@ -498,6 +553,7 @@ impl ShedVi {
|
|||||||
|
|
||||||
pub fn handle_key(&mut self, key: KeyEvent) -> ShResult<Option<ReadlineEvent>> {
|
pub fn handle_key(&mut self, key: KeyEvent) -> ShResult<Option<ReadlineEvent>> {
|
||||||
if self.should_accept_hint(&key) {
|
if self.should_accept_hint(&key) {
|
||||||
|
log::debug!("Accepting hint on key {key:?} in mode {:?}", self.mode.report_mode());
|
||||||
self.editor.accept_hint();
|
self.editor.accept_hint();
|
||||||
if !self.history.at_pending() {
|
if !self.history.at_pending() {
|
||||||
self.history.reset_to_pending();
|
self.history.reset_to_pending();
|
||||||
@@ -510,6 +566,12 @@ impl ShedVi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let KeyEvent(KeyCode::Tab, mod_keys) = key {
|
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 {
|
let direction = match mod_keys {
|
||||||
ModKeys::SHIFT => -1,
|
ModKeys::SHIFT => -1,
|
||||||
_ => 1,
|
_ => 1,
|
||||||
@@ -524,7 +586,14 @@ impl ShedVi {
|
|||||||
self.old_layout = None;
|
self.old_layout = None;
|
||||||
}
|
}
|
||||||
Ok(Some(line)) => {
|
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 span_start = self.completer.token_span().0;
|
||||||
|
|
||||||
let new_cursor = span_start
|
let new_cursor = span_start
|
||||||
+ self
|
+ self
|
||||||
.completer
|
.completer
|
||||||
@@ -532,7 +601,7 @@ impl ShedVi {
|
|||||||
.map(|c| c.len())
|
.map(|c| c.len())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
self.editor.set_buffer(line);
|
self.editor.set_buffer(line.clone());
|
||||||
self.editor.cursor.set(new_cursor);
|
self.editor.cursor.set(new_cursor);
|
||||||
|
|
||||||
if !self.history.at_pending() {
|
if !self.history.at_pending() {
|
||||||
@@ -543,10 +612,19 @@ impl ShedVi {
|
|||||||
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
|
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
|
||||||
let hint = self.history.get_hint();
|
let hint = self.history.get_hint();
|
||||||
self.editor.set_hint(hint);
|
self.editor.set_hint(hint);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
|
let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnCompletionStart));
|
||||||
|
|
||||||
|
post_cmds.exec();
|
||||||
|
|
||||||
self.writer.send_bell().ok();
|
self.writer.send_bell().ok();
|
||||||
if self.completer.is_active() {
|
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);
|
self.editor.set_hint(None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -554,6 +632,33 @@ impl ShedVi {
|
|||||||
|
|
||||||
self.needs_redraw = true;
|
self.needs_redraw = true;
|
||||||
return Ok(None);
|
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
|
if let KeyEvent(KeyCode::Char('\\'), ModKeys::NONE) = key
|
||||||
@@ -586,6 +691,12 @@ impl ShedVi {
|
|||||||
&& !self.editor.buffer.ends_with('\\')
|
&& !self.editor.buffer.ends_with('\\')
|
||||||
&& (self.should_submit()? || !read_shopts(|o| o.prompt.linebreak_on_incomplete))
|
&& (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.set_hint(None);
|
||||||
self.editor.cursor.set(self.editor.cursor_max());
|
self.editor.cursor.set(self.editor.cursor_max());
|
||||||
self.print_line(true)?;
|
self.print_line(true)?;
|
||||||
@@ -747,6 +858,7 @@ impl ShedVi {
|
|||||||
let one_line = new_layout.end.row == 0;
|
let one_line = new_layout.end.row == 0;
|
||||||
|
|
||||||
self.completer.clear(&mut self.writer)?;
|
self.completer.clear(&mut self.writer)?;
|
||||||
|
self.history.fuzzy_finder.clear(&mut self.writer)?;
|
||||||
|
|
||||||
if let Some(layout) = self.old_layout.as_ref() {
|
if let Some(layout) = self.old_layout.as_ref() {
|
||||||
self.writer.clear_rows(layout)?;
|
self.writer.clear_rows(layout)?;
|
||||||
@@ -837,6 +949,9 @@ impl ShedVi {
|
|||||||
.set_prompt_line_context(preceding_width, new_layout.cursor.col);
|
.set_prompt_line_context(preceding_width, new_layout.cursor.col);
|
||||||
self.completer.draw(&mut self.writer)?;
|
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.old_layout = Some(new_layout);
|
||||||
self.needs_redraw = false;
|
self.needs_redraw = false;
|
||||||
|
|
||||||
@@ -1529,6 +1644,35 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
|
|||||||
insertions.push((span_start + index, markers::GLOB));
|
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
|
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 total_lines = s.lines().count();
|
||||||
let max_num_len = total_lines.to_string().len();
|
let max_num_len = total_lines.to_string().len();
|
||||||
s.lines()
|
s.lines()
|
||||||
@@ -84,23 +84,15 @@ fn enumerate_lines(s: &str, left_pad: usize) -> String {
|
|||||||
// " 2 | " — num + padding + " | "
|
// " 2 | " — num + padding + " | "
|
||||||
let prefix_len = max_num_len + 3; // "N | "
|
let prefix_len = max_num_len + 3; // "N | "
|
||||||
let trail_pad = left_pad.saturating_sub(prefix_len);
|
let trail_pad = left_pad.saturating_sub(prefix_len);
|
||||||
if i == total_lines - 1 {
|
let prefix = if show_numbers {
|
||||||
// Don't add a newline to the last line
|
format!("\x1b[0m\x1b[90m{}{num} |\x1b[0m ", " ".repeat(num_pad))
|
||||||
write!(
|
|
||||||
acc,
|
|
||||||
"\x1b[0m\x1b[90m{}{num} |\x1b[0m {}{ln}",
|
|
||||||
" ".repeat(num_pad),
|
|
||||||
" ".repeat(trail_pad),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
} else {
|
} else {
|
||||||
writeln!(
|
" ".repeat(prefix_len + 1).to_string()
|
||||||
acc,
|
};
|
||||||
"\x1b[0m\x1b[90m{}{num} |\x1b[0m {}{ln}",
|
if i == total_lines - 1 {
|
||||||
" ".repeat(num_pad),
|
write!(acc, "{prefix}{}{ln}", " ".repeat(trail_pad)).unwrap();
|
||||||
" ".repeat(trail_pad),
|
} else {
|
||||||
)
|
writeln!(acc, "{prefix}{}{ln}", " ".repeat(trail_pad)).unwrap();
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
acc
|
acc
|
||||||
@@ -1013,7 +1005,8 @@ impl LineWriter for TermWriter {
|
|||||||
let multiline = line.contains('\n');
|
let multiline = line.contains('\n');
|
||||||
if multiline {
|
if multiline {
|
||||||
let prompt_end = Layout::calc_pos(self.t_cols, prompt, Pos { col: 0, row: 0 }, 0, false);
|
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);
|
self.buffer.push_str(&display_line);
|
||||||
} else {
|
} else {
|
||||||
self.buffer.push_str(line);
|
self.buffer.push_str(line);
|
||||||
|
|||||||
@@ -54,6 +54,15 @@ impl ViMode for ViInsert {
|
|||||||
.set_motion(MotionCmd(1, Motion::ForwardChar));
|
.set_motion(MotionCmd(1, Motion::ForwardChar));
|
||||||
self.register_and_return()
|
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) => {
|
E(K::Char('W'), M::CTRL) => {
|
||||||
self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete));
|
self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete));
|
||||||
self.pending_cmd.set_motion(MotionCmd(
|
self.pending_cmd.set_motion(MotionCmd(
|
||||||
|
|||||||
@@ -756,6 +756,15 @@ impl ViMode for ViNormal {
|
|||||||
raw_seq: "".into(),
|
raw_seq: "".into(),
|
||||||
flags: self.flags(),
|
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) => {
|
E(K::Char('A'), M::CTRL) => {
|
||||||
let count = self
|
let count = self
|
||||||
.parse_count(&mut self.pending_seq.chars().peekable())
|
.parse_count(&mut self.pending_seq.chars().peekable())
|
||||||
|
|||||||
@@ -41,6 +41,15 @@ impl ViMode for ViReplace {
|
|||||||
.set_motion(MotionCmd(1, Motion::ForwardChar));
|
.set_motion(MotionCmd(1, Motion::ForwardChar));
|
||||||
self.register_and_return()
|
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) => {
|
E(K::Char('W'), M::CTRL) => {
|
||||||
self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete));
|
self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete));
|
||||||
self.pending_cmd.set_motion(MotionCmd(
|
self.pending_cmd.set_motion(MotionCmd(
|
||||||
|
|||||||
@@ -614,6 +614,15 @@ impl ViMode for ViVisual {
|
|||||||
raw_seq: "".into(),
|
raw_seq: "".into(),
|
||||||
flags: CmdFlags::empty(),
|
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) => {
|
E(K::Char('A'), M::CTRL) => {
|
||||||
let count = self
|
let count = self
|
||||||
.parse_count(&mut self.pending_seq.chars().peekable())
|
.parse_count(&mut self.pending_seq.chars().peekable())
|
||||||
|
|||||||
18
src/state.rs
18
src/state.rs
@@ -550,6 +550,12 @@ pub enum AutoCmdKind {
|
|||||||
PostPrompt,
|
PostPrompt,
|
||||||
PreModeChange,
|
PreModeChange,
|
||||||
PostModeChange,
|
PostModeChange,
|
||||||
|
OnHistoryOpen,
|
||||||
|
OnHistoryClose,
|
||||||
|
OnHistorySelect,
|
||||||
|
OnCompletionStart,
|
||||||
|
OnCompletionCancel,
|
||||||
|
OnCompletionSelect,
|
||||||
OnExit,
|
OnExit,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,6 +571,12 @@ impl Display for AutoCmdKind {
|
|||||||
Self::PostPrompt => write!(f, "post-prompt"),
|
Self::PostPrompt => write!(f, "post-prompt"),
|
||||||
Self::PreModeChange => write!(f, "pre-mode-change"),
|
Self::PreModeChange => write!(f, "pre-mode-change"),
|
||||||
Self::PostModeChange => write!(f, "post-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"),
|
Self::OnExit => write!(f, "on-exit"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -583,6 +595,12 @@ impl FromStr for AutoCmdKind {
|
|||||||
"post-prompt" => Ok(Self::PostPrompt),
|
"post-prompt" => Ok(Self::PostPrompt),
|
||||||
"pre-mode-change" => Ok(Self::PreModeChange),
|
"pre-mode-change" => Ok(Self::PreModeChange),
|
||||||
"post-mode-change" => Ok(Self::PostModeChange),
|
"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),
|
"on-exit" => Ok(Self::OnExit),
|
||||||
_ => Err(ShErr::simple(
|
_ => Err(ShErr::simple(
|
||||||
ShErrKind::ParseErr,
|
ShErrKind::ParseErr,
|
||||||
|
|||||||
Reference in New Issue
Block a user