command arguments are now underlined if they match an existing path -m ran rustfmt on the entire codebase
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
pub mod readline;
|
||||
pub mod statusline;
|
||||
|
||||
|
||||
use crate::{expand::expand_prompt, libsh::error::ShResult, prelude::*};
|
||||
|
||||
/// Initialize the line editor
|
||||
@@ -16,7 +15,7 @@ pub fn get_prompt() -> ShResult<String> {
|
||||
"\\e[0m\\n\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m ";
|
||||
return expand_prompt(default);
|
||||
};
|
||||
let sanitized = format!("\\e[0m{prompt}");
|
||||
let sanitized = format!("\\e[0m{prompt}");
|
||||
|
||||
expand_prompt(&sanitized)
|
||||
}
|
||||
|
||||
@@ -1,445 +1,467 @@
|
||||
use std::{env, os::unix::fs::PermissionsExt, path::PathBuf, sync::Arc};
|
||||
|
||||
use crate::{builtin::BUILTINS, libsh::error::{ShErr, ShErrKind, ShResult}, parse::lex::{self, LexFlags, Tk, TkFlags}, prompt::readline::{Marker, annotate_input, annotate_input_recursive, get_insertions, markers::{self, is_marker}}, state::{read_logic, read_vars}};
|
||||
use crate::{
|
||||
builtin::BUILTINS,
|
||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||
parse::lex::{self, LexFlags, Tk, TkFlags},
|
||||
prompt::readline::{
|
||||
Marker, annotate_input, annotate_input_recursive, get_insertions,
|
||||
markers::{self, is_marker},
|
||||
},
|
||||
state::{read_logic, read_vars},
|
||||
};
|
||||
|
||||
pub enum CompCtx {
|
||||
CmdName,
|
||||
FileName
|
||||
CmdName,
|
||||
FileName,
|
||||
}
|
||||
|
||||
pub enum CompResult {
|
||||
NoMatch,
|
||||
Single {
|
||||
result: String
|
||||
},
|
||||
Many {
|
||||
candidates: Vec<String>
|
||||
}
|
||||
NoMatch,
|
||||
Single { result: String },
|
||||
Many { candidates: Vec<String> },
|
||||
}
|
||||
|
||||
impl CompResult {
|
||||
pub fn from_candidates(candidates: Vec<String>) -> Self {
|
||||
if candidates.is_empty() {
|
||||
Self::NoMatch
|
||||
} else if candidates.len() == 1 {
|
||||
Self::Single { result: candidates[0].clone() }
|
||||
} else {
|
||||
Self::Many { candidates }
|
||||
}
|
||||
}
|
||||
pub fn from_candidates(candidates: Vec<String>) -> Self {
|
||||
if candidates.is_empty() {
|
||||
Self::NoMatch
|
||||
} else if candidates.len() == 1 {
|
||||
Self::Single {
|
||||
result: candidates[0].clone(),
|
||||
}
|
||||
} else {
|
||||
Self::Many { candidates }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Completer {
|
||||
pub candidates: Vec<String>,
|
||||
pub selected_idx: usize,
|
||||
pub original_input: String,
|
||||
pub token_span: (usize, usize),
|
||||
pub active: bool,
|
||||
pub candidates: Vec<String>,
|
||||
pub selected_idx: usize,
|
||||
pub original_input: String,
|
||||
pub token_span: (usize, usize),
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
impl Completer {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
candidates: vec![],
|
||||
selected_idx: 0,
|
||||
original_input: String::new(),
|
||||
token_span: (0, 0),
|
||||
active: false,
|
||||
}
|
||||
}
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
candidates: vec![],
|
||||
selected_idx: 0,
|
||||
original_input: String::new(),
|
||||
token_span: (0, 0),
|
||||
active: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn slice_line(line: &str, cursor_pos: usize) -> (&str, &str) {
|
||||
let (before_cursor, after_cursor) = line.split_at(cursor_pos);
|
||||
(before_cursor, after_cursor)
|
||||
}
|
||||
pub fn slice_line(line: &str, cursor_pos: usize) -> (&str, &str) {
|
||||
let (before_cursor, after_cursor) = line.split_at(cursor_pos);
|
||||
(before_cursor, after_cursor)
|
||||
}
|
||||
|
||||
pub fn get_completion_context(&self, line: &str, cursor_pos: usize) -> (Vec<Marker>, usize) {
|
||||
let annotated = annotate_input_recursive(line);
|
||||
log::debug!("Annotated input for completion context: {:?}", annotated);
|
||||
let mut ctx = vec![markers::NULL];
|
||||
let mut last_priority = 0;
|
||||
let mut ctx_start = 0;
|
||||
let mut pos = 0;
|
||||
pub fn get_completion_context(&self, line: &str, cursor_pos: usize) -> (Vec<Marker>, usize) {
|
||||
let annotated = annotate_input_recursive(line);
|
||||
let mut ctx = vec![markers::NULL];
|
||||
let mut last_priority = 0;
|
||||
let mut ctx_start = 0;
|
||||
let mut pos = 0;
|
||||
|
||||
for ch in annotated.chars() {
|
||||
match ch {
|
||||
_ if is_marker(ch) => {
|
||||
match ch {
|
||||
markers::COMMAND | markers::BUILTIN => {
|
||||
log::debug!("Found command marker at position {}", pos);
|
||||
if last_priority < 2 {
|
||||
if last_priority > 0 {
|
||||
ctx.pop();
|
||||
}
|
||||
ctx_start = pos;
|
||||
last_priority = 2;
|
||||
ctx.push(markers::COMMAND);
|
||||
}
|
||||
}
|
||||
markers::VAR_SUB => {
|
||||
log::debug!("Found variable substitution marker at position {}", pos);
|
||||
if last_priority < 3 {
|
||||
if last_priority > 0 {
|
||||
ctx.pop();
|
||||
}
|
||||
ctx_start = pos;
|
||||
last_priority = 3;
|
||||
ctx.push(markers::VAR_SUB);
|
||||
}
|
||||
}
|
||||
markers::ARG | markers::ASSIGNMENT => {
|
||||
log::debug!("Found argument/assignment marker at position {}", pos);
|
||||
if last_priority < 1 {
|
||||
ctx_start = pos;
|
||||
ctx.push(markers::ARG);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
last_priority = 0; // reset priority on normal characters
|
||||
pos += 1; // we hit a normal character, advance our position
|
||||
if pos >= cursor_pos {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for ch in annotated.chars() {
|
||||
match ch {
|
||||
_ if is_marker(ch) => match ch {
|
||||
markers::COMMAND | markers::BUILTIN => {
|
||||
if last_priority < 2 {
|
||||
if last_priority > 0 {
|
||||
ctx.pop();
|
||||
}
|
||||
ctx_start = pos;
|
||||
last_priority = 2;
|
||||
ctx.push(markers::COMMAND);
|
||||
}
|
||||
}
|
||||
markers::VAR_SUB => {
|
||||
if last_priority < 3 {
|
||||
if last_priority > 0 {
|
||||
ctx.pop();
|
||||
}
|
||||
ctx_start = pos;
|
||||
last_priority = 3;
|
||||
ctx.push(markers::VAR_SUB);
|
||||
}
|
||||
}
|
||||
markers::ARG | markers::ASSIGNMENT => {
|
||||
if last_priority < 1 {
|
||||
ctx_start = pos;
|
||||
ctx.push(markers::ARG);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {
|
||||
last_priority = 0; // reset priority on normal characters
|
||||
pos += 1; // we hit a normal character, advance our position
|
||||
if pos >= cursor_pos {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(ctx, ctx_start)
|
||||
}
|
||||
(ctx, ctx_start)
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.candidates.clear();
|
||||
self.selected_idx = 0;
|
||||
self.original_input.clear();
|
||||
self.token_span = (0, 0);
|
||||
self.active = false;
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.candidates.clear();
|
||||
self.selected_idx = 0;
|
||||
self.original_input.clear();
|
||||
self.token_span = (0, 0);
|
||||
self.active = false;
|
||||
}
|
||||
|
||||
pub fn complete(&mut self, line: String, cursor_pos: usize, direction: i32) -> ShResult<Option<String>> {
|
||||
if self.active {
|
||||
Ok(Some(self.cycle_completion(direction)))
|
||||
} else {
|
||||
self.start_completion(line, cursor_pos)
|
||||
}
|
||||
}
|
||||
pub fn complete(
|
||||
&mut self,
|
||||
line: String,
|
||||
cursor_pos: usize,
|
||||
direction: i32,
|
||||
) -> ShResult<Option<String>> {
|
||||
if self.active {
|
||||
Ok(Some(self.cycle_completion(direction)))
|
||||
} else {
|
||||
self.start_completion(line, cursor_pos)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn selected_candidate(&self) -> Option<String> {
|
||||
self.candidates.get(self.selected_idx).cloned()
|
||||
}
|
||||
pub fn selected_candidate(&self) -> Option<String> {
|
||||
self.candidates.get(self.selected_idx).cloned()
|
||||
}
|
||||
|
||||
pub fn cycle_completion(&mut self, direction: i32) -> String {
|
||||
if self.candidates.is_empty() {
|
||||
return self.original_input.clone();
|
||||
}
|
||||
pub fn cycle_completion(&mut self, direction: i32) -> String {
|
||||
if self.candidates.is_empty() {
|
||||
return self.original_input.clone();
|
||||
}
|
||||
|
||||
let len = self.candidates.len();
|
||||
self.selected_idx = (self.selected_idx as i32 + direction).rem_euclid(len as i32) as usize;
|
||||
let len = self.candidates.len();
|
||||
self.selected_idx = (self.selected_idx as i32 + direction).rem_euclid(len as i32) as usize;
|
||||
|
||||
self.get_completed_line()
|
||||
}
|
||||
self.get_completed_line()
|
||||
}
|
||||
|
||||
pub fn start_completion(&mut self, line: String, cursor_pos: usize) -> ShResult<Option<String>> {
|
||||
let result = self.get_candidates(line.clone(), cursor_pos)?;
|
||||
match result {
|
||||
CompResult::Many { candidates } => {
|
||||
self.candidates = candidates.clone();
|
||||
self.selected_idx = 0;
|
||||
self.original_input = line;
|
||||
self.active = true;
|
||||
pub fn start_completion(&mut self, line: String, cursor_pos: usize) -> ShResult<Option<String>> {
|
||||
let result = self.get_candidates(line.clone(), cursor_pos)?;
|
||||
match result {
|
||||
CompResult::Many { candidates } => {
|
||||
self.candidates = candidates.clone();
|
||||
self.selected_idx = 0;
|
||||
self.original_input = line;
|
||||
self.active = true;
|
||||
|
||||
Ok(Some(self.get_completed_line()))
|
||||
}
|
||||
CompResult::Single { result } => {
|
||||
self.candidates = vec![result.clone()];
|
||||
self.selected_idx = 0;
|
||||
self.original_input = line;
|
||||
self.active = false;
|
||||
Ok(Some(self.get_completed_line()))
|
||||
}
|
||||
CompResult::Single { result } => {
|
||||
self.candidates = vec![result.clone()];
|
||||
self.selected_idx = 0;
|
||||
self.original_input = line;
|
||||
self.active = false;
|
||||
|
||||
Ok(Some(self.get_completed_line()))
|
||||
}
|
||||
CompResult::NoMatch => Ok(None)
|
||||
Ok(Some(self.get_completed_line()))
|
||||
}
|
||||
CompResult::NoMatch => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
pub fn extract_var_name(text: &str) -> Option<(String, usize, usize)> {
|
||||
let mut chars = text.chars().peekable();
|
||||
let mut name = String::new();
|
||||
let mut reading_name = false;
|
||||
let mut pos = 0;
|
||||
let mut name_start = 0;
|
||||
let mut name_end = 0;
|
||||
|
||||
pub fn extract_var_name(text: &str) -> Option<(String,usize,usize)> {
|
||||
let mut chars = text.chars().peekable();
|
||||
let mut name = String::new();
|
||||
let mut reading_name = false;
|
||||
let mut pos = 0;
|
||||
let mut name_start = 0;
|
||||
let mut name_end = 0;
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
'$' => {
|
||||
if chars.peek() == Some(&'{') {
|
||||
continue;
|
||||
}
|
||||
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
'$' => {
|
||||
if chars.peek() == Some(&'{') {
|
||||
continue;
|
||||
}
|
||||
reading_name = true;
|
||||
name_start = pos + 1; // Start after the '$'
|
||||
}
|
||||
'{' if !reading_name => {
|
||||
reading_name = true;
|
||||
name_start = pos + 1;
|
||||
}
|
||||
ch if ch.is_alphanumeric() || ch == '_' => {
|
||||
if reading_name {
|
||||
name.push(ch);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if reading_name {
|
||||
name_end = pos; // End before the non-alphanumeric character
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
reading_name = true;
|
||||
name_start = pos + 1; // Start after the '$'
|
||||
}
|
||||
'{' if !reading_name => {
|
||||
reading_name = true;
|
||||
name_start = pos + 1;
|
||||
}
|
||||
ch if ch.is_alphanumeric() || ch == '_' => {
|
||||
if reading_name {
|
||||
name.push(ch);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if reading_name {
|
||||
name_end = pos; // End before the non-alphanumeric character
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
pos += 1;
|
||||
}
|
||||
if !reading_name {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !reading_name {
|
||||
return None;
|
||||
}
|
||||
if name_end == 0 {
|
||||
name_end = pos;
|
||||
}
|
||||
|
||||
if name_end == 0 {
|
||||
name_end = pos;
|
||||
}
|
||||
Some((name, name_start, name_end))
|
||||
}
|
||||
|
||||
Some((name, name_start, name_end))
|
||||
}
|
||||
pub fn get_completed_line(&self) -> String {
|
||||
if self.candidates.is_empty() {
|
||||
return self.original_input.clone();
|
||||
}
|
||||
|
||||
pub fn get_completed_line(&self) -> String {
|
||||
if self.candidates.is_empty() {
|
||||
return self.original_input.clone();
|
||||
}
|
||||
let selected = &self.candidates[self.selected_idx];
|
||||
let (start, end) = self.token_span;
|
||||
format!(
|
||||
"{}{}{}",
|
||||
&self.original_input[..start],
|
||||
selected,
|
||||
&self.original_input[end..]
|
||||
)
|
||||
}
|
||||
|
||||
let selected = &self.candidates[self.selected_idx];
|
||||
let (start, end) = self.token_span;
|
||||
format!("{}{}{}", &self.original_input[..start], selected, &self.original_input[end..])
|
||||
}
|
||||
pub fn get_candidates(&mut self, line: String, cursor_pos: usize) -> ShResult<CompResult> {
|
||||
let source = Arc::new(line.clone());
|
||||
let tokens =
|
||||
lex::LexStream::new(source, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>()?;
|
||||
|
||||
pub fn get_candidates(&mut self, line: String, cursor_pos: usize) -> ShResult<CompResult> {
|
||||
let source = Arc::new(line.clone());
|
||||
let tokens = lex::LexStream::new(source, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>()?;
|
||||
let Some(mut cur_token) = tokens.into_iter().find(|tk| {
|
||||
let start = tk.span.start;
|
||||
let end = tk.span.end;
|
||||
(start..=end).contains(&cursor_pos)
|
||||
}) else {
|
||||
let candidates = Self::complete_filename("./"); // Default to filename completion if no token is found
|
||||
let end_pos = line.len();
|
||||
self.token_span = (end_pos, end_pos);
|
||||
return Ok(CompResult::from_candidates(candidates));
|
||||
};
|
||||
|
||||
let Some(mut cur_token) = tokens.into_iter().find(|tk| {
|
||||
let start = tk.span.start;
|
||||
let end = tk.span.end;
|
||||
(start..=end).contains(&cursor_pos)
|
||||
}) else {
|
||||
log::debug!("No token found at cursor position");
|
||||
let candidates = Self::complete_filename("./"); // Default to filename completion if no token is found
|
||||
let end_pos = line.len();
|
||||
self.token_span = (end_pos, end_pos);
|
||||
return Ok(CompResult::from_candidates(candidates));
|
||||
};
|
||||
self.token_span = (cur_token.span.start, cur_token.span.end);
|
||||
|
||||
self.token_span = (cur_token.span.start, cur_token.span.end);
|
||||
// Look for marker at the START of what we're completing, not at cursor
|
||||
let (mut ctx, token_start) = self.get_completion_context(&line, cursor_pos);
|
||||
self.token_span.0 = token_start; // Update start of token span based on context
|
||||
cur_token
|
||||
.span
|
||||
.set_range(self.token_span.0..self.token_span.1); // Update token span to reflect context
|
||||
|
||||
// If token contains '=', only complete after the '='
|
||||
let token_str = cur_token.span.as_str();
|
||||
if let Some(eq_pos) = token_str.rfind('=') {
|
||||
// Adjust span to only replace the part after '='
|
||||
self.token_span.0 = cur_token.span.start + eq_pos + 1;
|
||||
}
|
||||
|
||||
// Look for marker at the START of what we're completing, not at cursor
|
||||
let (mut ctx, token_start) = self.get_completion_context(&line, cursor_pos);
|
||||
self.token_span.0 = token_start; // Update start of token span based on context
|
||||
cur_token.span.set_range(self.token_span.0..self.token_span.1); // Update token span to reflect context
|
||||
if ctx.last().is_some_and(|m| *m == markers::VAR_SUB) {
|
||||
let var_sub = &cur_token.as_str();
|
||||
if let Some((var_name, start, end)) = Self::extract_var_name(var_sub) {
|
||||
if read_vars(|v| v.get_var(&var_name)).is_empty() {
|
||||
// if we are here, we have a variable substitution that isn't complete
|
||||
// so let's try to complete it
|
||||
let ret: ShResult<CompResult> = read_vars(|v| {
|
||||
let var_matches = v
|
||||
.flatten_vars()
|
||||
.keys()
|
||||
.filter(|k| k.starts_with(&var_name) && *k != &var_name)
|
||||
.map(|k| k.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// If token contains '=', only complete after the '='
|
||||
let token_str = cur_token.span.as_str();
|
||||
if let Some(eq_pos) = token_str.rfind('=') {
|
||||
// Adjust span to only replace the part after '='
|
||||
self.token_span.0 = cur_token.span.start + eq_pos + 1;
|
||||
}
|
||||
if !var_matches.is_empty() {
|
||||
let name_start = cur_token.span.start + start;
|
||||
let name_end = cur_token.span.start + end;
|
||||
self.token_span = (name_start, name_end);
|
||||
cur_token
|
||||
.span
|
||||
.set_range(self.token_span.0..self.token_span.1);
|
||||
Ok(CompResult::from_candidates(var_matches))
|
||||
} else {
|
||||
Ok(CompResult::NoMatch)
|
||||
}
|
||||
});
|
||||
|
||||
if ctx.last().is_some_and(|m| *m == markers::VAR_SUB) {
|
||||
let var_sub = &cur_token.as_str();
|
||||
if let Some((var_name,start,end)) = Self::extract_var_name(var_sub) {
|
||||
log::debug!("Extracted variable name for completion: {}", var_name);
|
||||
if read_vars(|v| v.get_var(&var_name)).is_empty() {
|
||||
// if we are here, we have a variable substitution that isn't complete
|
||||
// so let's try to complete it
|
||||
let ret: ShResult<CompResult> = read_vars(|v| {
|
||||
let var_matches = v.flatten_vars()
|
||||
.keys()
|
||||
.filter(|k| k.starts_with(&var_name) && *k != &var_name)
|
||||
.map(|k| k.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
if !matches!(ret, Ok(CompResult::NoMatch)) {
|
||||
return ret;
|
||||
} else {
|
||||
ctx.pop();
|
||||
}
|
||||
} else {
|
||||
ctx.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !var_matches.is_empty() {
|
||||
let name_start = cur_token.span.start + start;
|
||||
let name_end = cur_token.span.start + end;
|
||||
self.token_span = (name_start, name_end);
|
||||
cur_token.span.set_range(self.token_span.0..self.token_span.1);
|
||||
Ok(CompResult::from_candidates(var_matches))
|
||||
} else {
|
||||
Ok(CompResult::NoMatch)
|
||||
}
|
||||
});
|
||||
let raw_tk = cur_token.as_str().to_string();
|
||||
let expanded_tk = cur_token.expand()?;
|
||||
let expanded_words = expanded_tk.get_words().into_iter().collect::<Vec<_>>();
|
||||
let expanded = expanded_words.join("\\ ");
|
||||
|
||||
if !matches!(ret, Ok(CompResult::NoMatch)) {
|
||||
return ret;
|
||||
} else {
|
||||
ctx.pop();
|
||||
}
|
||||
} else {
|
||||
ctx.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut candidates = match ctx.pop() {
|
||||
Some(markers::COMMAND) => Self::complete_command(&expanded)?,
|
||||
Some(markers::ARG) => Self::complete_filename(&expanded),
|
||||
Some(_) => {
|
||||
return Ok(CompResult::NoMatch);
|
||||
}
|
||||
None => {
|
||||
return Ok(CompResult::NoMatch);
|
||||
}
|
||||
};
|
||||
|
||||
let raw_tk = cur_token.as_str().to_string();
|
||||
let expanded_tk = cur_token.expand()?;
|
||||
let expanded_words = expanded_tk.get_words().into_iter().collect::<Vec<_>>();
|
||||
let expanded = expanded_words.join("\\ ");
|
||||
// Now we are just going to graft the completed text
|
||||
// onto the original token. This prevents something like
|
||||
// $SOME_PATH/
|
||||
// from being completed into
|
||||
// /path/to/some_path/file.txt
|
||||
// and instead returns
|
||||
// $SOME_PATH/file.txt
|
||||
candidates = candidates
|
||||
.into_iter()
|
||||
.map(|c| match c.strip_prefix(&expanded) {
|
||||
Some(suffix) => format!("{raw_tk}{suffix}"),
|
||||
None => c,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut candidates = match ctx.pop() {
|
||||
Some(markers::COMMAND) => {
|
||||
log::debug!("Completing command: {}", &expanded);
|
||||
Self::complete_command(&expanded)?
|
||||
}
|
||||
Some(markers::ARG) => {
|
||||
log::debug!("Completing filename: {}", &expanded);
|
||||
Self::complete_filename(&expanded)
|
||||
}
|
||||
Some(m) => {
|
||||
log::warn!("Unknown marker {:?} in completion context", m);
|
||||
return Ok(CompResult::NoMatch);
|
||||
}
|
||||
None => {
|
||||
log::warn!("No marker found in completion context");
|
||||
return Ok(CompResult::NoMatch);
|
||||
}
|
||||
};
|
||||
let limit = crate::state::read_shopts(|s| s.prompt.comp_limit);
|
||||
candidates.truncate(limit);
|
||||
|
||||
// Now we are just going to graft the completed text
|
||||
// onto the original token. This prevents something like
|
||||
// $SOME_PATH/
|
||||
// from being completed into
|
||||
// /path/to/some_path/file.txt
|
||||
// and instead returns
|
||||
// $SOME_PATH/file.txt
|
||||
candidates = candidates.into_iter()
|
||||
.map(|c| match c.strip_prefix(&expanded) {
|
||||
Some(suffix) => format!("{raw_tk}{suffix}"),
|
||||
None => c
|
||||
})
|
||||
.collect();
|
||||
Ok(CompResult::from_candidates(candidates))
|
||||
}
|
||||
|
||||
let limit = crate::state::read_shopts(|s| s.prompt.comp_limit);
|
||||
candidates.truncate(limit);
|
||||
fn complete_command(start: &str) -> ShResult<Vec<String>> {
|
||||
let mut candidates = vec![];
|
||||
|
||||
Ok(CompResult::from_candidates(candidates))
|
||||
}
|
||||
let path = env::var("PATH").unwrap_or_default();
|
||||
let paths = path.split(':').map(PathBuf::from).collect::<Vec<_>>();
|
||||
for path in paths {
|
||||
// Skip directories that don't exist (common in PATH)
|
||||
let Ok(entries) = std::fs::read_dir(path) else {
|
||||
continue;
|
||||
};
|
||||
for entry in entries {
|
||||
let Ok(entry) = entry else {
|
||||
continue;
|
||||
};
|
||||
let Ok(meta) = entry.metadata() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
fn complete_command(start: &str) -> ShResult<Vec<String>> {
|
||||
let mut candidates = vec![];
|
||||
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||
|
||||
let path = env::var("PATH").unwrap_or_default();
|
||||
let paths = path.split(':').map(PathBuf::from).collect::<Vec<_>>();
|
||||
for path in paths {
|
||||
// Skip directories that don't exist (common in PATH)
|
||||
let Ok(entries) = std::fs::read_dir(path) else { continue; };
|
||||
for entry in entries {
|
||||
let Ok(entry) = entry else { continue; };
|
||||
let Ok(meta) = entry.metadata() else { continue; };
|
||||
if meta.is_file()
|
||||
&& (meta.permissions().mode() & 0o111) != 0
|
||||
&& file_name.starts_with(start)
|
||||
{
|
||||
candidates.push(file_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||
let builtin_candidates = BUILTINS
|
||||
.iter()
|
||||
.filter(|b| b.starts_with(start))
|
||||
.map(|s| s.to_string());
|
||||
|
||||
if meta.is_file()
|
||||
&& (meta.permissions().mode() & 0o111) != 0
|
||||
&& file_name.starts_with(start) {
|
||||
candidates.push(file_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
candidates.extend(builtin_candidates);
|
||||
|
||||
let builtin_candidates = BUILTINS
|
||||
.iter()
|
||||
.filter(|b| b.starts_with(start))
|
||||
.map(|s| s.to_string());
|
||||
read_logic(|l| {
|
||||
let func_table = l.funcs();
|
||||
let matches = func_table
|
||||
.keys()
|
||||
.filter(|k| k.starts_with(start))
|
||||
.map(|k| k.to_string());
|
||||
|
||||
candidates.extend(builtin_candidates);
|
||||
candidates.extend(matches);
|
||||
|
||||
read_logic(|l| {
|
||||
let func_table = l.funcs();
|
||||
let matches = func_table
|
||||
.keys()
|
||||
.filter(|k| k.starts_with(start))
|
||||
.map(|k| k.to_string());
|
||||
let aliases = l.aliases();
|
||||
let matches = aliases
|
||||
.keys()
|
||||
.filter(|k| k.starts_with(start))
|
||||
.map(|k| k.to_string());
|
||||
|
||||
candidates.extend(matches);
|
||||
candidates.extend(matches);
|
||||
});
|
||||
|
||||
let aliases = l.aliases();
|
||||
let matches = aliases
|
||||
.keys()
|
||||
.filter(|k| k.starts_with(start))
|
||||
.map(|k| k.to_string());
|
||||
// Deduplicate (same command may appear in multiple PATH dirs)
|
||||
candidates.sort();
|
||||
candidates.dedup();
|
||||
|
||||
candidates.extend(matches);
|
||||
});
|
||||
Ok(candidates)
|
||||
}
|
||||
|
||||
// Deduplicate (same command may appear in multiple PATH dirs)
|
||||
candidates.sort();
|
||||
candidates.dedup();
|
||||
fn complete_filename(start: &str) -> Vec<String> {
|
||||
let mut candidates = vec![];
|
||||
let has_dotslash = start.starts_with("./");
|
||||
|
||||
Ok(candidates)
|
||||
}
|
||||
// Split path into directory and filename parts
|
||||
// Use "." if start is empty (e.g., after "foo=")
|
||||
let path = PathBuf::from(if start.is_empty() { "." } else { start });
|
||||
let (dir, prefix) = if start.ends_with('/') || start.is_empty() {
|
||||
// Completing inside a directory: "src/" → dir="src/", prefix=""
|
||||
(path, "")
|
||||
} else if let Some(parent) = path.parent()
|
||||
&& !parent.as_os_str().is_empty()
|
||||
{
|
||||
// Has directory component: "src/ma" → dir="src", prefix="ma"
|
||||
(
|
||||
parent.to_path_buf(),
|
||||
path.file_name().unwrap().to_str().unwrap_or(""),
|
||||
)
|
||||
} else {
|
||||
// No directory: "fil" → dir=".", prefix="fil"
|
||||
(PathBuf::from("."), start)
|
||||
};
|
||||
|
||||
fn complete_filename(start: &str) -> Vec<String> {
|
||||
let mut candidates = vec![];
|
||||
let Ok(entries) = std::fs::read_dir(&dir) else {
|
||||
return candidates;
|
||||
};
|
||||
|
||||
// If completing after '=', only use the part after it
|
||||
let start = if let Some(eq_pos) = start.rfind('=') {
|
||||
&start[eq_pos + 1..]
|
||||
} else {
|
||||
start
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let file_name = entry.file_name();
|
||||
let file_str = file_name.to_string_lossy();
|
||||
|
||||
// Split path into directory and filename parts
|
||||
// Use "." if start is empty (e.g., after "foo=")
|
||||
let path = PathBuf::from(if start.is_empty() { "." } else { start });
|
||||
let (dir, prefix) = if start.ends_with('/') || start.is_empty() {
|
||||
// Completing inside a directory: "src/" → dir="src/", prefix=""
|
||||
(path, "")
|
||||
} else if let Some(parent) = path.parent()
|
||||
&& !parent.as_os_str().is_empty() {
|
||||
// Has directory component: "src/ma" → dir="src", prefix="ma"
|
||||
(parent.to_path_buf(), path.file_name().unwrap().to_str().unwrap_or(""))
|
||||
} else {
|
||||
// No directory: "fil" → dir=".", prefix="fil"
|
||||
(PathBuf::from("."), start)
|
||||
};
|
||||
// Skip hidden files unless explicitly requested
|
||||
if !prefix.starts_with('.') && file_str.starts_with('.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Ok(entries) = std::fs::read_dir(&dir) else {
|
||||
return candidates;
|
||||
};
|
||||
if file_str.starts_with(prefix) {
|
||||
// Reconstruct full path
|
||||
let mut full_path = dir.join(&file_name);
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let file_name = entry.file_name();
|
||||
let file_str = file_name.to_string_lossy();
|
||||
// Add trailing slash for directories
|
||||
if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
|
||||
full_path.push(""); // adds trailing /
|
||||
}
|
||||
|
||||
// Skip hidden files unless explicitly requested
|
||||
if !prefix.starts_with('.') && file_str.starts_with('.') {
|
||||
continue;
|
||||
}
|
||||
let mut path_raw = full_path.to_string_lossy().to_string();
|
||||
if path_raw.starts_with("./") && !has_dotslash {
|
||||
path_raw = path_raw.trim_start_matches("./").to_string();
|
||||
}
|
||||
|
||||
if file_str.starts_with(prefix) {
|
||||
// Reconstruct full path
|
||||
let mut full_path = dir.join(&file_name);
|
||||
candidates.push(path_raw);
|
||||
}
|
||||
}
|
||||
|
||||
// Add trailing slash for directories
|
||||
if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
|
||||
full_path.push(""); // adds trailing /
|
||||
}
|
||||
|
||||
candidates.push(full_path.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
candidates.sort();
|
||||
candidates
|
||||
}
|
||||
candidates.sort();
|
||||
candidates
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Completer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,318 +1,395 @@
|
||||
use std::{env, os::unix::fs::PermissionsExt, path::{Path, PathBuf}};
|
||||
use std::{
|
||||
env,
|
||||
os::unix::fs::PermissionsExt,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::{libsh::term::{Style, StyleSet, Styled}, prompt::readline::{annotate_input, markers}, state::{read_logic, read_shopts}};
|
||||
use crate::{
|
||||
libsh::term::{Style, StyleSet, Styled},
|
||||
prompt::readline::{annotate_input, markers},
|
||||
state::{read_logic, read_shopts},
|
||||
};
|
||||
|
||||
/// Syntax highlighter for shell input using Unicode marker-based annotation
|
||||
///
|
||||
/// The highlighter processes annotated input strings containing invisible Unicode markers
|
||||
/// (U+FDD0-U+FDEF range) that indicate syntax elements. It generates ANSI escape codes
|
||||
/// for terminal display while maintaining a style stack for proper color restoration
|
||||
/// in nested constructs (e.g., variables inside strings inside command substitutions).
|
||||
/// The highlighter processes annotated input strings containing invisible
|
||||
/// Unicode markers (U+FDD0-U+FDEF range) that indicate syntax elements. It
|
||||
/// generates ANSI escape codes for terminal display while maintaining a style
|
||||
/// stack for proper color restoration in nested constructs (e.g., variables
|
||||
/// inside strings inside command substitutions).
|
||||
pub struct Highlighter {
|
||||
input: String,
|
||||
output: String,
|
||||
style_stack: Vec<StyleSet>,
|
||||
last_was_reset: bool,
|
||||
input: String,
|
||||
output: String,
|
||||
style_stack: Vec<StyleSet>,
|
||||
last_was_reset: bool,
|
||||
}
|
||||
|
||||
impl Highlighter {
|
||||
/// Creates a new highlighter with empty buffers and reset state
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
input: String::new(),
|
||||
output: String::new(),
|
||||
style_stack: Vec::new(),
|
||||
last_was_reset: true, // start as true so we don't emit a leading reset
|
||||
}
|
||||
}
|
||||
/// Creates a new highlighter with empty buffers and reset state
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
input: String::new(),
|
||||
output: String::new(),
|
||||
style_stack: Vec::new(),
|
||||
last_was_reset: true, // start as true so we don't emit a leading reset
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads raw input text and annotates it with syntax markers
|
||||
///
|
||||
/// The input is passed through the annotator which inserts Unicode markers
|
||||
/// indicating token types and sub-token constructs (strings, variables, etc.)
|
||||
pub fn load_input(&mut self, input: &str) {
|
||||
let input = annotate_input(input);
|
||||
self.input = input;
|
||||
}
|
||||
/// Loads raw input text and annotates it with syntax markers
|
||||
///
|
||||
/// The input is passed through the annotator which inserts Unicode markers
|
||||
/// indicating token types and sub-token constructs (strings, variables, etc.)
|
||||
pub fn load_input(&mut self, input: &str) {
|
||||
let input = annotate_input(input);
|
||||
self.input = input;
|
||||
}
|
||||
|
||||
/// Processes the annotated input and generates ANSI-styled output
|
||||
///
|
||||
/// Walks through the input character by character, interpreting markers and
|
||||
/// applying appropriate styles. Nested constructs (command substitutions,
|
||||
/// subshells, strings) are handled recursively with proper style restoration.
|
||||
pub fn highlight(&mut self) {
|
||||
let input = self.input.clone();
|
||||
let mut input_chars = input.chars().peekable();
|
||||
while let Some(ch) = input_chars.next() {
|
||||
match ch {
|
||||
markers::STRING_DQ_END |
|
||||
markers::STRING_SQ_END |
|
||||
markers::VAR_SUB_END |
|
||||
markers::CMD_SUB_END |
|
||||
markers::PROC_SUB_END |
|
||||
markers::SUBSH_END => self.pop_style(),
|
||||
/// Processes the annotated input and generates ANSI-styled output
|
||||
///
|
||||
/// Walks through the input character by character, interpreting markers and
|
||||
/// applying appropriate styles. Nested constructs (command substitutions,
|
||||
/// subshells, strings) are handled recursively with proper style restoration.
|
||||
pub fn highlight(&mut self) {
|
||||
let input = self.input.clone();
|
||||
let mut input_chars = input.chars().peekable();
|
||||
while let Some(ch) = input_chars.next() {
|
||||
match ch {
|
||||
markers::STRING_DQ_END
|
||||
| markers::STRING_SQ_END
|
||||
| markers::VAR_SUB_END
|
||||
| markers::CMD_SUB_END
|
||||
| markers::PROC_SUB_END
|
||||
| markers::SUBSH_END => self.pop_style(),
|
||||
|
||||
markers::CMD_SEP |
|
||||
markers::RESET => self.clear_styles(),
|
||||
markers::CMD_SEP | markers::RESET => self.clear_styles(),
|
||||
|
||||
markers::STRING_DQ | markers::STRING_SQ | markers::KEYWORD => {
|
||||
self.push_style(Style::Yellow)
|
||||
}
|
||||
markers::BUILTIN => self.push_style(Style::Green),
|
||||
markers::CASE_PAT => self.push_style(Style::Blue),
|
||||
|
||||
markers::STRING_DQ |
|
||||
markers::STRING_SQ |
|
||||
markers::KEYWORD => self.push_style(Style::Yellow),
|
||||
markers::BUILTIN => self.push_style(Style::Green),
|
||||
markers::CASE_PAT => self.push_style(Style::Blue),
|
||||
markers::ARG => self.push_style(Style::White),
|
||||
markers::COMMENT => self.push_style(Style::BrightBlack),
|
||||
markers::COMMENT => self.push_style(Style::BrightBlack),
|
||||
|
||||
markers::GLOB => self.push_style(Style::Blue),
|
||||
markers::GLOB => self.push_style(Style::Blue),
|
||||
|
||||
markers::REDIRECT |
|
||||
markers::OPERATOR => self.push_style(Style::Magenta | Style::Bold),
|
||||
markers::REDIRECT | markers::OPERATOR => self.push_style(Style::Magenta | Style::Bold),
|
||||
|
||||
markers::ASSIGNMENT => {
|
||||
let mut var_name = String::new();
|
||||
markers::ASSIGNMENT => {
|
||||
let mut var_name = String::new();
|
||||
|
||||
while let Some(ch) = input_chars.peek() {
|
||||
if ch == &'=' {
|
||||
input_chars.next(); // consume the '='
|
||||
break;
|
||||
}
|
||||
match *ch {
|
||||
markers::RESET => break,
|
||||
_ => {
|
||||
var_name.push(*ch);
|
||||
input_chars.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
while let Some(ch) = input_chars.peek() {
|
||||
if ch == &'=' {
|
||||
input_chars.next(); // consume the '='
|
||||
break;
|
||||
}
|
||||
match *ch {
|
||||
markers::RESET => break,
|
||||
_ => {
|
||||
var_name.push(*ch);
|
||||
input_chars.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.output.push_str(&var_name);
|
||||
self.push_style(Style::Blue);
|
||||
self.output.push('=');
|
||||
self.pop_style();
|
||||
}
|
||||
self.output.push_str(&var_name);
|
||||
self.push_style(Style::Blue);
|
||||
self.output.push('=');
|
||||
self.pop_style();
|
||||
}
|
||||
|
||||
markers::COMMAND => {
|
||||
let mut cmd_name = String::new();
|
||||
while let Some(ch) = input_chars.peek() {
|
||||
if *ch == markers::RESET {
|
||||
break;
|
||||
}
|
||||
cmd_name.push(*ch);
|
||||
input_chars.next();
|
||||
}
|
||||
let style = if Self::is_valid(&cmd_name) {
|
||||
Style::Green.into()
|
||||
} else {
|
||||
Style::Red | Style::Bold
|
||||
};
|
||||
self.push_style(style);
|
||||
self.output.push_str(&cmd_name);
|
||||
self.last_was_reset = false;
|
||||
}
|
||||
markers::CMD_SUB | markers::SUBSH | markers::PROC_SUB => {
|
||||
let mut inner = String::new();
|
||||
let mut incomplete = true;
|
||||
let end_marker = match ch {
|
||||
markers::CMD_SUB => markers::CMD_SUB_END,
|
||||
markers::SUBSH => markers::SUBSH_END,
|
||||
markers::PROC_SUB => markers::PROC_SUB_END,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
while let Some(ch) = input_chars.peek() {
|
||||
if *ch == end_marker {
|
||||
incomplete = false;
|
||||
input_chars.next(); // consume the end marker
|
||||
break;
|
||||
}
|
||||
inner.push(*ch);
|
||||
input_chars.next();
|
||||
}
|
||||
markers::ARG => {
|
||||
let mut arg = String::new();
|
||||
let mut chars_clone = input_chars.clone();
|
||||
while let Some(ch) = chars_clone.next() {
|
||||
if ch == markers::RESET {
|
||||
break;
|
||||
}
|
||||
arg.push(ch);
|
||||
}
|
||||
|
||||
// Determine prefix from content (handles both <( and >( for proc subs)
|
||||
let prefix = match ch {
|
||||
markers::CMD_SUB => "$(",
|
||||
markers::SUBSH => "(",
|
||||
markers::PROC_SUB => {
|
||||
if inner.starts_with("<(") { "<(" }
|
||||
else if inner.starts_with(">(") { ">(" }
|
||||
else { "<(" } // fallback
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let style = if Self::is_filename(&arg) {
|
||||
Style::White | Style::Underline
|
||||
} else {
|
||||
Style::White.into()
|
||||
};
|
||||
|
||||
let inner_content = if incomplete {
|
||||
inner
|
||||
.strip_prefix(prefix)
|
||||
.unwrap_or(&inner)
|
||||
} else {
|
||||
inner
|
||||
.strip_prefix(prefix)
|
||||
.and_then(|s| s.strip_suffix(")"))
|
||||
.unwrap_or(&inner)
|
||||
};
|
||||
self.push_style(style);
|
||||
self.last_was_reset = false;
|
||||
}
|
||||
|
||||
let mut recursive_highlighter = Self::new();
|
||||
recursive_highlighter.load_input(inner_content);
|
||||
recursive_highlighter.highlight();
|
||||
self.push_style(Style::Blue);
|
||||
self.output.push_str(prefix);
|
||||
self.pop_style();
|
||||
self.output.push_str(&recursive_highlighter.take());
|
||||
if !incomplete {
|
||||
self.push_style(Style::Blue);
|
||||
self.output.push(')');
|
||||
self.pop_style();
|
||||
}
|
||||
self.last_was_reset = false;
|
||||
}
|
||||
markers::VAR_SUB => {
|
||||
let mut var_sub = String::new();
|
||||
while let Some(ch) = input_chars.peek() {
|
||||
if *ch == markers::VAR_SUB_END {
|
||||
input_chars.next(); // consume the end marker
|
||||
break;
|
||||
} else if markers::is_marker(*ch) {
|
||||
input_chars.next(); // skip the marker
|
||||
continue;
|
||||
}
|
||||
var_sub.push(*ch);
|
||||
input_chars.next();
|
||||
}
|
||||
let style = Style::Cyan;
|
||||
self.push_style(style);
|
||||
self.output.push_str(&var_sub);
|
||||
self.pop_style();
|
||||
}
|
||||
_ => {
|
||||
if markers::is_marker(ch) {
|
||||
} else {
|
||||
self.output.push(ch);
|
||||
self.last_was_reset = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
markers::COMMAND => {
|
||||
let mut cmd_name = String::new();
|
||||
let mut chars_clone = input_chars.clone();
|
||||
while let Some(ch) = chars_clone.next() {
|
||||
if ch == markers::RESET {
|
||||
break;
|
||||
}
|
||||
cmd_name.push(ch);
|
||||
}
|
||||
let style = if Self::is_valid(&cmd_name) {
|
||||
Style::Green.into()
|
||||
} else {
|
||||
Style::Red | Style::Bold
|
||||
};
|
||||
self.push_style(style);
|
||||
self.last_was_reset = false;
|
||||
}
|
||||
markers::CMD_SUB | markers::SUBSH | markers::PROC_SUB => {
|
||||
let mut inner = String::new();
|
||||
let mut incomplete = true;
|
||||
let end_marker = match ch {
|
||||
markers::CMD_SUB => markers::CMD_SUB_END,
|
||||
markers::SUBSH => markers::SUBSH_END,
|
||||
markers::PROC_SUB => markers::PROC_SUB_END,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
while let Some(ch) = input_chars.peek() {
|
||||
if *ch == end_marker {
|
||||
incomplete = false;
|
||||
input_chars.next(); // consume the end marker
|
||||
break;
|
||||
}
|
||||
inner.push(*ch);
|
||||
input_chars.next();
|
||||
}
|
||||
|
||||
/// Extracts the highlighted output and resets the highlighter state
|
||||
///
|
||||
/// Clears the input buffer, style stack, and returns the generated output
|
||||
/// containing ANSI escape codes. The highlighter is ready for reuse after this.
|
||||
pub fn take(&mut self) -> String {
|
||||
self.input.clear();
|
||||
self.clear_styles();
|
||||
std::mem::take(&mut self.output)
|
||||
}
|
||||
// Determine prefix from content (handles both <( and >( for proc subs)
|
||||
let prefix = match ch {
|
||||
markers::CMD_SUB => "$(",
|
||||
markers::SUBSH => "(",
|
||||
markers::PROC_SUB => {
|
||||
if inner.starts_with("<(") {
|
||||
"<("
|
||||
} else if inner.starts_with(">(") {
|
||||
">("
|
||||
} else {
|
||||
"<("
|
||||
} // fallback
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
/// Checks if a command name is valid (exists in PATH, is a function, or is an alias)
|
||||
///
|
||||
/// Searches:
|
||||
/// 1. Current directory if command is a path
|
||||
/// 2. All directories in PATH environment variable
|
||||
/// 3. Shell functions and aliases in the current shell state
|
||||
fn is_valid(command: &str) -> bool {
|
||||
let path = env::var("PATH").unwrap_or_default();
|
||||
let paths = path.split(':');
|
||||
let cmd_path = PathBuf::from(&command);
|
||||
let inner_content = if incomplete {
|
||||
inner.strip_prefix(prefix).unwrap_or(&inner)
|
||||
} else {
|
||||
inner
|
||||
.strip_prefix(prefix)
|
||||
.and_then(|s| s.strip_suffix(")"))
|
||||
.unwrap_or(&inner)
|
||||
};
|
||||
|
||||
if cmd_path.exists() {
|
||||
// the user has given us an absolute path
|
||||
if cmd_path.is_dir() && read_shopts(|o| o.core.autocd) {
|
||||
// this is a directory and autocd is enabled
|
||||
return true;
|
||||
} else {
|
||||
let Ok(meta) = cmd_path.metadata() else { return false };
|
||||
// this is a file that is executable by someone
|
||||
return meta.permissions().mode() & 0o111 == 0
|
||||
}
|
||||
} else {
|
||||
// they gave us a command name
|
||||
// now we must traverse the PATH env var
|
||||
// and see if we find any matches
|
||||
for path in paths {
|
||||
let path = PathBuf::from(path).join(command);
|
||||
if path.exists() {
|
||||
let Ok(meta) = path.metadata() else { continue };
|
||||
return meta.permissions().mode() & 0o111 != 0;
|
||||
}
|
||||
}
|
||||
let mut recursive_highlighter = Self::new();
|
||||
recursive_highlighter.load_input(inner_content);
|
||||
recursive_highlighter.highlight();
|
||||
self.push_style(Style::Blue);
|
||||
self.output.push_str(prefix);
|
||||
self.pop_style();
|
||||
self.output.push_str(&recursive_highlighter.take());
|
||||
if !incomplete {
|
||||
self.push_style(Style::Blue);
|
||||
self.output.push(')');
|
||||
self.pop_style();
|
||||
}
|
||||
self.last_was_reset = false;
|
||||
}
|
||||
markers::VAR_SUB => {
|
||||
let mut var_sub = String::new();
|
||||
while let Some(ch) = input_chars.peek() {
|
||||
if *ch == markers::VAR_SUB_END {
|
||||
input_chars.next(); // consume the end marker
|
||||
break;
|
||||
} else if markers::is_marker(*ch) {
|
||||
input_chars.next(); // skip the marker
|
||||
continue;
|
||||
}
|
||||
var_sub.push(*ch);
|
||||
input_chars.next();
|
||||
}
|
||||
let style = Style::Cyan;
|
||||
self.push_style(style);
|
||||
self.output.push_str(&var_sub);
|
||||
self.pop_style();
|
||||
}
|
||||
_ => {
|
||||
if markers::is_marker(ch) {
|
||||
} else {
|
||||
self.output.push(ch);
|
||||
self.last_was_reset = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// also check shell functions and aliases for any matches
|
||||
let found = read_logic(|l| l.get_func(command).is_some() || l.get_alias(command).is_some());
|
||||
if found {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
/// Extracts the highlighted output and resets the highlighter state
|
||||
///
|
||||
/// Clears the input buffer, style stack, and returns the generated output
|
||||
/// containing ANSI escape codes. The highlighter is ready for reuse after
|
||||
/// this.
|
||||
pub fn take(&mut self) -> String {
|
||||
self.input.clear();
|
||||
self.clear_styles();
|
||||
std::mem::take(&mut self.output)
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
/// Checks if a command name is valid (exists in PATH, is a function, or is an
|
||||
/// alias)
|
||||
///
|
||||
/// Searches:
|
||||
/// 1. Current directory if command is a path
|
||||
/// 2. All directories in PATH environment variable
|
||||
/// 3. Shell functions and aliases in the current shell state
|
||||
fn is_valid(command: &str) -> bool {
|
||||
let path = env::var("PATH").unwrap_or_default();
|
||||
let paths = path.split(':');
|
||||
let cmd_path = PathBuf::from(&command);
|
||||
|
||||
/// Emits a reset ANSI code to the output, with deduplication
|
||||
///
|
||||
/// Only emits the reset if the last emitted code was not already a reset,
|
||||
/// preventing redundant `\x1b[0m` sequences in the output.
|
||||
fn emit_reset(&mut self) {
|
||||
if !self.last_was_reset {
|
||||
self.output.push_str(&Style::Reset.to_string());
|
||||
self.last_was_reset = true;
|
||||
}
|
||||
}
|
||||
if cmd_path.exists() {
|
||||
// the user has given us an absolute path
|
||||
if cmd_path.is_dir() && read_shopts(|o| o.core.autocd) {
|
||||
// this is a directory and autocd is enabled
|
||||
return true;
|
||||
} else {
|
||||
let Ok(meta) = cmd_path.metadata() else {
|
||||
return false;
|
||||
};
|
||||
// this is a file that is executable by someone
|
||||
return meta.permissions().mode() & 0o111 == 0;
|
||||
}
|
||||
} else {
|
||||
// they gave us a command name
|
||||
// now we must traverse the PATH env var
|
||||
// and see if we find any matches
|
||||
for path in paths {
|
||||
let path = PathBuf::from(path).join(command);
|
||||
if path.exists() {
|
||||
let Ok(meta) = path.metadata() else { continue };
|
||||
return meta.permissions().mode() & 0o111 != 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Emits a style ANSI code to the output
|
||||
///
|
||||
/// Unconditionally appends the ANSI escape sequence for the given style
|
||||
/// and marks that we're no longer in a reset state.
|
||||
fn emit_style(&mut self, style: &StyleSet) {
|
||||
self.output.push_str(&style.to_string());
|
||||
self.last_was_reset = false;
|
||||
}
|
||||
// also check shell functions and aliases for any matches
|
||||
let found = read_logic(|l| l.get_func(command).is_some() || l.get_alias(command).is_some());
|
||||
if found {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Pushes a new style onto the stack and emits its ANSI code
|
||||
///
|
||||
/// Used when entering a new syntax context (string, variable, command, etc.).
|
||||
/// The style stack allows proper restoration when exiting nested constructs.
|
||||
pub fn push_style(&mut self, style: impl Into<StyleSet>) {
|
||||
let set: StyleSet = style.into();
|
||||
self.style_stack.push(set.clone());
|
||||
self.emit_style(&set);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Pops a style from the stack and restores the previous style
|
||||
///
|
||||
/// Used when exiting a syntax context. If there's a parent style on the stack,
|
||||
/// it's re-emitted to restore the previous color. Otherwise, emits a reset.
|
||||
/// This ensures colors are properly restored in nested constructs like
|
||||
/// `"string with $VAR"` where the string color resumes after the variable.
|
||||
pub fn pop_style(&mut self) {
|
||||
self.style_stack.pop();
|
||||
if let Some(style) = self.style_stack.last().cloned() {
|
||||
self.emit_style(&style);
|
||||
} else {
|
||||
self.emit_reset();
|
||||
}
|
||||
}
|
||||
fn is_filename(arg: &str) -> bool {
|
||||
let path = PathBuf::from(arg);
|
||||
|
||||
/// Clears all styles from the stack and emits a reset
|
||||
///
|
||||
/// Used at command separators and explicit reset markers to return to
|
||||
/// the default terminal color between independent commands.
|
||||
pub fn clear_styles(&mut self) {
|
||||
self.style_stack.clear();
|
||||
self.emit_reset();
|
||||
}
|
||||
if path.exists() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Simple marker-to-ANSI replacement (unused in favor of stack-based highlighting)
|
||||
///
|
||||
/// Performs direct string replacement of markers with ANSI codes, without
|
||||
/// handling nesting or proper color restoration. Kept for reference but not
|
||||
/// used in the current implementation.
|
||||
pub fn trivial_replace(&mut self) {
|
||||
self.input = self.input
|
||||
.replace([markers::RESET, markers::ARG], "\x1b[0m")
|
||||
.replace(markers::KEYWORD, "\x1b[33m")
|
||||
.replace(markers::CASE_PAT, "\x1b[34m")
|
||||
.replace(markers::COMMENT, "\x1b[90m")
|
||||
.replace(markers::OPERATOR, "\x1b[35m");
|
||||
}
|
||||
if let Some(parent_dir) = path.parent()
|
||||
&& let Ok(entries) = parent_dir.read_dir()
|
||||
{
|
||||
let files = entries
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.file_name().to_string_lossy().to_string())
|
||||
.collect::<Vec<_>>();
|
||||
let Some(arg_filename) = PathBuf::from(arg)
|
||||
.file_name()
|
||||
.map(|s| s.to_string_lossy().to_string())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
for file in files {
|
||||
if file.starts_with(&arg_filename) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Ok(this_dir) = env::current_dir()
|
||||
&& let Ok(entries) = this_dir.read_dir()
|
||||
{
|
||||
let this_dir_files = entries
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.file_name().to_string_lossy().to_string())
|
||||
.collect::<Vec<_>>();
|
||||
for file in this_dir_files {
|
||||
if file.starts_with(arg) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
false
|
||||
}
|
||||
|
||||
/// Emits a reset ANSI code to the output, with deduplication
|
||||
///
|
||||
/// Only emits the reset if the last emitted code was not already a reset,
|
||||
/// preventing redundant `\x1b[0m` sequences in the output.
|
||||
fn emit_reset(&mut self) {
|
||||
if !self.last_was_reset {
|
||||
self.output.push_str(&Style::Reset.to_string());
|
||||
self.last_was_reset = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Emits a style ANSI code to the output
|
||||
///
|
||||
/// Unconditionally appends the ANSI escape sequence for the given style
|
||||
/// and marks that we're no longer in a reset state.
|
||||
fn emit_style(&mut self, style: &StyleSet) {
|
||||
self.output.push_str(&style.to_string());
|
||||
self.last_was_reset = false;
|
||||
}
|
||||
|
||||
/// Pushes a new style onto the stack and emits its ANSI code
|
||||
///
|
||||
/// Used when entering a new syntax context (string, variable, command, etc.).
|
||||
/// The style stack allows proper restoration when exiting nested constructs.
|
||||
pub fn push_style(&mut self, style: impl Into<StyleSet>) {
|
||||
let set: StyleSet = style.into();
|
||||
self.style_stack.push(set.clone());
|
||||
self.emit_style(&set);
|
||||
}
|
||||
|
||||
/// Pops a style from the stack and restores the previous style
|
||||
///
|
||||
/// Used when exiting a syntax context. If there's a parent style on the
|
||||
/// stack, it's re-emitted to restore the previous color. Otherwise, emits a
|
||||
/// reset. This ensures colors are properly restored in nested constructs
|
||||
/// like `"string with $VAR"` where the string color resumes after the
|
||||
/// variable.
|
||||
pub fn pop_style(&mut self) {
|
||||
self.style_stack.pop();
|
||||
if let Some(style) = self.style_stack.last().cloned() {
|
||||
self.emit_style(&style);
|
||||
} else {
|
||||
self.emit_reset();
|
||||
}
|
||||
}
|
||||
|
||||
/// Clears all styles from the stack and emits a reset
|
||||
///
|
||||
/// Used at command separators and explicit reset markers to return to
|
||||
/// the default terminal color between independent commands.
|
||||
pub fn clear_styles(&mut self) {
|
||||
self.style_stack.clear();
|
||||
self.emit_reset();
|
||||
}
|
||||
|
||||
/// Simple marker-to-ANSI replacement (unused in favor of stack-based
|
||||
/// highlighting)
|
||||
///
|
||||
/// Performs direct string replacement of markers with ANSI codes, without
|
||||
/// handling nesting or proper color restoration. Kept for reference but not
|
||||
/// used in the current implementation.
|
||||
pub fn trivial_replace(&mut self) {
|
||||
self.input = self
|
||||
.input
|
||||
.replace([markers::RESET, markers::ARG], "\x1b[0m")
|
||||
.replace(markers::KEYWORD, "\x1b[33m")
|
||||
.replace(markers::CASE_PAT, "\x1b[34m")
|
||||
.replace(markers::COMMENT, "\x1b[90m")
|
||||
.replace(markers::OPERATOR, "\x1b[35m");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,8 +189,8 @@ fn read_hist_file(path: &Path) -> ShResult<Vec<HistEntry>> {
|
||||
Ok(raw.parse::<HistEntries>()?.0)
|
||||
}
|
||||
|
||||
/// Deduplicate entries, keeping only the most recent occurrence of each command.
|
||||
/// Preserves chronological order (oldest to newest).
|
||||
/// Deduplicate entries, keeping only the most recent occurrence of each
|
||||
/// command. Preserves chronological order (oldest to newest).
|
||||
fn dedupe_entries(entries: &[HistEntry]) -> Vec<HistEntry> {
|
||||
let mut seen = HashSet::new();
|
||||
// Iterate backwards (newest first), keeping first occurrence of each command
|
||||
@@ -207,10 +207,10 @@ fn dedupe_entries(entries: &[HistEntry]) -> Vec<HistEntry> {
|
||||
|
||||
pub struct History {
|
||||
path: PathBuf,
|
||||
pub pending: Option<String>,
|
||||
pub pending: Option<(String, usize)>, // command, cursor_pos
|
||||
entries: Vec<HistEntry>,
|
||||
search_mask: Vec<HistEntry>,
|
||||
no_matches: bool,
|
||||
no_matches: bool,
|
||||
pub cursor: usize,
|
||||
search_direction: Direction,
|
||||
ignore_dups: bool,
|
||||
@@ -235,9 +235,9 @@ impl History {
|
||||
Ok(Self {
|
||||
path,
|
||||
entries,
|
||||
pending: None,
|
||||
pending: None,
|
||||
search_mask,
|
||||
no_matches: false,
|
||||
no_matches: false,
|
||||
cursor,
|
||||
search_direction: Direction::Backward,
|
||||
ignore_dups,
|
||||
@@ -245,10 +245,10 @@ impl History {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.search_mask = dedupe_entries(&self.entries);
|
||||
self.cursor = self.search_mask.len();
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.search_mask = dedupe_entries(&self.entries);
|
||||
self.cursor = self.search_mask.len();
|
||||
}
|
||||
|
||||
pub fn entries(&self) -> &[HistEntry] {
|
||||
&self.entries
|
||||
@@ -270,14 +270,14 @@ impl History {
|
||||
self.cursor = self.search_mask.len();
|
||||
}
|
||||
|
||||
pub fn update_pending_cmd(&mut self, command: &str) {
|
||||
let cmd = command.to_string();
|
||||
pub fn update_pending_cmd(&mut self, buf: (&str, usize)) {
|
||||
let cmd = buf.0.to_string();
|
||||
let constraint = SearchConstraint {
|
||||
kind: SearchKind::Prefix,
|
||||
term: cmd.clone(),
|
||||
};
|
||||
|
||||
self.pending = Some(cmd);
|
||||
self.pending = Some((cmd, buf.1));
|
||||
self.constrain_entries(constraint);
|
||||
}
|
||||
|
||||
@@ -315,11 +315,11 @@ impl History {
|
||||
.collect();
|
||||
|
||||
self.search_mask = dedupe_entries(&filtered);
|
||||
self.no_matches = self.search_mask.is_empty();
|
||||
if self.no_matches {
|
||||
// If no matches, reset to full history so user can still scroll through it
|
||||
self.search_mask = dedupe_entries(&self.entries);
|
||||
}
|
||||
self.no_matches = self.search_mask.is_empty();
|
||||
if self.no_matches {
|
||||
// If no matches, reset to full history so user can still scroll through it
|
||||
self.search_mask = dedupe_entries(&self.entries);
|
||||
}
|
||||
}
|
||||
self.cursor = self.search_mask.len();
|
||||
}
|
||||
@@ -328,12 +328,14 @@ impl History {
|
||||
}
|
||||
|
||||
pub fn hint_entry(&self) -> Option<&HistEntry> {
|
||||
if self.no_matches { return None };
|
||||
if self.no_matches {
|
||||
return None;
|
||||
};
|
||||
self.search_mask.last()
|
||||
}
|
||||
|
||||
pub fn get_hint(&self) -> Option<String> {
|
||||
if self.at_pending() && self.pending.as_ref().is_some_and(|p| !p.is_empty()) {
|
||||
if self.at_pending() && self.pending.as_ref().is_some_and(|p| !p.0.is_empty()) {
|
||||
let entry = self.hint_entry()?;
|
||||
Some(entry.command().to_string())
|
||||
} else {
|
||||
@@ -342,9 +344,15 @@ impl History {
|
||||
}
|
||||
|
||||
pub fn scroll(&mut self, offset: isize) -> Option<&HistEntry> {
|
||||
self.cursor = self.cursor.saturating_add_signed(offset).clamp(0, self.search_mask.len());
|
||||
self.cursor = self
|
||||
.cursor
|
||||
.saturating_add_signed(offset)
|
||||
.clamp(0, self.search_mask.len());
|
||||
|
||||
log::debug!("Scrolling history by offset {offset} from cursor at index {}", self.cursor);
|
||||
log::debug!(
|
||||
"Scrolling history by offset {offset} from cursor at index {}",
|
||||
self.cursor
|
||||
);
|
||||
self.search_mask.get(self.cursor)
|
||||
}
|
||||
|
||||
@@ -378,7 +386,8 @@ impl History {
|
||||
|
||||
let last_file_entry = self
|
||||
.entries
|
||||
.iter().rfind(|ent| !ent.new)
|
||||
.iter()
|
||||
.rfind(|ent| !ent.new)
|
||||
.map(|ent| ent.command.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -399,8 +408,8 @@ impl History {
|
||||
}
|
||||
|
||||
file.write_all(data.as_bytes())?;
|
||||
self.pending = None;
|
||||
self.reset();
|
||||
self.pending = None;
|
||||
self.reset();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -133,10 +133,10 @@ impl SelectMode {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum MotionKind {
|
||||
To(usize), // Absolute position, exclusive
|
||||
On(usize), // Absolute position, inclusive
|
||||
Onto(usize), /* Absolute position, operations include the position but motions
|
||||
* exclude it (wtf vim) */
|
||||
To(usize), // Absolute position, exclusive
|
||||
On(usize), // Absolute position, inclusive
|
||||
Onto(usize), /* Absolute position, operations include the position but motions
|
||||
* exclude it (wtf vim) */
|
||||
Inclusive((usize, usize)), // Range, inclusive
|
||||
Exclusive((usize, usize)), // Range, exclusive
|
||||
|
||||
@@ -360,12 +360,12 @@ impl LineBuf {
|
||||
pub fn set_hint(&mut self, hint: Option<String>) {
|
||||
if let Some(hint) = hint {
|
||||
if let Some(hint) = hint.strip_prefix(&self.buffer) {
|
||||
if !hint.is_empty() {
|
||||
self.hint = Some(hint.to_string())
|
||||
} else {
|
||||
self.hint = None
|
||||
}
|
||||
}
|
||||
if !hint.is_empty() {
|
||||
self.hint = Some(hint.to_string())
|
||||
} else {
|
||||
self.hint = None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.hint = None
|
||||
}
|
||||
@@ -563,8 +563,8 @@ impl LineBuf {
|
||||
self.update_graphemes();
|
||||
}
|
||||
pub fn drain(&mut self, start: usize, end: usize) -> String {
|
||||
let start = start.max(0);
|
||||
let end = end.min(self.grapheme_indices().len());
|
||||
let start = start.max(0);
|
||||
let end = end.min(self.grapheme_indices().len());
|
||||
let drained = if end == self.grapheme_indices().len() {
|
||||
if start == self.grapheme_indices().len() {
|
||||
return String::new();
|
||||
@@ -628,8 +628,9 @@ impl LineBuf {
|
||||
self.next_sentence_start_from_punctuation(pos).is_some()
|
||||
}
|
||||
|
||||
/// If position is at sentence-ending punctuation, returns the position of the next sentence start.
|
||||
/// Handles closing delimiters (`)`, `]`, `"`, `'`) after punctuation.
|
||||
/// If position is at sentence-ending punctuation, returns the position of the
|
||||
/// next sentence start. Handles closing delimiters (`)`, `]`, `"`, `'`)
|
||||
/// after punctuation.
|
||||
#[allow(clippy::collapsible_if)]
|
||||
pub fn next_sentence_start_from_punctuation(&self, pos: usize) -> Option<usize> {
|
||||
if let Some(gr) = self.read_grapheme_at(pos) {
|
||||
@@ -956,9 +957,10 @@ impl LineBuf {
|
||||
let start = start.unwrap_or(0);
|
||||
|
||||
if count > 1
|
||||
&& let Some((_, new_end)) = self.text_obj_sentence(end, count - 1, bound) {
|
||||
end = new_end;
|
||||
}
|
||||
&& let Some((_, new_end)) = self.text_obj_sentence(end, count - 1, bound)
|
||||
{
|
||||
end = new_end;
|
||||
}
|
||||
|
||||
Some((start, end))
|
||||
}
|
||||
@@ -1363,7 +1365,12 @@ impl LineBuf {
|
||||
}
|
||||
|
||||
/// Find the start of the next word forward
|
||||
pub fn start_of_word_forward(&mut self, mut pos: usize, word: Word, include_last_char: bool) -> usize {
|
||||
pub fn start_of_word_forward(
|
||||
&mut self,
|
||||
mut pos: usize,
|
||||
word: Word,
|
||||
include_last_char: bool,
|
||||
) -> usize {
|
||||
let default = self.grapheme_indices().len();
|
||||
let mut indices_iter = (pos..self.cursor.max).peekable();
|
||||
|
||||
@@ -1390,8 +1397,7 @@ impl LineBuf {
|
||||
let on_whitespace = is_whitespace(&cur_char);
|
||||
|
||||
if !on_whitespace {
|
||||
let Some(ws_pos) =
|
||||
indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace))
|
||||
let Some(ws_pos) = indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace))
|
||||
else {
|
||||
return default;
|
||||
};
|
||||
@@ -1457,7 +1463,12 @@ impl LineBuf {
|
||||
}
|
||||
|
||||
/// Find the end of the previous word backward
|
||||
pub fn end_of_word_backward(&mut self, mut pos: usize, word: Word, include_last_char: bool) -> usize {
|
||||
pub fn end_of_word_backward(
|
||||
&mut self,
|
||||
mut pos: usize,
|
||||
word: Word,
|
||||
include_last_char: bool,
|
||||
) -> usize {
|
||||
let default = self.grapheme_indices().len();
|
||||
let mut indices_iter = (0..pos).rev().peekable();
|
||||
|
||||
@@ -1484,8 +1495,7 @@ impl LineBuf {
|
||||
let on_whitespace = is_whitespace(&cur_char);
|
||||
|
||||
if !on_whitespace {
|
||||
let Some(ws_pos) =
|
||||
indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace))
|
||||
let Some(ws_pos) = indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace))
|
||||
else {
|
||||
return default;
|
||||
};
|
||||
@@ -1742,11 +1752,7 @@ impl LineBuf {
|
||||
};
|
||||
pos = next_ws_pos;
|
||||
|
||||
if pos == 0 {
|
||||
pos
|
||||
} else {
|
||||
pos + 1
|
||||
}
|
||||
if pos == 0 { pos } else { pos + 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1903,7 +1909,7 @@ impl LineBuf {
|
||||
&& self.grapheme_at(target_pos) == Some("\n")
|
||||
{
|
||||
target_pos = target_pos.saturating_sub(1); // Don't land on the
|
||||
// newline
|
||||
// newline
|
||||
}
|
||||
MotionKind::InclusiveWithTargetCol((start, end), target_pos)
|
||||
}
|
||||
@@ -2141,7 +2147,7 @@ impl LineBuf {
|
||||
&& self.grapheme_at(target_pos) == Some("\n")
|
||||
{
|
||||
target_pos = target_pos.saturating_sub(1); // Don't land on the
|
||||
// newline
|
||||
// newline
|
||||
}
|
||||
|
||||
let (start, end) = match motion.1 {
|
||||
@@ -2575,15 +2581,16 @@ impl LineBuf {
|
||||
}
|
||||
Verb::SwapVisualAnchor => {
|
||||
if let Some((start, end)) = self.select_range()
|
||||
&& let Some(mut mode) = self.select_mode {
|
||||
mode.invert_anchor();
|
||||
let new_cursor_pos = match mode.anchor() {
|
||||
SelectAnchor::Start => start,
|
||||
SelectAnchor::End => end,
|
||||
};
|
||||
self.cursor.set(new_cursor_pos);
|
||||
self.select_mode = Some(mode)
|
||||
}
|
||||
&& let Some(mut mode) = self.select_mode
|
||||
{
|
||||
mode.invert_anchor();
|
||||
let new_cursor_pos = match mode.anchor() {
|
||||
SelectAnchor::Start => start,
|
||||
SelectAnchor::End => end,
|
||||
};
|
||||
self.cursor.set(new_cursor_pos);
|
||||
self.select_mode = Some(mode)
|
||||
}
|
||||
}
|
||||
Verb::JoinLines => {
|
||||
let start = self.start_of_line();
|
||||
@@ -2731,10 +2738,12 @@ impl LineBuf {
|
||||
let edit_is_merging = self.undo_stack.last().is_some_and(|edit| edit.merging);
|
||||
|
||||
// Merge character inserts into one edit
|
||||
if edit_is_merging && cmd.verb.as_ref().is_none_or(|v| !v.1.is_char_insert())
|
||||
&& let Some(edit) = self.undo_stack.last_mut() {
|
||||
edit.stop_merge();
|
||||
}
|
||||
if edit_is_merging
|
||||
&& cmd.verb.as_ref().is_none_or(|v| !v.1.is_char_insert())
|
||||
&& let Some(edit) = self.undo_stack.last_mut()
|
||||
{
|
||||
edit.stop_merge();
|
||||
}
|
||||
|
||||
let ViCmd {
|
||||
register,
|
||||
@@ -2821,10 +2830,9 @@ impl LineBuf {
|
||||
self.saved_col = None;
|
||||
}
|
||||
|
||||
if is_char_insert
|
||||
&& let Some(edit) = self.undo_stack.last_mut() {
|
||||
edit.start_merge();
|
||||
}
|
||||
if is_char_insert && let Some(edit) = self.undo_stack.last_mut() {
|
||||
edit.start_merge();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2832,9 +2840,13 @@ impl LineBuf {
|
||||
&self.buffer // FIXME: this will have to be fixed up later
|
||||
}
|
||||
|
||||
pub fn get_hint_text(&self) -> String {
|
||||
self.hint.clone().map(|h| h.styled(Style::BrightBlack)).unwrap_or_default()
|
||||
}
|
||||
pub fn get_hint_text(&self) -> String {
|
||||
self
|
||||
.hint
|
||||
.clone()
|
||||
.map(|h| h.styled(Style::BrightBlack))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for LineBuf {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,11 +17,15 @@ use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||||
use vte::{Parser, Perform};
|
||||
|
||||
use crate::{prelude::*, procio::borrow_fd, state::{read_meta, write_meta}};
|
||||
use crate::{
|
||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||
prompt::readline::keys::{KeyCode, ModKeys},
|
||||
};
|
||||
use crate::{
|
||||
prelude::*,
|
||||
procio::borrow_fd,
|
||||
state::{read_meta, write_meta},
|
||||
};
|
||||
|
||||
use super::{keys::KeyEvent, linebuf::LineBuf};
|
||||
|
||||
@@ -41,7 +45,7 @@ pub fn raw_mode() -> RawModeGuard {
|
||||
)
|
||||
.expect("Failed to set terminal to raw mode");
|
||||
|
||||
let (cols, rows) = get_win_size(STDIN_FILENO);
|
||||
let (cols, rows) = get_win_size(STDIN_FILENO);
|
||||
|
||||
RawModeGuard {
|
||||
orig,
|
||||
@@ -242,9 +246,7 @@ impl Read for TermBuffer {
|
||||
let result = nix::unistd::read(self.tty, buf);
|
||||
match result {
|
||||
Ok(n) => Ok(n),
|
||||
Err(Errno::EINTR) => {
|
||||
Err(Errno::EINTR.into())
|
||||
}
|
||||
Err(Errno::EINTR) => Err(Errno::EINTR.into()),
|
||||
Err(e) => Err(std::io::Error::from_raw_os_error(e as i32)),
|
||||
}
|
||||
}
|
||||
@@ -280,17 +282,21 @@ impl RawModeGuard {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_cooked_mode<F, R>(f: F) -> R
|
||||
where F: FnOnce() -> R {
|
||||
let raw = tcgetattr(borrow_fd(STDIN_FILENO)).expect("Failed to get terminal attributes");
|
||||
let mut cooked = raw.clone();
|
||||
cooked.local_flags |= termios::LocalFlags::ICANON | termios::LocalFlags::ECHO;
|
||||
cooked.input_flags |= termios::InputFlags::ICRNL;
|
||||
tcsetattr(borrow_fd(STDIN_FILENO), termios::SetArg::TCSANOW, &cooked).expect("Failed to set cooked mode");
|
||||
let res = f();
|
||||
tcsetattr(borrow_fd(STDIN_FILENO), termios::SetArg::TCSANOW, &raw).expect("Failed to restore raw mode");
|
||||
res
|
||||
}
|
||||
pub fn with_cooked_mode<F, R>(f: F) -> R
|
||||
where
|
||||
F: FnOnce() -> R,
|
||||
{
|
||||
let raw = tcgetattr(borrow_fd(STDIN_FILENO)).expect("Failed to get terminal attributes");
|
||||
let mut cooked = raw.clone();
|
||||
cooked.local_flags |= termios::LocalFlags::ICANON | termios::LocalFlags::ECHO;
|
||||
cooked.input_flags |= termios::InputFlags::ICRNL;
|
||||
tcsetattr(borrow_fd(STDIN_FILENO), termios::SetArg::TCSANOW, &cooked)
|
||||
.expect("Failed to set cooked mode");
|
||||
let res = f();
|
||||
tcsetattr(borrow_fd(STDIN_FILENO), termios::SetArg::TCSANOW, &raw)
|
||||
.expect("Failed to restore raw mode");
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for RawModeGuard {
|
||||
@@ -333,9 +339,15 @@ impl KeyCollector {
|
||||
// CSI modifiers: param = 1 + (shift) + (alt*2) + (ctrl*4) + (meta*8)
|
||||
let bits = param.saturating_sub(1);
|
||||
let mut mods = ModKeys::empty();
|
||||
if bits & 1 != 0 { mods |= ModKeys::SHIFT; }
|
||||
if bits & 2 != 0 { mods |= ModKeys::ALT; }
|
||||
if bits & 4 != 0 { mods |= ModKeys::CTRL; }
|
||||
if bits & 1 != 0 {
|
||||
mods |= ModKeys::SHIFT;
|
||||
}
|
||||
if bits & 2 != 0 {
|
||||
mods |= ModKeys::ALT;
|
||||
}
|
||||
if bits & 4 != 0 {
|
||||
mods |= ModKeys::CTRL;
|
||||
}
|
||||
mods
|
||||
}
|
||||
}
|
||||
@@ -374,46 +386,72 @@ impl Perform for KeyCollector {
|
||||
self.push(event);
|
||||
}
|
||||
|
||||
fn csi_dispatch(&mut self, params: &vte::Params, intermediates: &[u8], _ignore: bool, action: char) {
|
||||
let params: Vec<u16> = params.iter()
|
||||
fn csi_dispatch(
|
||||
&mut self,
|
||||
params: &vte::Params,
|
||||
intermediates: &[u8],
|
||||
_ignore: bool,
|
||||
action: char,
|
||||
) {
|
||||
let params: Vec<u16> = params
|
||||
.iter()
|
||||
.map(|p| p.first().copied().unwrap_or(0))
|
||||
.collect();
|
||||
|
||||
let event = match (intermediates, action) {
|
||||
// Arrow keys: CSI A/B/C/D or CSI 1;mod A/B/C/D
|
||||
([], 'A') => {
|
||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
||||
let mods = params
|
||||
.get(1)
|
||||
.map(|&m| Self::parse_modifiers(m))
|
||||
.unwrap_or(ModKeys::empty());
|
||||
KeyEvent(KeyCode::Up, mods)
|
||||
}
|
||||
([], 'B') => {
|
||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
||||
let mods = params
|
||||
.get(1)
|
||||
.map(|&m| Self::parse_modifiers(m))
|
||||
.unwrap_or(ModKeys::empty());
|
||||
KeyEvent(KeyCode::Down, mods)
|
||||
}
|
||||
([], 'C') => {
|
||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
||||
let mods = params
|
||||
.get(1)
|
||||
.map(|&m| Self::parse_modifiers(m))
|
||||
.unwrap_or(ModKeys::empty());
|
||||
KeyEvent(KeyCode::Right, mods)
|
||||
}
|
||||
([], 'D') => {
|
||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
||||
let mods = params
|
||||
.get(1)
|
||||
.map(|&m| Self::parse_modifiers(m))
|
||||
.unwrap_or(ModKeys::empty());
|
||||
KeyEvent(KeyCode::Left, mods)
|
||||
}
|
||||
// Home/End: CSI H/F or CSI 1;mod H/F
|
||||
([], 'H') => {
|
||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
||||
let mods = params
|
||||
.get(1)
|
||||
.map(|&m| Self::parse_modifiers(m))
|
||||
.unwrap_or(ModKeys::empty());
|
||||
KeyEvent(KeyCode::Home, mods)
|
||||
}
|
||||
([], 'F') => {
|
||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
||||
let mods = params
|
||||
.get(1)
|
||||
.map(|&m| Self::parse_modifiers(m))
|
||||
.unwrap_or(ModKeys::empty());
|
||||
KeyEvent(KeyCode::End, mods)
|
||||
}
|
||||
// Shift+Tab: CSI Z
|
||||
([], 'Z') => {
|
||||
KeyEvent(KeyCode::Tab, ModKeys::SHIFT)
|
||||
}
|
||||
([], 'Z') => KeyEvent(KeyCode::Tab, ModKeys::SHIFT),
|
||||
// Special keys with tilde: CSI num ~ or CSI num;mod ~
|
||||
([], '~') => {
|
||||
let key_num = params.first().copied().unwrap_or(0);
|
||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
||||
let mods = params
|
||||
.get(1)
|
||||
.map(|&m| Self::parse_modifiers(m))
|
||||
.unwrap_or(ModKeys::empty());
|
||||
let key = match key_num {
|
||||
1 | 7 => KeyCode::Home,
|
||||
2 => KeyCode::Insert,
|
||||
@@ -473,7 +511,9 @@ impl PollReader {
|
||||
pub fn feed_bytes(&mut self, bytes: &[u8]) {
|
||||
if bytes == [b'\x1b'] {
|
||||
// Single escape byte - user pressed ESC key
|
||||
self.collector.push(KeyEvent(KeyCode::Esc, ModKeys::empty()));
|
||||
self
|
||||
.collector
|
||||
.push(KeyEvent(KeyCode::Esc, ModKeys::empty()));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -914,13 +954,13 @@ impl LineWriter for TermWriter {
|
||||
let end = new_layout.end;
|
||||
let cursor = new_layout.cursor;
|
||||
|
||||
if read_meta(|m| m.system_msg_pending()) {
|
||||
let mut system_msg = String::new();
|
||||
while let Some(msg) = write_meta(|m| m.pop_system_message()) {
|
||||
writeln!(system_msg, "{msg}").map_err(err)?;
|
||||
}
|
||||
self.buffer.push_str(&system_msg);
|
||||
}
|
||||
if read_meta(|m| m.system_msg_pending()) {
|
||||
let mut system_msg = String::new();
|
||||
while let Some(msg) = write_meta(|m| m.pop_system_message()) {
|
||||
writeln!(system_msg, "{msg}").map_err(err)?;
|
||||
}
|
||||
self.buffer.push_str(&system_msg);
|
||||
}
|
||||
|
||||
self.buffer.push_str(prompt);
|
||||
self.buffer.push_str(line);
|
||||
|
||||
@@ -161,14 +161,16 @@ impl ViCmd {
|
||||
}
|
||||
/// If a ViCmd has a linewise motion, but no verb, we change it to charwise
|
||||
pub fn alter_line_motion_if_no_verb(&mut self) {
|
||||
if self.is_line_motion() && self.verb.is_none()
|
||||
&& let Some(motion) = self.motion.as_mut() {
|
||||
match motion.1 {
|
||||
Motion::LineUp => motion.1 = Motion::LineUpCharwise,
|
||||
Motion::LineDown => motion.1 = Motion::LineDownCharwise,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
if self.is_line_motion()
|
||||
&& self.verb.is_none()
|
||||
&& let Some(motion) = self.motion.as_mut()
|
||||
{
|
||||
match motion.1 {
|
||||
Motion::LineUp => motion.1 = Motion::LineUpCharwise,
|
||||
Motion::LineDown => motion.1 = Motion::LineDownCharwise,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn is_mode_transition(&self) -> bool {
|
||||
self.verb.as_ref().is_some_and(|v| {
|
||||
|
||||
@@ -315,7 +315,7 @@ impl ViNormal {
|
||||
return match obj {
|
||||
TextObj::Sentence(_) | TextObj::Paragraph(_) => CmdState::Complete,
|
||||
_ => CmdState::Invalid,
|
||||
}
|
||||
};
|
||||
}
|
||||
Some(_) => return CmdState::Complete,
|
||||
None => return CmdState::Pending,
|
||||
@@ -410,7 +410,7 @@ impl ViNormal {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'~' => {
|
||||
chars_clone.next();
|
||||
@@ -445,7 +445,7 @@ impl ViNormal {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'x' => {
|
||||
return Some(ViCmd {
|
||||
@@ -454,7 +454,7 @@ impl ViNormal {
|
||||
motion: Some(MotionCmd(1, Motion::ForwardChar)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'X' => {
|
||||
return Some(ViCmd {
|
||||
@@ -463,7 +463,7 @@ impl ViNormal {
|
||||
motion: Some(MotionCmd(1, Motion::BackwardChar)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
's' => {
|
||||
return Some(ViCmd {
|
||||
@@ -472,7 +472,7 @@ impl ViNormal {
|
||||
motion: Some(MotionCmd(1, Motion::ForwardChar)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'S' => {
|
||||
return Some(ViCmd {
|
||||
@@ -481,7 +481,7 @@ impl ViNormal {
|
||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'p' => {
|
||||
chars = chars_clone;
|
||||
@@ -516,7 +516,7 @@ impl ViNormal {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'~' => {
|
||||
return Some(ViCmd {
|
||||
@@ -525,7 +525,7 @@ impl ViNormal {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'u' => {
|
||||
return Some(ViCmd {
|
||||
@@ -534,7 +534,7 @@ impl ViNormal {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'v' => {
|
||||
return Some(ViCmd {
|
||||
@@ -543,7 +543,7 @@ impl ViNormal {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'V' => {
|
||||
return Some(ViCmd {
|
||||
@@ -552,7 +552,7 @@ impl ViNormal {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'o' => {
|
||||
return Some(ViCmd {
|
||||
@@ -561,7 +561,7 @@ impl ViNormal {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'O' => {
|
||||
return Some(ViCmd {
|
||||
@@ -570,7 +570,7 @@ impl ViNormal {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'a' => {
|
||||
return Some(ViCmd {
|
||||
@@ -579,7 +579,7 @@ impl ViNormal {
|
||||
motion: Some(MotionCmd(1, Motion::ForwardChar)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'A' => {
|
||||
return Some(ViCmd {
|
||||
@@ -588,7 +588,7 @@ impl ViNormal {
|
||||
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'i' => {
|
||||
return Some(ViCmd {
|
||||
@@ -597,7 +597,7 @@ impl ViNormal {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'I' => {
|
||||
return Some(ViCmd {
|
||||
@@ -606,7 +606,7 @@ impl ViNormal {
|
||||
motion: Some(MotionCmd(1, Motion::BeginningOfFirstWord)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'J' => {
|
||||
return Some(ViCmd {
|
||||
@@ -615,7 +615,7 @@ impl ViNormal {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'y' => {
|
||||
chars = chars_clone;
|
||||
@@ -636,7 +636,7 @@ impl ViNormal {
|
||||
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'D' => {
|
||||
return Some(ViCmd {
|
||||
@@ -645,7 +645,7 @@ impl ViNormal {
|
||||
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'C' => {
|
||||
return Some(ViCmd {
|
||||
@@ -654,7 +654,7 @@ impl ViNormal {
|
||||
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'=' => {
|
||||
chars = chars_clone;
|
||||
@@ -684,7 +684,7 @@ impl ViNormal {
|
||||
| ('~', Some(VerbCmd(_, Verb::ToggleCaseRange)))
|
||||
| ('>', Some(VerbCmd(_, Verb::Indent)))
|
||||
| ('<', Some(VerbCmd(_, Verb::Dedent))) => {
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::WholeLine))
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::WholeLine));
|
||||
}
|
||||
('W', Some(VerbCmd(_, Verb::Change))) => {
|
||||
// Same with 'W'
|
||||
@@ -994,8 +994,7 @@ impl ViNormal {
|
||||
}
|
||||
};
|
||||
|
||||
if chars.peek().is_some() {
|
||||
}
|
||||
if chars.peek().is_some() {}
|
||||
|
||||
let verb_ref = verb.as_ref().map(|v| &v.1);
|
||||
let motion_ref = motion.as_ref().map(|m| &m.1);
|
||||
@@ -1185,7 +1184,7 @@ impl ViVisual {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'?' => {
|
||||
return Some(ViCmd {
|
||||
@@ -1194,7 +1193,7 @@ impl ViVisual {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
_ => break 'verb_parse None,
|
||||
}
|
||||
@@ -1209,7 +1208,7 @@ impl ViVisual {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'x' => {
|
||||
chars = chars_clone;
|
||||
@@ -1222,7 +1221,7 @@ impl ViVisual {
|
||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'Y' => {
|
||||
return Some(ViCmd {
|
||||
@@ -1231,7 +1230,7 @@ impl ViVisual {
|
||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'D' => {
|
||||
return Some(ViCmd {
|
||||
@@ -1240,7 +1239,7 @@ impl ViVisual {
|
||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'R' | 'C' => {
|
||||
return Some(ViCmd {
|
||||
@@ -1249,7 +1248,7 @@ impl ViVisual {
|
||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'>' => {
|
||||
return Some(ViCmd {
|
||||
@@ -1258,7 +1257,7 @@ impl ViVisual {
|
||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'<' => {
|
||||
return Some(ViCmd {
|
||||
@@ -1267,7 +1266,7 @@ impl ViVisual {
|
||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'=' => {
|
||||
return Some(ViCmd {
|
||||
@@ -1276,7 +1275,7 @@ impl ViVisual {
|
||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'p' | 'P' => {
|
||||
chars = chars_clone;
|
||||
@@ -1299,7 +1298,7 @@ impl ViVisual {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'u' => {
|
||||
return Some(ViCmd {
|
||||
@@ -1308,7 +1307,7 @@ impl ViVisual {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'U' => {
|
||||
return Some(ViCmd {
|
||||
@@ -1317,7 +1316,7 @@ impl ViVisual {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'O' | 'o' => {
|
||||
return Some(ViCmd {
|
||||
@@ -1326,7 +1325,7 @@ impl ViVisual {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'A' => {
|
||||
return Some(ViCmd {
|
||||
@@ -1335,7 +1334,7 @@ impl ViVisual {
|
||||
motion: Some(MotionCmd(1, Motion::ForwardChar)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'I' => {
|
||||
return Some(ViCmd {
|
||||
@@ -1344,7 +1343,7 @@ impl ViVisual {
|
||||
motion: Some(MotionCmd(1, Motion::BeginningOfLine)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'J' => {
|
||||
return Some(ViCmd {
|
||||
@@ -1353,7 +1352,7 @@ impl ViVisual {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'y' => {
|
||||
chars = chars_clone;
|
||||
@@ -1395,7 +1394,7 @@ impl ViVisual {
|
||||
| ('=', Some(VerbCmd(_, Verb::Equalize)))
|
||||
| ('>', Some(VerbCmd(_, Verb::Indent)))
|
||||
| ('<', Some(VerbCmd(_, Verb::Dedent))) => {
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::WholeLine))
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::WholeLine));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -1652,8 +1651,7 @@ impl ViVisual {
|
||||
}
|
||||
};
|
||||
|
||||
if chars.peek().is_some() {
|
||||
}
|
||||
if chars.peek().is_some() {}
|
||||
|
||||
let verb_ref = verb.as_ref().map(|v| &v.1);
|
||||
let motion_ref = motion.as_ref().map(|m| &m.1);
|
||||
|
||||
Reference in New Issue
Block a user