Compare commits

...

3 Commits

19 changed files with 523 additions and 207 deletions

View File

@@ -51,7 +51,7 @@ pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
unreachable!() unreachable!()
}; };
assert!(!argv.is_empty()); 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 flags = get_echo_flags(opts).blame(blame)?;
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;

View File

@@ -73,7 +73,7 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S
unreachable!() 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 read_opts = get_read_flags(opts).blame(blame.clone())?;
let (argv, _) = setup_builtin(argv, job, None).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())?; 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)? { let input = if isatty(STDIN_FILENO)? {
// Restore default terminal settings // Restore default terminal settings
RawModeGuard::with_cooked_mode(|| { 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![]; let mut input: Vec<u8> = vec![];
loop { loop {
let mut buf = [0u8; 1]; let mut buf = [0u8; 1];
log::info!("read: about to call read()");
match read(STDIN_FILENO, &mut buf) { match read(STDIN_FILENO, &mut buf) {
Ok(0) => { Ok(0) => {
log::info!("read: got EOF");
state::set_status(1); state::set_status(1);
break; // EOF break; // EOF
} }
Ok(n) => { Ok(n) => {
log::info!("read: got {} bytes: {:?}", n, &buf[..1]);
if buf[0] == read_opts.delim { if buf[0] == read_opts.delim {
state::set_status(0); state::set_status(0);
break; // Delimiter reached, stop reading 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) => { Err(Errno::EINTR) => {
let pending = crate::signal::sigint_pending(); let pending = crate::signal::sigint_pending();
log::info!("read: got EINTR, sigint_pending={}", pending);
if pending { if pending {
state::set_status(130); state::set_status(130);
break; break;
@@ -168,7 +166,6 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S
continue; continue;
} }
Err(e) => { Err(e) => {
log::info!("read: got error: {}", e);
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::ExecFail, ShErrKind::ExecFail,
format!("read: Failed to read from stdin: {e}"), 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() { if argv.is_empty() {
write_vars(|v| { write_vars(|v| {
v.set_var("REPLY", &input, VarFlags::NONE); v.set_var("REPLY", &input, VarFlags::NONE)
}); })?;
} else { } else {
// get our field separator // get our field separator
let mut field_sep = read_vars(|v| v.get_var("IFS")); 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() { for (i, arg) in argv.iter().enumerate() {
if i == argv.len() - 1 { if i == argv.len() - 1 {
// Last arg, stuff the rest of the input into it // 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; 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)) { if let Some(idx) = trimmed.find(|c: char| field_sep.contains(c)) {
// We found a field separator, split at the char index // We found a field separator, split at the char index
let (field, rest) = trimmed.split_at(idx); 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 // note that this doesn't account for consecutive IFS characters, which is what
// that trim above is for // that trim above is for
remaining = rest.to_string(); remaining = rest.to_string();
} else { } 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(); remaining.clear();
} }
} }

View File

@@ -65,7 +65,7 @@ pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu
]; ];
let mut flags = ZoltFlags::empty(); 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 { for opt in opts {
match opt { match opt {

View File

@@ -2010,7 +2010,6 @@ pub fn expand_prompt(raw: &str) -> ShResult<String> {
result.push_str(&count.to_string()); result.push_str(&count.to_string());
} }
PromptTk::Function(f) => { PromptTk::Function(f) => {
log::debug!("Expanding prompt function: {f}");
let output = expand_cmd_sub(&f)?; let output = expand_cmd_sub(&f)?;
result.push_str(&output); result.push_str(&output);
} }

View File

@@ -2,7 +2,7 @@ use std::sync::Arc;
use fmt::Display; use fmt::Display;
use crate::{parse::lex::Tk, prelude::*}; use crate::{libsh::error::ShResult, parse::lex::Tk, prelude::*};
pub type OptSet = Arc<[Opt]>; pub type OptSet = Arc<[Opt]>;
@@ -67,8 +67,12 @@ pub fn get_opts(words: Vec<String>) -> (Vec<String>, Vec<Opt>) {
(non_opts, opts) (non_opts, opts)
} }
pub fn get_opts_from_tokens(tokens: Vec<Tk>, opt_specs: &[OptSpec]) -> (Vec<Tk>, Vec<Opt>) { pub fn get_opts_from_tokens(tokens: Vec<Tk>, opt_specs: &[OptSpec]) -> ShResult<(Vec<Tk>, Vec<Opt>)> {
let mut tokens_iter = tokens.into_iter(); let mut tokens_iter = tokens
.into_iter()
.map(|t| t.expand())
.collect::<ShResult<Vec<_>>>()?
.into_iter();
let mut opts = vec![]; let mut opts = vec![];
let mut non_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))
} }

View File

@@ -33,8 +33,8 @@ use crate::parse::execute::exec_input;
use crate::prelude::*; use crate::prelude::*;
use crate::prompt::get_prompt; use crate::prompt::get_prompt;
use crate::prompt::readline::term::{LineWriter, RawModeGuard, raw_mode}; use crate::prompt::readline::term::{LineWriter, RawModeGuard, raw_mode};
use crate::prompt::readline::{ShedVi, ReadlineEvent}; use crate::prompt::readline::{Prompt, ReadlineEvent, ShedVi};
use crate::signal::{QUIT_CODE, check_signals, sig_setup, signals_pending}; 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 crate::state::{read_logic, source_rc, write_jobs, write_meta};
use clap::Parser; use clap::Parser;
use state::{read_vars, write_vars}; use state::{read_vars, write_vars};
@@ -161,7 +161,7 @@ fn shed_interactive() -> ShResult<()> {
} }
// Create readline instance with initial prompt // 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, Ok(rl) => rl,
Err(e) => { Err(e) => {
eprintln!("Failed to initialize readline: {e}"); eprintln!("Failed to initialize readline: {e}");
@@ -175,13 +175,18 @@ fn shed_interactive() -> ShResult<()> {
// Main poll loop // Main poll loop
loop { loop {
write_meta(|m| {
m.try_rehash_commands();
m.try_rehash_cwd_listing();
});
// Handle any pending signals // Handle any pending signals
while signals_pending() { while signals_pending() {
if let Err(e) = check_signals() { if let Err(e) = check_signals() {
match e.kind() { match e.kind() {
ShErrKind::ClearReadline => { ShErrKind::ClearReadline => {
// Ctrl+C - clear current input and show new prompt // Ctrl+C - clear current input and show new prompt
readline.reset(get_prompt().ok()); readline.reset(Prompt::new());
} }
ShErrKind::CleanExit(code) => { ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst); 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)?; readline.print_line(false)?;
// Poll for stdin input // Poll for stdin input
@@ -255,7 +265,7 @@ fn shed_interactive() -> ShResult<()> {
readline.writer.flush_write("\n")?; readline.writer.flush_write("\n")?;
// Reset for next command with fresh prompt // Reset for next command with fresh prompt
readline.reset(get_prompt().ok()); readline.reset(Prompt::new());
let real_end = start.elapsed(); let real_end = start.elapsed();
log::info!("Total round trip time: {:.2?}", real_end); log::info!("Total round trip time: {:.2?}", real_end);
} }

View File

@@ -936,7 +936,7 @@ impl ParseStream {
self.catch_separator(&mut node_tks); self.catch_separator(&mut node_tks);
let mut nodes = vec![]; 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()); node_tks.extend(node.tokens.clone());
let sep = node.tokens.last().unwrap(); let sep = node.tokens.last().unwrap();
if sep.has_double_semi() { if sep.has_double_semi() {
@@ -1015,7 +1015,7 @@ impl ParseStream {
self.catch_separator(&mut node_tks); self.catch_separator(&mut node_tks);
let mut body_blocks = vec![]; 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()); node_tks.extend(body_block.tokens.clone());
body_blocks.push(body_block); body_blocks.push(body_block);
} }
@@ -1043,7 +1043,7 @@ impl ParseStream {
if self.check_keyword("else") { if self.check_keyword("else") {
node_tks.push(self.next_tk().unwrap()); node_tks.push(self.next_tk().unwrap());
self.catch_separator(&mut node_tks); 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) else_block.push(block)
} }
if else_block.is_empty() { if else_block.is_empty() {
@@ -1133,7 +1133,7 @@ impl ParseStream {
node_tks.push(self.next_tk().unwrap()); node_tks.push(self.next_tk().unwrap());
self.catch_separator(&mut node_tks); 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) body.push(node)
} }
@@ -1196,7 +1196,7 @@ impl ParseStream {
self.catch_separator(&mut node_tks); self.catch_separator(&mut node_tks);
let mut body = vec![]; 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()); node_tks.extend(block.tokens.clone());
body.push(block); body.push(block);
} }

View File

@@ -11,8 +11,7 @@ pub fn get_prompt() -> ShResult<String> {
// username@hostname // username@hostname
// short/path/to/pwd/ // short/path/to/pwd/
// $ _ // $ _
let default = 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 ";
"\\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); return expand_prompt(default);
}; };
let sanitized = format!("\\e[0m{prompt}"); let sanitized = format!("\\e[0m{prompt}");

View File

@@ -7,7 +7,7 @@ use std::{
use crate::{ use crate::{
libsh::term::{Style, StyleSet, Styled}, libsh::term::{Style, StyleSet, Styled},
prompt::readline::{annotate_input, markers::{self, is_marker}}, 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 /// Syntax highlighter for shell input using Unicode marker-based annotation
@@ -173,7 +173,6 @@ impl Highlighter {
} }
cmd_name.push(ch); 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") { let style = if matches!(Self::strip_markers(&cmd_name).as_str(), "break" | "continue" | "return") {
Style::Magenta.into() Style::Magenta.into()
} else if Self::is_valid(&Self::strip_markers(&cmd_name)) { } else if Self::is_valid(&Self::strip_markers(&cmd_name)) {
@@ -291,54 +290,35 @@ impl Highlighter {
/// 2. All directories in PATH environment variable /// 2. All directories in PATH environment variable
/// 3. Shell functions and aliases in the current shell state /// 3. Shell functions and aliases in the current shell state
fn is_valid(command: &str) -> bool { fn is_valid(command: &str) -> bool {
let path = env::var("PATH").unwrap_or_default(); let cmd_path = Path::new(&command);
let paths = path.split(':');
let cmd_path = PathBuf::from(&command);
if cmd_path.exists() { if cmd_path.is_absolute() {
// the user has given us an absolute path // the user has given us an absolute path
if cmd_path.is_dir() && read_shopts(|o| o.core.autocd) { if cmd_path.is_dir() && read_shopts(|o| o.core.autocd) {
// this is a directory and autocd is enabled // this is a directory and autocd is enabled
return true; true
} else { } else {
let Ok(meta) = cmd_path.metadata() else { let Ok(meta) = cmd_path.metadata() else {
return false; return false;
}; };
// this is a file that is executable by someone // this is a file that is executable by someone
return meta.permissions().mode() & 0o111 == 0; meta.permissions().mode() & 0o111 != 0
} }
} else { } else {
// they gave us a command name read_meta(|m| m.cached_cmds().get(command).is_some())
// 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;
} }
} }
// 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 { 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; return true;
} }
if let Some(parent_dir) = path.parent() if path.is_absolute()
&& let Ok(entries) = parent_dir.read_dir() && let Some(parent_dir) = path.parent()
{ && let Ok(entries) = parent_dir.read_dir() {
let files = entries let files = entries
.filter_map(|e| e.ok()) .filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().to_string()) .map(|e| e.file_name().to_string_lossy().to_string())
@@ -354,22 +334,17 @@ impl Highlighter {
return true; return true;
} }
} }
}; }
if let Ok(this_dir) = env::current_dir() read_meta(|m| {
&& let Ok(entries) = this_dir.read_dir() let files = m.cwd_cache();
{ for file in files {
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) { if file.starts_with(arg) {
return true; return true;
} }
} }
};
false false
})
} }
/// Emits a reset ANSI code to the output, with deduplication /// Emits a reset ANSI code to the output, with deduplication

View File

@@ -14,7 +14,7 @@ use crate::{
libsh::{ libsh::{
error::ShResult, error::ShResult,
term::{Style, Styled}, term::{Style, Styled},
}, parse::lex::{LexFlags, LexStream, Tk, TkFlags, TkRule}, prelude::*, prompt::readline::{markers, register::write_register}, state::read_shopts }, parse::lex::{LexFlags, LexStream, Tk, TkFlags, TkRule}, prelude::*, prompt::readline::{markers, register::{write_register, RegisterContent}}, state::read_shopts
}; };
const PUNCTUATION: [&str; 3] = ["?", "!", "."]; const PUNCTUATION: [&str; 3] = ["?", "!", "."];
@@ -496,6 +496,12 @@ impl LineBuf {
pub fn grapheme_at_cursor(&mut self) -> Option<&str> { pub fn grapheme_at_cursor(&mut self) -> Option<&str> {
self.grapheme_at(self.cursor.get()) 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) { pub fn mark_insert_mode_start_pos(&mut self) {
self.insert_mode_start_pos = Some(self.cursor.get()) self.insert_mode_start_pos = Some(self.cursor.get())
} }
@@ -1884,7 +1890,11 @@ impl LineBuf {
self.buffer.replace_range(start..end, new); self.buffer.replace_range(start..end, new);
} }
pub fn calc_indent_level(&mut self) { 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 { let Ok(tokens) = LexStream::new(input, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>() else {
log::error!("Failed to lex buffer for indent calculation"); log::error!("Failed to lex buffer for indent calculation");
return; return;
@@ -1914,8 +1924,10 @@ impl LineBuf {
} }
let eval = match motion { let eval = match motion {
MotionCmd(count, Motion::WholeLine) => { MotionCmd(count, motion @ (Motion::WholeLineInclusive | Motion::WholeLineExclusive)) => {
let Some((start, end)) = (if count == 1 { let exclusive = matches!(motion, Motion::WholeLineExclusive);
let Some((start, mut end)) = (if count == 1 {
Some(self.this_line()) Some(self.this_line())
} else { } else {
self.select_lines_down(count) self.select_lines_down(count)
@@ -1923,6 +1935,10 @@ impl LineBuf {
return MotionKind::Null; 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 { let target_col = if let Some(col) = self.saved_col {
col col
} else { } else {
@@ -1938,6 +1954,7 @@ impl LineBuf {
if self.cursor.exclusive if self.cursor.exclusive
&& line.ends_with("\n") && line.ends_with("\n")
&& self.grapheme_at(target_pos) == Some("\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 target_pos = target_pos.saturating_sub(1); // Don't land on the
// newline // newline
@@ -2171,12 +2188,14 @@ impl LineBuf {
}; };
let Some(line) = self.slice(start..end).map(|s| s.to_string()) else { 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; return MotionKind::Null;
}; };
let mut target_pos = self.grapheme_index_for_display_col(&line, target_col); let mut target_pos = self.grapheme_index_for_display_col(&line, target_col);
if self.cursor.exclusive if self.cursor.exclusive
&& line.ends_with("\n") && line.ends_with("\n")
&& self.grapheme_at(target_pos) == Some("\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 target_pos = target_pos.saturating_sub(1); // Don't land on the
// newline // newline
@@ -2188,6 +2207,7 @@ impl LineBuf {
_ => unreachable!(), _ => unreachable!(),
}; };
MotionKind::InclusiveWithTargetCol((start, end), target_pos) MotionKind::InclusiveWithTargetCol((start, end), target_pos)
} }
MotionCmd(count, Motion::LineDownCharwise) | MotionCmd(count, Motion::LineUpCharwise) => { MotionCmd(count, Motion::LineDownCharwise) | MotionCmd(count, Motion::LineUpCharwise) => {
@@ -2412,10 +2432,16 @@ impl LineBuf {
) -> ShResult<()> { ) -> ShResult<()> {
match verb { match verb {
Verb::Delete | Verb::Yank | Verb::Change => { 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(()); return Ok(());
}; };
let register_text = if verb == Verb::Yank {
let mut do_indent = false;
if verb == Verb::Change && (start,end) == self.this_line() {
do_indent = read_shopts(|o| o.prompt.auto_indent);
}
let text = if verb == Verb::Yank {
self self
.slice(start..end) .slice(start..end)
.map(|c| c.to_string()) .map(|c| c.to_string())
@@ -2425,15 +2451,27 @@ impl LineBuf {
self.update_graphemes(); self.update_graphemes();
drained drained
}; };
register.write_to_register(register_text); let is_linewise = matches!(
match motion { motion,
MotionKind::ExclusiveWithTargetCol((_, _), pos) MotionKind::InclusiveWithTargetCol(..) | MotionKind::ExclusiveWithTargetCol(..)
| MotionKind::InclusiveWithTargetCol((_, _), pos) => { );
let (start, end) = self.this_line(); let register_content = if is_linewise {
RegisterContent::Line(text)
} else {
RegisterContent::Span(text)
};
register.write_to_register(register_content);
self.cursor.set(start); 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 => { Verb::Rot13 => {
@@ -2601,22 +2639,46 @@ impl LineBuf {
let Some(content) = register.read_from_register() else { let Some(content) = register.read_from_register() else {
return Ok(()); return Ok(());
}; };
if content.is_empty() {
return Ok(());
}
if let Some(range) = self.select_range { if let Some(range) = self.select_range {
let register_text = self.drain_inclusive(range.0..=range.1); let register_text = self.drain_inclusive(range.0..=range.1);
write_register(None, register_text); // swap deleted text into register write_register(None, RegisterContent::Span(register_text)); // swap deleted text into register
self.insert_str_at(range.0, &content); let text = content.as_str();
self.cursor.set(range.0 + content.chars().count()); self.insert_str_at(range.0, text);
self.cursor.set(range.0 + content.char_count());
self.select_range = None; self.select_range = None;
self.update_graphemes(); self.update_graphemes();
return Ok(()); return Ok(());
} }
match content {
RegisterContent::Span(ref text) => {
let insert_idx = match anchor { let insert_idx = match anchor {
Anchor::After => self.cursor.ret_add(1), Anchor::After => self.cursor.ret_add(1),
Anchor::Before => self.cursor.get(), Anchor::Before => self.cursor.get(),
}; };
self.insert_str_at(insert_idx, &content); self.insert_str_at(insert_idx, text);
self.cursor.add(content.len().saturating_sub(1)); self.cursor.add(text.len().saturating_sub(1));
}
RegisterContent::Line(ref text) => {
let insert_idx = match anchor {
Anchor::After => self.end_of_line(),
Anchor::Before => self.start_of_line(),
};
let needs_newline = self.grapheme_before(insert_idx).is_some_and(|gr| gr != "\n");
if needs_newline {
let full = format!("\n{}", text);
self.insert_str_at(insert_idx, &full);
self.cursor.set(insert_idx + 1);
} else {
self.insert_str_at(insert_idx, text);
self.cursor.set(insert_idx);
}
}
RegisterContent::Empty => {}
}
} }
Verb::SwapVisualAnchor => { Verb::SwapVisualAnchor => {
if let Some((start, end)) = self.select_range() if let Some((start, end)) = self.select_range()
@@ -2667,7 +2729,11 @@ impl LineBuf {
let Some((start, end)) = self.range_from_motion(&motion) else { let Some((start, end)) = self.range_from_motion(&motion) else {
return Ok(()); return Ok(());
}; };
let move_cursor = self.cursor.get() == start;
self.insert_at(start, '\t'); 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(); let mut range_indices = self.grapheme_indices()[start..end].to_vec().into_iter();
while let Some(idx) = range_indices.next() { while let Some(idx) = range_indices.next() {
let gr = self.grapheme_at(idx).unwrap(); let gr = self.grapheme_at(idx).unwrap();
@@ -2733,12 +2799,9 @@ impl LineBuf {
Anchor::After => { Anchor::After => {
self.push('\n'); self.push('\n');
if auto_indent { if auto_indent {
log::debug!("Calculating indent level for new line");
self.calc_indent_level(); self.calc_indent_level();
log::debug!("Auto-indent level: {}", self.auto_indent_level);
let tabs = (0..self.auto_indent_level).map(|_| '\t'); let tabs = (0..self.auto_indent_level).map(|_| '\t');
for tab in tabs { for tab in tabs {
log::debug!("Pushing tab for auto-indent");
self.push(tab); self.push(tab);
} }
} }
@@ -2793,8 +2856,24 @@ impl LineBuf {
Verb::AcceptLineOrNewline => { Verb::AcceptLineOrNewline => {
// If this verb has reached this function, it means we have incomplete input // If this verb has reached this function, it means we have incomplete input
// and therefore must insert a newline instead of accepting the 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); 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 Verb::Complete
@@ -2813,7 +2892,7 @@ impl LineBuf {
pub fn exec_cmd(&mut self, cmd: ViCmd) -> ShResult<()> { 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 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_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 is_undo_op = cmd.is_undo_op();
let edit_is_merging = self.undo_stack.last().is_some_and(|edit| edit.merging); let edit_is_merging = self.undo_stack.last().is_some_and(|edit| edit.merging);
@@ -2886,6 +2965,14 @@ impl LineBuf {
self.apply_motion(motion_eval); 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 */ /* Done executing, do some cleanup */
let after = self.buffer.clone(); let after = self.buffer.clone();

View File

@@ -129,11 +129,78 @@ pub enum ReadlineEvent {
Pending, 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 struct ShedVi {
pub reader: PollReader, pub reader: PollReader,
pub writer: TermWriter, pub writer: TermWriter,
pub prompt: String, pub prompt: Prompt,
pub highlighter: Highlighter, pub highlighter: Highlighter,
pub completer: Completer, pub completer: Completer,
@@ -149,11 +216,11 @@ pub struct ShedVi {
} }
impl 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 { let mut new = Self {
reader: PollReader::new(), reader: PollReader::new(),
writer: TermWriter::new(tty), writer: TermWriter::new(tty),
prompt: prompt.unwrap_or("$ ".styled(Style::Green)), prompt,
completer: Completer::new(), completer: Completer::new(),
highlighter: Highlighter::new(), highlighter: Highlighter::new(),
mode: Box::new(ViInsert::new()), mode: Box::new(ViInsert::new()),
@@ -187,11 +254,10 @@ impl ShedVi {
self.needs_redraw = true; self.needs_redraw = true;
} }
/// Reset readline state for a new prompt /// Reset readline state for a new prompt
pub fn reset(&mut self, prompt: Option<String>) { pub fn reset(&mut self, prompt: Prompt) {
if let Some(p) = prompt { self.prompt = prompt;
self.prompt = p;
}
self.editor = Default::default(); self.editor = Default::default();
self.mode = Box::new(ViInsert::new()); self.mode = Box::new(ViInsert::new());
self.old_layout = None; self.old_layout = None;
@@ -200,9 +266,12 @@ impl ShedVi {
self.history.reset(); self.history.reset();
} }
pub fn update_prompt(&mut self, prompt: String) { pub fn prompt(&self) -> &Prompt {
self.prompt = prompt; &self.prompt
self.needs_redraw = true; }
pub fn prompt_mut(&mut self) -> &mut Prompt {
&mut self.prompt
} }
fn should_submit(&mut self) -> ShResult<bool> { fn should_submit(&mut self) -> ShResult<bool> {
@@ -366,7 +435,7 @@ impl ShedVi {
pub fn get_layout(&mut self, line: &str) -> Layout { pub fn get_layout(&mut self, line: &str) -> Layout {
let to_cursor = self.editor.slice_to_cursor().unwrap_or_default(); let to_cursor = self.editor.slice_to_cursor().unwrap_or_default();
let (cols, _) = get_win_size(*TTY_FILENO); 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) { pub fn scroll_history(&mut self, cmd: ViCmd) {
/* /*
@@ -457,6 +526,7 @@ impl ShedVi {
} }
let row0_used = self.prompt let row0_used = self.prompt
.get_ps1()
.lines() .lines()
.next() .next()
.map(|l| Layout::calc_pos(self.writer.t_cols, l, Pos { col: 0, row: 0 })) .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.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 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()); 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());

View File

@@ -1,26 +1,84 @@
use std::sync::Mutex; use std::{fmt::Display, sync::Mutex};
pub static REGISTERS: Mutex<Registers> = Mutex::new(Registers::new()); pub static REGISTERS: Mutex<Registers> = Mutex::new(Registers::new());
pub fn read_register(ch: Option<char>) -> Option<String> { pub fn read_register(ch: Option<char>) -> Option<RegisterContent> {
let lock = REGISTERS.lock().unwrap(); let lock = REGISTERS.lock().unwrap();
lock.get_reg(ch).map(|r| r.buf().clone()) lock.get_reg(ch).map(|r| r.content().clone())
} }
pub fn write_register(ch: Option<char>, buf: String) { pub fn write_register(ch: Option<char>, buf: RegisterContent) {
let mut lock = REGISTERS.lock().unwrap(); let mut lock = REGISTERS.lock().unwrap();
if let Some(r) = lock.get_reg_mut(ch) { if let Some(r) = lock.get_reg_mut(ch) {
r.write(buf) r.write(buf)
} }
} }
pub fn append_register(ch: Option<char>, buf: String) { pub fn append_register(ch: Option<char>, buf: RegisterContent) {
let mut lock = REGISTERS.lock().unwrap(); let mut lock = REGISTERS.lock().unwrap();
if let Some(r) = lock.get_reg_mut(ch) { if let Some(r) = lock.get_reg_mut(ch) {
r.append(buf) r.append(buf)
} }
} }
#[derive(Default, Clone, Debug)]
pub enum RegisterContent {
Span(String),
Line(String),
#[default]
Empty,
}
impl Display for RegisterContent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Span(s) => write!(f, "{}", s),
Self::Line(s) => write!(f, "{}", s),
Self::Empty => write!(f, ""),
}
}
}
impl RegisterContent {
pub fn clear(&mut self) {
match self {
Self::Span(s) => s.clear(),
Self::Line(s) => s.clear(),
Self::Empty => {}
}
}
pub fn len(&self) -> usize {
match self {
Self::Span(s) => s.len(),
Self::Line(s) => s.len(),
Self::Empty => 0,
}
}
pub fn is_empty(&self) -> bool {
match self {
Self::Span(s) => s.is_empty(),
Self::Line(s) => s.is_empty(),
Self::Empty => true,
}
}
pub fn is_line(&self) -> bool {
matches!(self, Self::Line(_))
}
pub fn is_span(&self) -> bool {
matches!(self, Self::Span(_))
}
pub fn as_str(&self) -> &str {
match self {
Self::Span(s) => s,
Self::Line(s) => s,
Self::Empty => "",
}
}
pub fn char_count(&self) -> usize {
self.as_str().chars().count()
}
}
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct Registers { pub struct Registers {
default: Register, default: Register,
@@ -55,33 +113,33 @@ pub struct Registers {
impl Registers { impl Registers {
pub const fn new() -> Self { pub const fn new() -> Self {
Self { Self {
default: Register(String::new()), default: Register::new(),
a: Register(String::new()), a: Register::new(),
b: Register(String::new()), b: Register::new(),
c: Register(String::new()), c: Register::new(),
d: Register(String::new()), d: Register::new(),
e: Register(String::new()), e: Register::new(),
f: Register(String::new()), f: Register::new(),
g: Register(String::new()), g: Register::new(),
h: Register(String::new()), h: Register::new(),
i: Register(String::new()), i: Register::new(),
j: Register(String::new()), j: Register::new(),
k: Register(String::new()), k: Register::new(),
l: Register(String::new()), l: Register::new(),
m: Register(String::new()), m: Register::new(),
n: Register(String::new()), n: Register::new(),
o: Register(String::new()), o: Register::new(),
p: Register(String::new()), p: Register::new(),
q: Register(String::new()), q: Register::new(),
r: Register(String::new()), r: Register::new(),
s: Register(String::new()), s: Register::new(),
t: Register(String::new()), t: Register::new(),
u: Register(String::new()), u: Register::new(),
v: Register(String::new()), v: Register::new(),
w: Register(String::new()), w: Register::new(),
x: Register(String::new()), x: Register::new(),
y: Register(String::new()), y: Register::new(),
z: Register(String::new()), z: Register::new(),
} }
} }
pub fn get_reg(&self, ch: Option<char>) -> Option<&Register> { pub fn get_reg(&self, ch: Option<char>) -> Option<&Register> {
@@ -155,18 +213,39 @@ impl Registers {
} }
#[derive(Clone, Default, Debug)] #[derive(Clone, Default, Debug)]
pub struct Register(String); pub struct Register {
content: RegisterContent,
}
impl Register { impl Register {
pub fn buf(&self) -> &String { pub const fn new() -> Self {
&self.0 Self {
content: RegisterContent::Span(String::new()),
} }
pub fn write(&mut self, buf: String) {
self.0 = buf
} }
pub fn append(&mut self, buf: String) { pub fn content(&self) -> &RegisterContent {
self.0.push_str(&buf) &self.content
}
pub fn write(&mut self, buf: RegisterContent) {
self.content = buf
}
pub fn append(&mut self, buf: RegisterContent) {
match buf {
RegisterContent::Empty => {}
RegisterContent::Span(ref s) | RegisterContent::Line(ref s) => match &mut self.content {
RegisterContent::Empty => self.content = buf,
RegisterContent::Span(existing) => existing.push_str(s),
RegisterContent::Line(existing) => existing.push_str(s),
},
}
} }
pub fn clear(&mut self) { pub fn clear(&mut self) {
self.0.clear() self.content.clear()
}
pub fn is_line(&self) -> bool {
self.content.is_line()
}
pub fn is_span(&self) -> bool {
self.content.is_span()
} }
} }

View File

@@ -1,6 +1,6 @@
use bitflags::bitflags; use bitflags::bitflags;
use super::register::{append_register, read_register, write_register}; use super::register::{append_register, read_register, write_register, RegisterContent};
//TODO: write tests that take edit results and cursor positions from actual //TODO: write tests that take edit results and cursor positions from actual
// neovim edits and test them against the behavior of this editor // neovim edits and test them against the behavior of this editor
@@ -35,14 +35,14 @@ impl RegisterName {
pub fn count(&self) -> usize { pub fn count(&self) -> usize {
self.count self.count
} }
pub fn write_to_register(&self, buf: String) { pub fn write_to_register(&self, buf: RegisterContent) {
if self.append { if self.append {
append_register(self.name, buf); append_register(self.name, buf);
} else { } else {
write_register(self.name, buf); write_register(self.name, buf);
} }
} }
pub fn read_from_register(&self) -> Option<String> { pub fn read_from_register(&self) -> Option<RegisterContent> {
read_register(self.name) read_register(self.name)
} }
} }
@@ -299,7 +299,8 @@ impl Verb {
#[derive(Debug, Clone, Eq, PartialEq)] #[derive(Debug, Clone, Eq, PartialEq)]
pub enum Motion { pub enum Motion {
WholeLine, WholeLineInclusive, // whole line including the linebreak
WholeLineExclusive, // whole line excluding the linebreak
TextObj(TextObj), TextObj(TextObj),
EndOfLastWord, EndOfLastWord,
BeginningOfFirstWord, BeginningOfFirstWord,
@@ -381,7 +382,7 @@ impl Motion {
pub fn is_linewise(&self) -> bool { pub fn is_linewise(&self) -> bool {
matches!( matches!(
self, self,
Self::WholeLine | Self::LineUp | Self::LineDown | Self::ScreenLineDown | Self::ScreenLineUp Self::WholeLineInclusive | Self::WholeLineExclusive | Self::LineUp | Self::LineDown | Self::ScreenLineDown | Self::ScreenLineUp
) )
} }
} }

View File

@@ -451,7 +451,7 @@ impl ViNormal {
return Some(ViCmd { return Some(ViCmd {
register, register,
verb: Some(VerbCmd(count, Verb::Delete)), verb: Some(VerbCmd(count, Verb::Delete)),
motion: Some(MotionCmd(1, Motion::ForwardChar)), motion: Some(MotionCmd(1, Motion::ForwardCharForced)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: self.flags(), flags: self.flags(),
}); });
@@ -478,7 +478,7 @@ impl ViNormal {
return Some(ViCmd { return Some(ViCmd {
register, register,
verb: Some(VerbCmd(count, Verb::Change)), verb: Some(VerbCmd(count, Verb::Change)),
motion: Some(MotionCmd(1, Motion::WholeLine)), motion: Some(MotionCmd(1, Motion::WholeLineExclusive)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: self.flags(), flags: self.flags(),
}); });
@@ -684,7 +684,7 @@ impl ViNormal {
| ('~', Some(VerbCmd(_, Verb::ToggleCaseRange))) | ('~', Some(VerbCmd(_, Verb::ToggleCaseRange)))
| ('>', Some(VerbCmd(_, Verb::Indent))) | ('>', Some(VerbCmd(_, Verb::Indent)))
| ('<', Some(VerbCmd(_, Verb::Dedent))) => { | ('<', 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))) => { ('W', Some(VerbCmd(_, Verb::Change))) => {
// Same with 'W' // Same with 'W'
@@ -1218,7 +1218,7 @@ impl ViVisual {
return Some(ViCmd { return Some(ViCmd {
register, register,
verb: Some(VerbCmd(1, Verb::Delete)), verb: Some(VerbCmd(1, Verb::Delete)),
motion: Some(MotionCmd(1, Motion::WholeLine)), motion: Some(MotionCmd(1, Motion::WholeLineInclusive)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}); });
@@ -1227,7 +1227,7 @@ impl ViVisual {
return Some(ViCmd { return Some(ViCmd {
register, register,
verb: Some(VerbCmd(1, Verb::Yank)), verb: Some(VerbCmd(1, Verb::Yank)),
motion: Some(MotionCmd(1, Motion::WholeLine)), motion: Some(MotionCmd(1, Motion::WholeLineInclusive)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}); });
@@ -1236,7 +1236,7 @@ impl ViVisual {
return Some(ViCmd { return Some(ViCmd {
register, register,
verb: Some(VerbCmd(1, Verb::Delete)), verb: Some(VerbCmd(1, Verb::Delete)),
motion: Some(MotionCmd(1, Motion::WholeLine)), motion: Some(MotionCmd(1, Motion::WholeLineInclusive)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}); });
@@ -1245,7 +1245,7 @@ impl ViVisual {
return Some(ViCmd { return Some(ViCmd {
register, register,
verb: Some(VerbCmd(1, Verb::Change)), verb: Some(VerbCmd(1, Verb::Change)),
motion: Some(MotionCmd(1, Motion::WholeLine)), motion: Some(MotionCmd(1, Motion::WholeLineExclusive)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}); });
@@ -1254,7 +1254,7 @@ impl ViVisual {
return Some(ViCmd { return Some(ViCmd {
register, register,
verb: Some(VerbCmd(1, Verb::Indent)), verb: Some(VerbCmd(1, Verb::Indent)),
motion: Some(MotionCmd(1, Motion::WholeLine)), motion: Some(MotionCmd(1, Motion::WholeLineInclusive)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}); });
@@ -1263,7 +1263,7 @@ impl ViVisual {
return Some(ViCmd { return Some(ViCmd {
register, register,
verb: Some(VerbCmd(1, Verb::Dedent)), verb: Some(VerbCmd(1, Verb::Dedent)),
motion: Some(MotionCmd(1, Motion::WholeLine)), motion: Some(MotionCmd(1, Motion::WholeLineInclusive)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}); });
@@ -1272,7 +1272,7 @@ impl ViVisual {
return Some(ViCmd { return Some(ViCmd {
register, register,
verb: Some(VerbCmd(1, Verb::Equalize)), verb: Some(VerbCmd(1, Verb::Equalize)),
motion: Some(MotionCmd(1, Motion::WholeLine)), motion: Some(MotionCmd(1, Motion::WholeLineInclusive)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}); });
@@ -1389,12 +1389,14 @@ impl ViVisual {
}; };
match (ch, &verb) { match (ch, &verb) {
('d', Some(VerbCmd(_, Verb::Delete))) ('d', Some(VerbCmd(_, Verb::Delete)))
| ('c', Some(VerbCmd(_, Verb::Change)))
| ('y', Some(VerbCmd(_, Verb::Yank))) | ('y', Some(VerbCmd(_, Verb::Yank)))
| ('=', Some(VerbCmd(_, Verb::Equalize))) | ('=', Some(VerbCmd(_, Verb::Equalize)))
| ('>', Some(VerbCmd(_, Verb::Indent))) | ('>', Some(VerbCmd(_, Verb::Indent)))
| ('<', Some(VerbCmd(_, Verb::Dedent))) => { | ('<', 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));
} }
_ => {} _ => {}
} }

View File

@@ -15,6 +15,7 @@ static SIGNALS: AtomicU64 = AtomicU64::new(0);
pub static REAPING_ENABLED: AtomicBool = AtomicBool::new(true); pub static REAPING_ENABLED: AtomicBool = AtomicBool::new(true);
pub static SHOULD_QUIT: AtomicBool = AtomicBool::new(false); pub static SHOULD_QUIT: AtomicBool = AtomicBool::new(false);
pub static GOT_SIGWINCH: AtomicBool = AtomicBool::new(false);
pub static QUIT_CODE: AtomicI32 = AtomicI32::new(0); pub static QUIT_CODE: AtomicI32 = AtomicI32::new(0);
const MISC_SIGNALS: [Signal; 22] = [ const MISC_SIGNALS: [Signal; 22] = [
@@ -81,6 +82,10 @@ pub fn check_signals() -> ShResult<()> {
run_trap(Signal::SIGCHLD)?; run_trap(Signal::SIGCHLD)?;
wait_child()?; wait_child()?;
} }
if got_signal(Signal::SIGWINCH) {
GOT_SIGWINCH.store(true, Ordering::SeqCst);
run_trap(Signal::SIGWINCH)?;
}
for sig in MISC_SIGNALS { for sig in MISC_SIGNALS {
if got_signal(sig) { if got_signal(sig) {

View File

@@ -1,16 +1,11 @@
use std::{ use std::{
cell::RefCell, cell::RefCell, collections::{HashMap, HashSet, VecDeque}, fmt::Display, ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref}, os::unix::fs::PermissionsExt, str::FromStr, time::Duration
collections::{HashMap, VecDeque},
fmt::Display,
ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref},
str::FromStr,
time::Duration,
}; };
use nix::unistd::{User, gethostname, getppid}; use nix::unistd::{User, gethostname, getppid};
use crate::{ use crate::{
builtin::trap::TrapTarget, exec_input, jobs::JobTab, libsh::{ builtin::{BUILTINS, trap::TrapTarget}, exec_input, jobs::JobTab, libsh::{
error::{ShErr, ShErrKind, ShResult}, error::{ShErr, ShErrKind, ShResult},
utils::VecDequeExt, utils::VecDequeExt,
}, parse::{ConjunctNode, NdRule, Node, ParsedSrc}, prelude::*, prompt::readline::markers, shopt::ShOpts }, parse::{ConjunctNode, NdRule, Node, ParsedSrc}, prelude::*, prompt::readline::markers, shopt::ShOpts
@@ -741,13 +736,104 @@ pub struct MetaTab {
// pending system messages // pending system messages
system_msg: Vec<String>, 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 { impl MetaTab {
pub fn new() -> Self { pub fn new() -> Self {
Self::default() 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) = std::fs::metadata(entry.path()) else { continue };
let is_exec = meta.permissions().mode() & 0o111 != 0;
if meta.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) = std::fs::metadata(entry.path()) else { continue };
let is_exec = meta.permissions().mode() & 0o111 != 0;
if meta.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) = std::fs::metadata(entry.path()) else { continue };
let is_exec = meta.permissions().mode() & 0o111 != 0;
if meta.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) { pub fn start_timer(&mut self) {
self.runtime_start = Some(Instant::now()); self.runtime_start = Some(Instant::now());
} }

View File

@@ -1,6 +1,7 @@
use std::collections::HashSet; 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 crate::state::VarFlags;
use super::*; use super::*;
@@ -296,7 +297,7 @@ fn dquote_escape_dollar() {
// "\$foo" should strip backslash, produce literal $foo (no expansion) // "\$foo" should strip backslash, produce literal $foo (no expansion)
let result = unescape_str(r#""\$foo""#); let result = unescape_str(r#""\$foo""#);
assert!( assert!(
!result.contains(VAR_SUB), !result.contains(markers::VAR_SUB),
"Escaped $ should not become VAR_SUB" "Escaped $ should not become VAR_SUB"
); );
assert!(result.contains('$'), "Literal $ should be preserved"); assert!(result.contains('$'), "Literal $ should be preserved");
@@ -307,7 +308,7 @@ fn dquote_escape_dollar() {
fn dquote_escape_backslash() { fn dquote_escape_backslash() {
// "\\" in double quotes should produce a single backslash // "\\" in double quotes should produce a single backslash
let result = unescape_str(r#""\\""#); 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!( assert_eq!(
inner, "\\", inner, "\\",
"Double backslash should produce single backslash" "Double backslash should produce single backslash"
@@ -318,7 +319,7 @@ fn dquote_escape_backslash() {
fn dquote_escape_quote() { fn dquote_escape_quote() {
// "\"" should produce a literal double quote // "\"" should produce a literal double quote
let result = unescape_str(r#""\"""#); 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!( assert!(
inner.contains('"'), inner.contains('"'),
"Escaped quote should produce literal quote" "Escaped quote should produce literal quote"
@@ -329,7 +330,7 @@ fn dquote_escape_quote() {
fn dquote_escape_backtick() { fn dquote_escape_backtick() {
// "\`" should strip backslash, produce literal backtick // "\`" should strip backslash, produce literal backtick
let result = unescape_str(r#""\`""#); 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!( assert_eq!(
inner, "`", inner, "`",
"Escaped backtick should produce literal backtick" "Escaped backtick should produce literal backtick"
@@ -340,7 +341,7 @@ fn dquote_escape_backtick() {
fn dquote_escape_nonspecial_preserves_backslash() { fn dquote_escape_nonspecial_preserves_backslash() {
// "\a" inside double quotes should preserve the backslash (a is not special) // "\a" inside double quotes should preserve the backslash (a is not special)
let result = unescape_str(r#""\a""#); 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!( assert_eq!(
inner, "\\a", inner, "\\a",
"Backslash before non-special char should be preserved" "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) // "$foo" inside double quotes should produce VAR_SUB (expansion marker)
let result = unescape_str(r#""$foo""#); let result = unescape_str(r#""$foo""#);
assert!( assert!(
result.contains(VAR_SUB), result.contains(markers::VAR_SUB),
"Unescaped $ should become VAR_SUB" "Unescaped $ should become VAR_SUB"
); );
} }
@@ -361,10 +362,10 @@ fn dquote_unescaped_dollar_expands() {
fn dquote_mixed_escapes() { fn dquote_mixed_escapes() {
// "hello \$world \\end" should have literal $, single backslash // "hello \$world \\end" should have literal $, single backslash
let result = unescape_str(r#""hello \$world \\end""#); 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"); assert!(result.contains('$'), "Literal $ should be in output");
// Should have exactly one backslash (from \\) // 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(); let backslash_count = inner.chars().filter(|&c| c == '\\').count();
assert_eq!(backslash_count, 1, "\\\\ should produce one backslash"); assert_eq!(backslash_count, 1, "\\\\ should produce one backslash");
} }

View File

@@ -21,7 +21,7 @@ fn getopt_from_argv() {
panic!() 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!(words);
insta::assert_debug_snapshot!(opts) insta::assert_debug_snapshot!(opts)
} }

View File

@@ -1,18 +1,12 @@
use std::collections::VecDeque; use std::collections::VecDeque;
use crate::{ use crate::{
libsh::{ expand::expand_prompt, libsh::{
error::ShErr, error::ShErr,
term::{Style, Styled}, term::{Style, Styled},
}, }, prompt::readline::{
prompt::readline::{ Prompt, ShedVi, history::History, keys::{KeyCode, KeyEvent, ModKeys}, linebuf::LineBuf, term::{KeyReader, LineWriter, raw_mode}, vimode::{ViInsert, ViMode, ViNormal}
history::History, }
keys::{KeyCode, KeyEvent, ModKeys},
linebuf::LineBuf,
term::{raw_mode, KeyReader, LineWriter},
vimode::{ViInsert, ViMode, ViNormal},
ShedVi,
},
}; };
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
@@ -255,6 +249,13 @@ fn linebuf_ascii_content() {
assert_eq!(buf.slice_from(2), Some("llo")); 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] #[test]
fn linebuf_unicode_graphemes() { fn linebuf_unicode_graphemes() {
let mut buf = LineBuf::new().with_initial("a🇺🇸b́c", 0); let mut buf = LineBuf::new().with_initial("a🇺🇸b́c", 0);
@@ -598,7 +599,7 @@ fn editor_delete_line_up() {
"dk", "dk",
LOREM_IPSUM, LOREM_IPSUM,
237), 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,)
) )
} }