command arguments are now underlined if they match an existing path -m ran rustfmt on the entire codebase

This commit is contained in:
2026-02-19 21:32:03 -05:00
parent b668dab522
commit a18a0b622f
44 changed files with 5549 additions and 5019 deletions

View File

@@ -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)
}

View File

@@ -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()
}
}

View File

@@ -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");
}
}

View File

@@ -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(())
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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| {

View File

@@ -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);