Various line editor fixes and optimizations
This commit is contained in:
@@ -51,7 +51,7 @@ pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
|
||||
unreachable!()
|
||||
};
|
||||
assert!(!argv.is_empty());
|
||||
let (argv, opts) = get_opts_from_tokens(argv, &ECHO_OPTS);
|
||||
let (argv, opts) = get_opts_from_tokens(argv, &ECHO_OPTS)?;
|
||||
let flags = get_echo_flags(opts).blame(blame)?;
|
||||
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let (argv, opts) = get_opts_from_tokens(argv, &READ_OPTS);
|
||||
let (argv, opts) = get_opts_from_tokens(argv, &READ_OPTS)?;
|
||||
let read_opts = get_read_flags(opts).blame(blame.clone())?;
|
||||
let (argv, _) = setup_builtin(argv, job, None).blame(blame.clone())?;
|
||||
|
||||
@@ -81,6 +81,8 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S
|
||||
write(borrow_fd(STDOUT_FILENO), prompt.as_bytes())?;
|
||||
}
|
||||
|
||||
log::info!("read_builtin: starting read with delim={}", read_opts.delim as char);
|
||||
|
||||
let input = if isatty(STDIN_FILENO)? {
|
||||
// Restore default terminal settings
|
||||
RawModeGuard::with_cooked_mode(|| {
|
||||
@@ -143,15 +145,12 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S
|
||||
let mut input: Vec<u8> = vec![];
|
||||
loop {
|
||||
let mut buf = [0u8; 1];
|
||||
log::info!("read: about to call read()");
|
||||
match read(STDIN_FILENO, &mut buf) {
|
||||
Ok(0) => {
|
||||
log::info!("read: got EOF");
|
||||
state::set_status(1);
|
||||
break; // EOF
|
||||
}
|
||||
Ok(n) => {
|
||||
log::info!("read: got {} bytes: {:?}", n, &buf[..1]);
|
||||
if buf[0] == read_opts.delim {
|
||||
state::set_status(0);
|
||||
break; // Delimiter reached, stop reading
|
||||
@@ -160,7 +159,6 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S
|
||||
}
|
||||
Err(Errno::EINTR) => {
|
||||
let pending = crate::signal::sigint_pending();
|
||||
log::info!("read: got EINTR, sigint_pending={}", pending);
|
||||
if pending {
|
||||
state::set_status(130);
|
||||
break;
|
||||
@@ -168,7 +166,6 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
log::info!("read: got error: {}", e);
|
||||
return Err(ShErr::simple(
|
||||
ShErrKind::ExecFail,
|
||||
format!("read: Failed to read from stdin: {e}"),
|
||||
@@ -186,8 +183,8 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S
|
||||
|
||||
if argv.is_empty() {
|
||||
write_vars(|v| {
|
||||
v.set_var("REPLY", &input, VarFlags::NONE);
|
||||
});
|
||||
v.set_var("REPLY", &input, VarFlags::NONE)
|
||||
})?;
|
||||
} else {
|
||||
// get our field separator
|
||||
let mut field_sep = read_vars(|v| v.get_var("IFS"));
|
||||
@@ -199,7 +196,7 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S
|
||||
for (i, arg) in argv.iter().enumerate() {
|
||||
if i == argv.len() - 1 {
|
||||
// Last arg, stuff the rest of the input into it
|
||||
write_vars(|v| v.set_var(&arg.0, &remaining, VarFlags::NONE));
|
||||
write_vars(|v| v.set_var(&arg.0, &remaining, VarFlags::NONE))?;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -209,13 +206,13 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S
|
||||
if let Some(idx) = trimmed.find(|c: char| field_sep.contains(c)) {
|
||||
// We found a field separator, split at the char index
|
||||
let (field, rest) = trimmed.split_at(idx);
|
||||
write_vars(|v| v.set_var(&arg.0, field, VarFlags::NONE));
|
||||
write_vars(|v| v.set_var(&arg.0, field, VarFlags::NONE))?;
|
||||
|
||||
// note that this doesn't account for consecutive IFS characters, which is what
|
||||
// that trim above is for
|
||||
remaining = rest.to_string();
|
||||
} else {
|
||||
write_vars(|v| v.set_var(&arg.0, trimmed, VarFlags::NONE));
|
||||
write_vars(|v| v.set_var(&arg.0, trimmed, VarFlags::NONE))?;
|
||||
remaining.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu
|
||||
];
|
||||
let mut flags = ZoltFlags::empty();
|
||||
|
||||
let (argv, opts) = get_opts_from_tokens(argv, &zolt_opts);
|
||||
let (argv, opts) = get_opts_from_tokens(argv, &zolt_opts)?;
|
||||
|
||||
for opt in opts {
|
||||
match opt {
|
||||
|
||||
@@ -2010,7 +2010,6 @@ pub fn expand_prompt(raw: &str) -> ShResult<String> {
|
||||
result.push_str(&count.to_string());
|
||||
}
|
||||
PromptTk::Function(f) => {
|
||||
log::debug!("Expanding prompt function: {f}");
|
||||
let output = expand_cmd_sub(&f)?;
|
||||
result.push_str(&output);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::sync::Arc;
|
||||
|
||||
use fmt::Display;
|
||||
|
||||
use crate::{parse::lex::Tk, prelude::*};
|
||||
use crate::{libsh::error::ShResult, parse::lex::Tk, prelude::*};
|
||||
|
||||
pub type OptSet = Arc<[Opt]>;
|
||||
|
||||
@@ -67,8 +67,12 @@ pub fn get_opts(words: Vec<String>) -> (Vec<String>, Vec<Opt>) {
|
||||
(non_opts, opts)
|
||||
}
|
||||
|
||||
pub fn get_opts_from_tokens(tokens: Vec<Tk>, opt_specs: &[OptSpec]) -> (Vec<Tk>, Vec<Opt>) {
|
||||
let mut tokens_iter = tokens.into_iter();
|
||||
pub fn get_opts_from_tokens(tokens: Vec<Tk>, opt_specs: &[OptSpec]) -> ShResult<(Vec<Tk>, Vec<Opt>)> {
|
||||
let mut tokens_iter = tokens
|
||||
.into_iter()
|
||||
.map(|t| t.expand())
|
||||
.collect::<ShResult<Vec<_>>>()?
|
||||
.into_iter();
|
||||
let mut opts = vec![];
|
||||
let mut non_opts = vec![];
|
||||
|
||||
@@ -111,5 +115,5 @@ pub fn get_opts_from_tokens(tokens: Vec<Tk>, opt_specs: &[OptSpec]) -> (Vec<Tk>,
|
||||
}
|
||||
}
|
||||
}
|
||||
(non_opts, opts)
|
||||
Ok((non_opts, opts))
|
||||
}
|
||||
|
||||
22
src/main.rs
22
src/main.rs
@@ -33,8 +33,8 @@ use crate::parse::execute::exec_input;
|
||||
use crate::prelude::*;
|
||||
use crate::prompt::get_prompt;
|
||||
use crate::prompt::readline::term::{LineWriter, RawModeGuard, raw_mode};
|
||||
use crate::prompt::readline::{ShedVi, ReadlineEvent};
|
||||
use crate::signal::{QUIT_CODE, check_signals, sig_setup, signals_pending};
|
||||
use crate::prompt::readline::{Prompt, ReadlineEvent, ShedVi};
|
||||
use crate::signal::{GOT_SIGWINCH, QUIT_CODE, check_signals, sig_setup, signals_pending};
|
||||
use crate::state::{read_logic, source_rc, write_jobs, write_meta};
|
||||
use clap::Parser;
|
||||
use state::{read_vars, write_vars};
|
||||
@@ -161,7 +161,7 @@ fn shed_interactive() -> ShResult<()> {
|
||||
}
|
||||
|
||||
// Create readline instance with initial prompt
|
||||
let mut readline = match ShedVi::new(get_prompt().ok(), *TTY_FILENO) {
|
||||
let mut readline = match ShedVi::new(Prompt::new(), *TTY_FILENO) {
|
||||
Ok(rl) => rl,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to initialize readline: {e}");
|
||||
@@ -175,13 +175,18 @@ fn shed_interactive() -> ShResult<()> {
|
||||
|
||||
// Main poll loop
|
||||
loop {
|
||||
write_meta(|m| {
|
||||
m.try_rehash_commands();
|
||||
m.try_rehash_cwd_listing();
|
||||
});
|
||||
|
||||
// Handle any pending signals
|
||||
while signals_pending() {
|
||||
if let Err(e) = check_signals() {
|
||||
match e.kind() {
|
||||
ShErrKind::ClearReadline => {
|
||||
// Ctrl+C - clear current input and show new prompt
|
||||
readline.reset(get_prompt().ok());
|
||||
readline.reset(Prompt::new());
|
||||
}
|
||||
ShErrKind::CleanExit(code) => {
|
||||
QUIT_CODE.store(*code, Ordering::SeqCst);
|
||||
@@ -192,7 +197,12 @@ fn shed_interactive() -> ShResult<()> {
|
||||
}
|
||||
}
|
||||
|
||||
readline.update_prompt(get_prompt().unwrap_or_default());
|
||||
if GOT_SIGWINCH.swap(false, Ordering::SeqCst) {
|
||||
log::info!("Window size change detected, updating readline dimensions");
|
||||
readline.writer.update_t_cols();
|
||||
}
|
||||
|
||||
readline.prompt_mut().refresh();
|
||||
readline.print_line(false)?;
|
||||
|
||||
// Poll for stdin input
|
||||
@@ -255,7 +265,7 @@ fn shed_interactive() -> ShResult<()> {
|
||||
readline.writer.flush_write("\n")?;
|
||||
|
||||
// Reset for next command with fresh prompt
|
||||
readline.reset(get_prompt().ok());
|
||||
readline.reset(Prompt::new());
|
||||
let real_end = start.elapsed();
|
||||
log::info!("Total round trip time: {:.2?}", real_end);
|
||||
}
|
||||
|
||||
@@ -936,7 +936,7 @@ impl ParseStream {
|
||||
self.catch_separator(&mut node_tks);
|
||||
|
||||
let mut nodes = vec![];
|
||||
while let Some(node) = self.parse_block(true /* check_pipelines */)? {
|
||||
while let Some(node) = self.parse_cmd_list()? {
|
||||
node_tks.extend(node.tokens.clone());
|
||||
let sep = node.tokens.last().unwrap();
|
||||
if sep.has_double_semi() {
|
||||
@@ -1015,7 +1015,7 @@ impl ParseStream {
|
||||
self.catch_separator(&mut node_tks);
|
||||
|
||||
let mut body_blocks = vec![];
|
||||
while let Some(body_block) = self.parse_block(true)? {
|
||||
while let Some(body_block) = self.parse_cmd_list()? {
|
||||
node_tks.extend(body_block.tokens.clone());
|
||||
body_blocks.push(body_block);
|
||||
}
|
||||
@@ -1043,7 +1043,7 @@ impl ParseStream {
|
||||
if self.check_keyword("else") {
|
||||
node_tks.push(self.next_tk().unwrap());
|
||||
self.catch_separator(&mut node_tks);
|
||||
while let Some(block) = self.parse_block(true)? {
|
||||
while let Some(block) = self.parse_cmd_list()? {
|
||||
else_block.push(block)
|
||||
}
|
||||
if else_block.is_empty() {
|
||||
@@ -1133,7 +1133,7 @@ impl ParseStream {
|
||||
node_tks.push(self.next_tk().unwrap());
|
||||
self.catch_separator(&mut node_tks);
|
||||
|
||||
while let Some(node) = self.parse_block(true)? {
|
||||
while let Some(node) = self.parse_cmd_list()? {
|
||||
body.push(node)
|
||||
}
|
||||
|
||||
@@ -1196,7 +1196,7 @@ impl ParseStream {
|
||||
self.catch_separator(&mut node_tks);
|
||||
|
||||
let mut body = vec![];
|
||||
while let Some(block) = self.parse_block(true)? {
|
||||
while let Some(block) = self.parse_cmd_list()? {
|
||||
node_tks.extend(block.tokens.clone());
|
||||
body.push(block);
|
||||
}
|
||||
|
||||
@@ -11,8 +11,7 @@ pub fn get_prompt() -> ShResult<String> {
|
||||
// username@hostname
|
||||
// short/path/to/pwd/
|
||||
// $ _
|
||||
let default =
|
||||
"\\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 ";
|
||||
let default = "\\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}");
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::{
|
||||
use crate::{
|
||||
libsh::term::{Style, StyleSet, Styled},
|
||||
prompt::readline::{annotate_input, markers::{self, is_marker}},
|
||||
state::{read_logic, read_shopts},
|
||||
state::{read_logic, read_meta, read_shopts},
|
||||
};
|
||||
|
||||
/// Syntax highlighter for shell input using Unicode marker-based annotation
|
||||
@@ -173,7 +173,6 @@ impl Highlighter {
|
||||
}
|
||||
cmd_name.push(ch);
|
||||
}
|
||||
log::debug!("Command name: '{}'", Self::strip_markers(&cmd_name));
|
||||
let style = if matches!(Self::strip_markers(&cmd_name).as_str(), "break" | "continue" | "return") {
|
||||
Style::Magenta.into()
|
||||
} else if Self::is_valid(&Self::strip_markers(&cmd_name)) {
|
||||
@@ -291,54 +290,35 @@ impl Highlighter {
|
||||
/// 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 cmd_path = Path::new(&command);
|
||||
|
||||
if cmd_path.exists() {
|
||||
if cmd_path.is_absolute() {
|
||||
// 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;
|
||||
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;
|
||||
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;
|
||||
read_meta(|m| m.cached_cmds().get(command).is_some())
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn is_filename(arg: &str) -> bool {
|
||||
let path = PathBuf::from(arg);
|
||||
let path = Path::new(arg);
|
||||
|
||||
if path.exists() {
|
||||
if path.is_absolute() && path.exists() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(parent_dir) = path.parent()
|
||||
&& let Ok(entries) = parent_dir.read_dir()
|
||||
{
|
||||
if path.is_absolute()
|
||||
&& 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())
|
||||
@@ -354,22 +334,17 @@ impl Highlighter {
|
||||
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 {
|
||||
read_meta(|m| {
|
||||
let files = m.cwd_cache();
|
||||
for file in files {
|
||||
if file.starts_with(arg) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
/// Emits a reset ANSI code to the output, with deduplication
|
||||
|
||||
@@ -496,6 +496,12 @@ impl LineBuf {
|
||||
pub fn grapheme_at_cursor(&mut self) -> Option<&str> {
|
||||
self.grapheme_at(self.cursor.get())
|
||||
}
|
||||
pub fn grapheme_before_cursor(&mut self) -> Option<&str> {
|
||||
if self.cursor.get() == 0 {
|
||||
return None;
|
||||
}
|
||||
self.grapheme_at(self.cursor.ret_sub(1))
|
||||
}
|
||||
pub fn mark_insert_mode_start_pos(&mut self) {
|
||||
self.insert_mode_start_pos = Some(self.cursor.get())
|
||||
}
|
||||
@@ -1884,7 +1890,11 @@ impl LineBuf {
|
||||
self.buffer.replace_range(start..end, new);
|
||||
}
|
||||
pub fn calc_indent_level(&mut self) {
|
||||
let input = Arc::new(self.buffer.clone());
|
||||
let to_cursor = self
|
||||
.slice_to_cursor()
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or(self.buffer.clone());
|
||||
let input = Arc::new(to_cursor);
|
||||
let Ok(tokens) = LexStream::new(input, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>() else {
|
||||
log::error!("Failed to lex buffer for indent calculation");
|
||||
return;
|
||||
@@ -1914,8 +1924,10 @@ impl LineBuf {
|
||||
}
|
||||
|
||||
let eval = match motion {
|
||||
MotionCmd(count, Motion::WholeLine) => {
|
||||
let Some((start, end)) = (if count == 1 {
|
||||
MotionCmd(count, motion @ (Motion::WholeLineInclusive | Motion::WholeLineExclusive)) => {
|
||||
let exclusive = matches!(motion, Motion::WholeLineExclusive);
|
||||
|
||||
let Some((start, mut end)) = (if count == 1 {
|
||||
Some(self.this_line())
|
||||
} else {
|
||||
self.select_lines_down(count)
|
||||
@@ -1923,6 +1935,10 @@ impl LineBuf {
|
||||
return MotionKind::Null;
|
||||
};
|
||||
|
||||
if exclusive && self.grapheme_before(end).is_some_and(|gr| gr == "\n") {
|
||||
end = end.saturating_sub(1);
|
||||
}
|
||||
|
||||
let target_col = if let Some(col) = self.saved_col {
|
||||
col
|
||||
} else {
|
||||
@@ -1938,6 +1954,7 @@ impl LineBuf {
|
||||
if self.cursor.exclusive
|
||||
&& line.ends_with("\n")
|
||||
&& self.grapheme_at(target_pos) == Some("\n")
|
||||
&& line != "\n" // Allow landing on newline for empty lines
|
||||
{
|
||||
target_pos = target_pos.saturating_sub(1); // Don't land on the
|
||||
// newline
|
||||
@@ -2171,12 +2188,14 @@ impl LineBuf {
|
||||
};
|
||||
|
||||
let Some(line) = self.slice(start..end).map(|s| s.to_string()) else {
|
||||
log::warn!("Failed to get line slice for motion, start: {start}, end: {end}");
|
||||
return MotionKind::Null;
|
||||
};
|
||||
let mut target_pos = self.grapheme_index_for_display_col(&line, target_col);
|
||||
if self.cursor.exclusive
|
||||
&& line.ends_with("\n")
|
||||
&& self.grapheme_at(target_pos) == Some("\n")
|
||||
&& line != "\n" // Allow landing on newline for empty lines
|
||||
{
|
||||
target_pos = target_pos.saturating_sub(1); // Don't land on the
|
||||
// newline
|
||||
@@ -2188,6 +2207,7 @@ impl LineBuf {
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
|
||||
MotionKind::InclusiveWithTargetCol((start, end), target_pos)
|
||||
}
|
||||
MotionCmd(count, Motion::LineDownCharwise) | MotionCmd(count, Motion::LineUpCharwise) => {
|
||||
@@ -2412,9 +2432,15 @@ impl LineBuf {
|
||||
) -> ShResult<()> {
|
||||
match verb {
|
||||
Verb::Delete | Verb::Yank | Verb::Change => {
|
||||
let Some((start, end)) = self.range_from_motion(&motion) else {
|
||||
let Some((mut start, mut end)) = self.range_from_motion(&motion) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let mut do_indent = false;
|
||||
if verb == Verb::Change && (start,end) == self.this_line() {
|
||||
do_indent = read_shopts(|o| o.prompt.auto_indent);
|
||||
}
|
||||
|
||||
let register_text = if verb == Verb::Yank {
|
||||
self
|
||||
.slice(start..end)
|
||||
@@ -2426,14 +2452,17 @@ impl LineBuf {
|
||||
drained
|
||||
};
|
||||
register.write_to_register(register_text);
|
||||
match motion {
|
||||
MotionKind::ExclusiveWithTargetCol((_, _), pos)
|
||||
| MotionKind::InclusiveWithTargetCol((_, _), pos) => {
|
||||
let (start, end) = self.this_line();
|
||||
self.cursor.set(start);
|
||||
self.cursor.add(end.min(pos));
|
||||
if do_indent {
|
||||
self.calc_indent_level();
|
||||
let tabs = (0..self.auto_indent_level).map(|_| '\t');
|
||||
for tab in tabs {
|
||||
self.insert_at_cursor(tab);
|
||||
self.cursor.add(1);
|
||||
}
|
||||
_ => self.cursor.set(start),
|
||||
} else if verb != Verb::Change
|
||||
&& let MotionKind::InclusiveWithTargetCol((_,_), col) = motion {
|
||||
self.cursor.add(col);
|
||||
}
|
||||
}
|
||||
Verb::Rot13 => {
|
||||
@@ -2667,7 +2696,11 @@ impl LineBuf {
|
||||
let Some((start, end)) = self.range_from_motion(&motion) else {
|
||||
return Ok(());
|
||||
};
|
||||
let move_cursor = self.cursor.get() == start;
|
||||
self.insert_at(start, '\t');
|
||||
if move_cursor {
|
||||
self.cursor.add(1);
|
||||
}
|
||||
let mut range_indices = self.grapheme_indices()[start..end].to_vec().into_iter();
|
||||
while let Some(idx) = range_indices.next() {
|
||||
let gr = self.grapheme_at(idx).unwrap();
|
||||
@@ -2733,12 +2766,9 @@ impl LineBuf {
|
||||
Anchor::After => {
|
||||
self.push('\n');
|
||||
if auto_indent {
|
||||
log::debug!("Calculating indent level for new line");
|
||||
self.calc_indent_level();
|
||||
log::debug!("Auto-indent level: {}", self.auto_indent_level);
|
||||
let tabs = (0..self.auto_indent_level).map(|_| '\t');
|
||||
for tab in tabs {
|
||||
log::debug!("Pushing tab for auto-indent");
|
||||
self.push(tab);
|
||||
}
|
||||
}
|
||||
@@ -2793,8 +2823,24 @@ impl LineBuf {
|
||||
Verb::AcceptLineOrNewline => {
|
||||
// If this verb has reached this function, it means we have incomplete input
|
||||
// and therefore must insert a newline instead of accepting the input
|
||||
self.push('\n');
|
||||
if self.cursor.exclusive {
|
||||
// in this case we are in normal/visual mode, so we don't insert anything
|
||||
// and just move down a line
|
||||
let motion = self.eval_motion(None, MotionCmd(1, Motion::LineDownCharwise));
|
||||
self.apply_motion(motion);
|
||||
return Ok(());
|
||||
}
|
||||
let auto_indent = read_shopts(|o| o.prompt.auto_indent);
|
||||
self.insert_at_cursor('\n');
|
||||
self.cursor.add(1);
|
||||
if auto_indent {
|
||||
self.calc_indent_level();
|
||||
let tabs = (0..self.auto_indent_level).map(|_| '\t');
|
||||
for tab in tabs {
|
||||
self.insert_at_cursor(tab);
|
||||
self.cursor.add(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Verb::Complete
|
||||
@@ -2813,7 +2859,7 @@ impl LineBuf {
|
||||
pub fn exec_cmd(&mut self, cmd: ViCmd) -> ShResult<()> {
|
||||
let clear_redos = !cmd.is_undo_op() || cmd.verb.as_ref().is_some_and(|v| v.1.is_edit());
|
||||
let is_char_insert = cmd.verb.as_ref().is_some_and(|v| v.1.is_char_insert());
|
||||
let is_line_motion = cmd.is_line_motion();
|
||||
let is_line_motion = cmd.is_line_motion() || cmd.verb.as_ref().is_some_and(|v| v.1 == Verb::AcceptLineOrNewline);
|
||||
let is_undo_op = cmd.is_undo_op();
|
||||
let edit_is_merging = self.undo_stack.last().is_some_and(|edit| edit.merging);
|
||||
|
||||
@@ -2886,6 +2932,14 @@ impl LineBuf {
|
||||
self.apply_motion(motion_eval);
|
||||
}
|
||||
|
||||
if self.cursor.exclusive
|
||||
&& self.grapheme_at_cursor().is_some_and(|gr| gr == "\n")
|
||||
&& self.grapheme_before_cursor().is_some_and(|gr| gr != "\n") {
|
||||
// we landed on a newline, and we aren't inbetween two newlines.
|
||||
self.cursor.sub(1);
|
||||
self.update_select_range();
|
||||
}
|
||||
|
||||
/* Done executing, do some cleanup */
|
||||
|
||||
let after = self.buffer.clone();
|
||||
|
||||
@@ -129,11 +129,78 @@ pub enum ReadlineEvent {
|
||||
Pending,
|
||||
}
|
||||
|
||||
pub struct Prompt {
|
||||
ps1_expanded: String,
|
||||
ps1_raw: String,
|
||||
psr_expanded: Option<String>,
|
||||
psr_raw: Option<String>,
|
||||
}
|
||||
|
||||
impl Prompt {
|
||||
const DEFAULT_PS1: &str = "\\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 ";
|
||||
pub fn new() -> Self {
|
||||
let Ok(ps1_raw) = env::var("PS1") else {
|
||||
return Self::default();
|
||||
};
|
||||
let Ok(ps1_expanded) = expand_prompt(&ps1_raw) else {
|
||||
return Self::default();
|
||||
};
|
||||
Self {
|
||||
ps1_expanded,
|
||||
ps1_raw,
|
||||
psr_expanded: None,
|
||||
psr_raw: None,
|
||||
}
|
||||
}
|
||||
pub fn with_psr(mut self, psr_raw: String) -> ShResult<Self> {
|
||||
let psr_expanded = expand_prompt(&psr_raw)?;
|
||||
self.psr_expanded = Some(psr_expanded);
|
||||
self.psr_raw = Some(psr_raw);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn get_ps1(&self) -> &str {
|
||||
&self.ps1_expanded
|
||||
}
|
||||
pub fn set_ps1(&mut self, ps1_raw: String) -> ShResult<()> {
|
||||
self.ps1_expanded = expand_prompt(&ps1_raw)?;
|
||||
self.ps1_raw = ps1_raw;
|
||||
Ok(())
|
||||
}
|
||||
pub fn set_psr(&mut self, psr_raw: String) -> ShResult<()> {
|
||||
self.psr_expanded = Some(expand_prompt(&psr_raw)?);
|
||||
self.psr_raw = Some(psr_raw);
|
||||
Ok(())
|
||||
}
|
||||
pub fn get_psr(&self) -> Option<&str> {
|
||||
self.psr_expanded.as_deref()
|
||||
}
|
||||
|
||||
pub fn refresh(&mut self) -> ShResult<()> {
|
||||
self.ps1_expanded = expand_prompt(&self.ps1_raw)?;
|
||||
if let Some(psr_raw) = &self.psr_raw {
|
||||
self.psr_expanded = Some(expand_prompt(psr_raw)?);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Prompt {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ps1_expanded: expand_prompt(Self::DEFAULT_PS1).unwrap_or_else(|_| Self::DEFAULT_PS1.to_string()),
|
||||
ps1_raw: Self::DEFAULT_PS1.to_string(),
|
||||
psr_expanded: None,
|
||||
psr_raw: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ShedVi {
|
||||
pub reader: PollReader,
|
||||
pub writer: TermWriter,
|
||||
|
||||
pub prompt: String,
|
||||
pub prompt: Prompt,
|
||||
pub highlighter: Highlighter,
|
||||
pub completer: Completer,
|
||||
|
||||
@@ -149,11 +216,11 @@ pub struct ShedVi {
|
||||
}
|
||||
|
||||
impl ShedVi {
|
||||
pub fn new(prompt: Option<String>, tty: RawFd) -> ShResult<Self> {
|
||||
pub fn new(prompt: Prompt, tty: RawFd) -> ShResult<Self> {
|
||||
let mut new = Self {
|
||||
reader: PollReader::new(),
|
||||
writer: TermWriter::new(tty),
|
||||
prompt: prompt.unwrap_or("$ ".styled(Style::Green)),
|
||||
prompt,
|
||||
completer: Completer::new(),
|
||||
highlighter: Highlighter::new(),
|
||||
mode: Box::new(ViInsert::new()),
|
||||
@@ -187,11 +254,10 @@ impl ShedVi {
|
||||
self.needs_redraw = true;
|
||||
}
|
||||
|
||||
|
||||
/// Reset readline state for a new prompt
|
||||
pub fn reset(&mut self, prompt: Option<String>) {
|
||||
if let Some(p) = prompt {
|
||||
self.prompt = p;
|
||||
}
|
||||
pub fn reset(&mut self, prompt: Prompt) {
|
||||
self.prompt = prompt;
|
||||
self.editor = Default::default();
|
||||
self.mode = Box::new(ViInsert::new());
|
||||
self.old_layout = None;
|
||||
@@ -200,9 +266,12 @@ impl ShedVi {
|
||||
self.history.reset();
|
||||
}
|
||||
|
||||
pub fn update_prompt(&mut self, prompt: String) {
|
||||
self.prompt = prompt;
|
||||
self.needs_redraw = true;
|
||||
pub fn prompt(&self) -> &Prompt {
|
||||
&self.prompt
|
||||
}
|
||||
|
||||
pub fn prompt_mut(&mut self) -> &mut Prompt {
|
||||
&mut self.prompt
|
||||
}
|
||||
|
||||
fn should_submit(&mut self) -> ShResult<bool> {
|
||||
@@ -366,7 +435,7 @@ impl ShedVi {
|
||||
pub fn get_layout(&mut self, line: &str) -> Layout {
|
||||
let to_cursor = self.editor.slice_to_cursor().unwrap_or_default();
|
||||
let (cols, _) = get_win_size(*TTY_FILENO);
|
||||
Layout::from_parts(cols, &self.prompt, to_cursor, line)
|
||||
Layout::from_parts(cols, self.prompt.get_ps1(), to_cursor, line)
|
||||
}
|
||||
pub fn scroll_history(&mut self, cmd: ViCmd) {
|
||||
/*
|
||||
@@ -457,6 +526,7 @@ impl ShedVi {
|
||||
}
|
||||
|
||||
let row0_used = self.prompt
|
||||
.get_ps1()
|
||||
.lines()
|
||||
.next()
|
||||
.map(|l| Layout::calc_pos(self.writer.t_cols, l, Pos { col: 0, row: 0 }))
|
||||
@@ -469,7 +539,7 @@ impl ShedVi {
|
||||
self.writer.clear_rows(layout)?;
|
||||
}
|
||||
|
||||
self.writer.redraw(&self.prompt, &line, &new_layout)?;
|
||||
self.writer.redraw(self.prompt.get_ps1(), &line, &new_layout)?;
|
||||
|
||||
let seq_fits = pending_seq.as_ref().is_some_and(|seq| row0_used + 1 < self.writer.t_cols as usize - seq.width());
|
||||
let psr_fits = prompt_string_right.as_ref().is_some_and(|psr| new_layout.end.col as usize + 1 < self.writer.t_cols as usize - psr.width());
|
||||
|
||||
@@ -299,7 +299,8 @@ impl Verb {
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum Motion {
|
||||
WholeLine,
|
||||
WholeLineInclusive, // whole line including the linebreak
|
||||
WholeLineExclusive, // whole line excluding the linebreak
|
||||
TextObj(TextObj),
|
||||
EndOfLastWord,
|
||||
BeginningOfFirstWord,
|
||||
@@ -381,7 +382,7 @@ impl Motion {
|
||||
pub fn is_linewise(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::WholeLine | Self::LineUp | Self::LineDown | Self::ScreenLineDown | Self::ScreenLineUp
|
||||
Self::WholeLineInclusive | Self::WholeLineExclusive | Self::LineUp | Self::LineDown | Self::ScreenLineDown | Self::ScreenLineUp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -451,7 +451,7 @@ impl ViNormal {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(count, Verb::Delete)),
|
||||
motion: Some(MotionCmd(1, Motion::ForwardChar)),
|
||||
motion: Some(MotionCmd(1, Motion::ForwardCharForced)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
});
|
||||
@@ -478,7 +478,7 @@ impl ViNormal {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(count, Verb::Change)),
|
||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||
motion: Some(MotionCmd(1, Motion::WholeLineExclusive)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
});
|
||||
@@ -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::WholeLineInclusive));
|
||||
}
|
||||
('W', Some(VerbCmd(_, Verb::Change))) => {
|
||||
// Same with 'W'
|
||||
@@ -1218,7 +1218,7 @@ impl ViVisual {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(1, Verb::Delete)),
|
||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||
motion: Some(MotionCmd(1, Motion::WholeLineInclusive)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
});
|
||||
@@ -1227,7 +1227,7 @@ impl ViVisual {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(1, Verb::Yank)),
|
||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||
motion: Some(MotionCmd(1, Motion::WholeLineInclusive)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
});
|
||||
@@ -1236,7 +1236,7 @@ impl ViVisual {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(1, Verb::Delete)),
|
||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||
motion: Some(MotionCmd(1, Motion::WholeLineInclusive)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
});
|
||||
@@ -1245,7 +1245,7 @@ impl ViVisual {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(1, Verb::Change)),
|
||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||
motion: Some(MotionCmd(1, Motion::WholeLineExclusive)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
});
|
||||
@@ -1254,7 +1254,7 @@ impl ViVisual {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(1, Verb::Indent)),
|
||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||
motion: Some(MotionCmd(1, Motion::WholeLineInclusive)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
});
|
||||
@@ -1263,7 +1263,7 @@ impl ViVisual {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(1, Verb::Dedent)),
|
||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||
motion: Some(MotionCmd(1, Motion::WholeLineInclusive)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
});
|
||||
@@ -1272,7 +1272,7 @@ impl ViVisual {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(1, Verb::Equalize)),
|
||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||
motion: Some(MotionCmd(1, Motion::WholeLineInclusive)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
});
|
||||
@@ -1389,12 +1389,14 @@ impl ViVisual {
|
||||
};
|
||||
match (ch, &verb) {
|
||||
('d', Some(VerbCmd(_, Verb::Delete)))
|
||||
| ('c', Some(VerbCmd(_, Verb::Change)))
|
||||
| ('y', Some(VerbCmd(_, Verb::Yank)))
|
||||
| ('=', 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::WholeLineInclusive));
|
||||
}
|
||||
('c', Some(VerbCmd(_, Verb::Change))) => {
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::WholeLineExclusive));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ static SIGNALS: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
pub static REAPING_ENABLED: AtomicBool = AtomicBool::new(true);
|
||||
pub static SHOULD_QUIT: AtomicBool = AtomicBool::new(false);
|
||||
pub static GOT_SIGWINCH: AtomicBool = AtomicBool::new(false);
|
||||
pub static QUIT_CODE: AtomicI32 = AtomicI32::new(0);
|
||||
|
||||
const MISC_SIGNALS: [Signal; 22] = [
|
||||
@@ -81,6 +82,10 @@ pub fn check_signals() -> ShResult<()> {
|
||||
run_trap(Signal::SIGCHLD)?;
|
||||
wait_child()?;
|
||||
}
|
||||
if got_signal(Signal::SIGWINCH) {
|
||||
GOT_SIGWINCH.store(true, Ordering::SeqCst);
|
||||
run_trap(Signal::SIGWINCH)?;
|
||||
}
|
||||
|
||||
for sig in MISC_SIGNALS {
|
||||
if got_signal(sig) {
|
||||
|
||||
105
src/state.rs
105
src/state.rs
@@ -1,16 +1,11 @@
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::{HashMap, VecDeque},
|
||||
fmt::Display,
|
||||
ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref},
|
||||
str::FromStr,
|
||||
time::Duration,
|
||||
cell::RefCell, collections::{HashMap, HashSet, VecDeque}, fmt::Display, ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref}, os::unix::fs::PermissionsExt, str::FromStr, time::Duration
|
||||
};
|
||||
|
||||
use nix::unistd::{User, gethostname, getppid};
|
||||
|
||||
use crate::{
|
||||
builtin::trap::TrapTarget, exec_input, jobs::JobTab, libsh::{
|
||||
builtin::{BUILTINS, trap::TrapTarget}, exec_input, jobs::JobTab, libsh::{
|
||||
error::{ShErr, ShErrKind, ShResult},
|
||||
utils::VecDequeExt,
|
||||
}, parse::{ConjunctNode, NdRule, Node, ParsedSrc}, prelude::*, prompt::readline::markers, shopt::ShOpts
|
||||
@@ -741,13 +736,107 @@ pub struct MetaTab {
|
||||
// pending system messages
|
||||
system_msg: Vec<String>,
|
||||
|
||||
dir_stack: VecDeque<PathBuf>
|
||||
// pushd/popd stack
|
||||
dir_stack: VecDeque<PathBuf>,
|
||||
|
||||
old_path: Option<String>,
|
||||
old_pwd: Option<String>,
|
||||
// valid command cache
|
||||
path_cache: HashSet<String>,
|
||||
cwd_cache: HashSet<String>
|
||||
}
|
||||
|
||||
impl MetaTab {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
pub fn cached_cmds(&self) -> &HashSet<String> {
|
||||
&self.path_cache
|
||||
}
|
||||
pub fn cwd_cache(&self) -> &HashSet<String> {
|
||||
&self.cwd_cache
|
||||
}
|
||||
pub fn try_rehash_commands(&mut self) {
|
||||
let path = env::var("PATH").unwrap_or_default();
|
||||
let cwd = env::var("PWD").unwrap_or_default();
|
||||
if self.old_path.as_ref().is_some_and(|old| *old == path)
|
||||
&& self.old_pwd.as_ref().is_some_and(|old| *old == cwd) {
|
||||
log::trace!("PATH and PWD unchanged, skipping rehash");
|
||||
return;
|
||||
}
|
||||
|
||||
log::debug!("Rehashing commands for PATH: '{}' and PWD: '{}'", path, cwd);
|
||||
|
||||
self.path_cache.clear();
|
||||
self.old_path = Some(path.clone());
|
||||
self.old_pwd = Some(cwd.clone());
|
||||
let paths = path.split(":")
|
||||
.map(PathBuf::from);
|
||||
|
||||
for path in paths {
|
||||
if let Ok(entries) = path.read_dir() {
|
||||
for entry in entries.flatten() {
|
||||
let Ok(meta) = entry.metadata() else { continue };
|
||||
let is_exec = meta.permissions().mode() & 0o111 != 0;
|
||||
|
||||
if let Ok(file_type) = entry.file_type()
|
||||
&& file_type.is_file() && is_exec
|
||||
&& let Some(name) = entry.file_name().to_str() {
|
||||
self.path_cache.insert(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Ok(entries) = Path::new(&cwd).read_dir() {
|
||||
for entry in entries.flatten() {
|
||||
let Ok(meta) = entry.metadata() else { continue };
|
||||
let is_exec = meta.permissions().mode() & 0o111 != 0;
|
||||
|
||||
if let Ok(file_type) = entry.file_type()
|
||||
&& file_type.is_file() && is_exec
|
||||
&& let Some(name) = entry.file_name().to_str() {
|
||||
self.path_cache.insert(format!("./{}", name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
read_logic(|l| {
|
||||
let funcs = l.funcs();
|
||||
let aliases = l.aliases();
|
||||
for func in funcs.keys() {
|
||||
self.path_cache.insert(func.clone());
|
||||
}
|
||||
for alias in aliases.keys() {
|
||||
self.path_cache.insert(alias.clone());
|
||||
}
|
||||
});
|
||||
|
||||
for cmd in BUILTINS {
|
||||
self.path_cache.insert(cmd.to_string());
|
||||
}
|
||||
}
|
||||
pub fn try_rehash_cwd_listing(&mut self) {
|
||||
let cwd = env::var("PWD").unwrap_or_default();
|
||||
if self.old_pwd.as_ref().is_some_and(|old| *old == cwd) {
|
||||
log::trace!("PWD unchanged, skipping rehash of cwd listing");
|
||||
return;
|
||||
}
|
||||
|
||||
log::debug!("Rehashing cwd listing for PWD: '{}'", cwd);
|
||||
|
||||
if let Ok(entries) = Path::new(&cwd).read_dir() {
|
||||
for entry in entries.flatten() {
|
||||
let Ok(meta) = entry.metadata() else { continue };
|
||||
let is_exec = meta.permissions().mode() & 0o111 != 0;
|
||||
|
||||
if let Ok(file_type) = entry.file_type()
|
||||
&& file_type.is_file() && is_exec
|
||||
&& let Some(name) = entry.file_name().to_str() {
|
||||
self.cwd_cache.insert(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn start_timer(&mut self) {
|
||||
self.runtime_start = Some(Instant::now());
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::expand::{perform_param_expansion, DUB_QUOTE, VAR_SUB};
|
||||
use crate::expand::perform_param_expansion;
|
||||
use crate::prompt::readline::markers;
|
||||
use crate::state::VarFlags;
|
||||
|
||||
use super::*;
|
||||
@@ -296,7 +297,7 @@ fn dquote_escape_dollar() {
|
||||
// "\$foo" should strip backslash, produce literal $foo (no expansion)
|
||||
let result = unescape_str(r#""\$foo""#);
|
||||
assert!(
|
||||
!result.contains(VAR_SUB),
|
||||
!result.contains(markers::VAR_SUB),
|
||||
"Escaped $ should not become VAR_SUB"
|
||||
);
|
||||
assert!(result.contains('$'), "Literal $ should be preserved");
|
||||
@@ -307,7 +308,7 @@ fn dquote_escape_dollar() {
|
||||
fn dquote_escape_backslash() {
|
||||
// "\\" in double quotes should produce a single backslash
|
||||
let result = unescape_str(r#""\\""#);
|
||||
let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
|
||||
let inner: String = result.chars().filter(|&c| c != markers::DUB_QUOTE).collect();
|
||||
assert_eq!(
|
||||
inner, "\\",
|
||||
"Double backslash should produce single backslash"
|
||||
@@ -318,7 +319,7 @@ fn dquote_escape_backslash() {
|
||||
fn dquote_escape_quote() {
|
||||
// "\"" should produce a literal double quote
|
||||
let result = unescape_str(r#""\"""#);
|
||||
let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
|
||||
let inner: String = result.chars().filter(|&c| c != markers::DUB_QUOTE).collect();
|
||||
assert!(
|
||||
inner.contains('"'),
|
||||
"Escaped quote should produce literal quote"
|
||||
@@ -329,7 +330,7 @@ fn dquote_escape_quote() {
|
||||
fn dquote_escape_backtick() {
|
||||
// "\`" should strip backslash, produce literal backtick
|
||||
let result = unescape_str(r#""\`""#);
|
||||
let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
|
||||
let inner: String = result.chars().filter(|&c| c != markers::DUB_QUOTE).collect();
|
||||
assert_eq!(
|
||||
inner, "`",
|
||||
"Escaped backtick should produce literal backtick"
|
||||
@@ -340,7 +341,7 @@ fn dquote_escape_backtick() {
|
||||
fn dquote_escape_nonspecial_preserves_backslash() {
|
||||
// "\a" inside double quotes should preserve the backslash (a is not special)
|
||||
let result = unescape_str(r#""\a""#);
|
||||
let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
|
||||
let inner: String = result.chars().filter(|&c| c != markers::DUB_QUOTE).collect();
|
||||
assert_eq!(
|
||||
inner, "\\a",
|
||||
"Backslash before non-special char should be preserved"
|
||||
@@ -352,7 +353,7 @@ fn dquote_unescaped_dollar_expands() {
|
||||
// "$foo" inside double quotes should produce VAR_SUB (expansion marker)
|
||||
let result = unescape_str(r#""$foo""#);
|
||||
assert!(
|
||||
result.contains(VAR_SUB),
|
||||
result.contains(markers::VAR_SUB),
|
||||
"Unescaped $ should become VAR_SUB"
|
||||
);
|
||||
}
|
||||
@@ -361,10 +362,10 @@ fn dquote_unescaped_dollar_expands() {
|
||||
fn dquote_mixed_escapes() {
|
||||
// "hello \$world \\end" should have literal $, single backslash
|
||||
let result = unescape_str(r#""hello \$world \\end""#);
|
||||
assert!(!result.contains(VAR_SUB), "Escaped $ should not expand");
|
||||
assert!(!result.contains(markers::VAR_SUB), "Escaped $ should not expand");
|
||||
assert!(result.contains('$'), "Literal $ should be in output");
|
||||
// Should have exactly one backslash (from \\)
|
||||
let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
|
||||
let inner: String = result.chars().filter(|&c| c != markers::DUB_QUOTE).collect();
|
||||
let backslash_count = inner.chars().filter(|&c| c == '\\').count();
|
||||
assert_eq!(backslash_count, 1, "\\\\ should produce one backslash");
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ fn getopt_from_argv() {
|
||||
panic!()
|
||||
};
|
||||
|
||||
let (words, opts) = get_opts_from_tokens(argv, &ECHO_OPTS);
|
||||
let (words, opts) = get_opts_from_tokens(argv, &ECHO_OPTS).expect("failed to get opts");
|
||||
insta::assert_debug_snapshot!(words);
|
||||
insta::assert_debug_snapshot!(opts)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::{
|
||||
libsh::{
|
||||
expand::expand_prompt, libsh::{
|
||||
error::ShErr,
|
||||
term::{Style, Styled},
|
||||
},
|
||||
prompt::readline::{
|
||||
history::History,
|
||||
keys::{KeyCode, KeyEvent, ModKeys},
|
||||
linebuf::LineBuf,
|
||||
term::{raw_mode, KeyReader, LineWriter},
|
||||
vimode::{ViInsert, ViMode, ViNormal},
|
||||
ShedVi,
|
||||
},
|
||||
}, prompt::readline::{
|
||||
Prompt, ShedVi, history::History, keys::{KeyCode, KeyEvent, ModKeys}, linebuf::LineBuf, term::{KeyReader, LineWriter, raw_mode}, vimode::{ViInsert, ViMode, ViNormal}
|
||||
}
|
||||
};
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -255,6 +249,13 @@ fn linebuf_ascii_content() {
|
||||
assert_eq!(buf.slice_from(2), Some("llo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_default_prompt() {
|
||||
let prompt = expand_prompt("\\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 ".into()).unwrap();
|
||||
|
||||
insta::assert_debug_snapshot!(prompt)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linebuf_unicode_graphemes() {
|
||||
let mut buf = LineBuf::new().with_initial("a🇺🇸b́c", 0);
|
||||
@@ -598,7 +599,7 @@ fn editor_delete_line_up() {
|
||||
"dk",
|
||||
LOREM_IPSUM,
|
||||
237),
|
||||
("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.".into(), 240,)
|
||||
("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.".into(), 129,)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user