command arguments are now underlined if they match an existing path -m ran rustfmt on the entire codebase
This commit is contained in:
@@ -3,7 +3,7 @@ use crate::{
|
|||||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||||
parse::{NdRule, Node},
|
parse::{NdRule, Node},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
procio::{borrow_fd, IoStack},
|
procio::{IoStack, borrow_fd},
|
||||||
state::{self, read_logic, write_logic},
|
state::{self, read_logic, write_logic},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -49,9 +49,13 @@ pub fn cd(node: Node, job: &mut JobBldr) -> ShResult<()> {
|
|||||||
span,
|
span,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let new_dir = env::current_dir().map_err(
|
let new_dir = env::current_dir().map_err(|e| {
|
||||||
|e| ShErr::full(ShErrKind::ExecFail, format!("cd: Failed to get current directory: {}", e), span)
|
ShErr::full(
|
||||||
)?;
|
ShErrKind::ExecFail,
|
||||||
|
format!("cd: Failed to get current directory: {}", e),
|
||||||
|
span,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
unsafe { env::set_var("PWD", new_dir) };
|
unsafe { env::set_var("PWD", new_dir) };
|
||||||
|
|
||||||
state::set_status(0);
|
state::set_status(0);
|
||||||
|
|||||||
@@ -1,14 +1,34 @@
|
|||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
builtin::setup_builtin, expand::expand_prompt, getopt::{Opt, OptSpec, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node}, prelude::*, procio::{IoStack, borrow_fd}, state
|
builtin::setup_builtin,
|
||||||
|
expand::expand_prompt,
|
||||||
|
getopt::{Opt, OptSpec, get_opts_from_tokens},
|
||||||
|
jobs::JobBldr,
|
||||||
|
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
|
||||||
|
parse::{NdRule, Node},
|
||||||
|
prelude::*,
|
||||||
|
procio::{IoStack, borrow_fd},
|
||||||
|
state,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const ECHO_OPTS: [OptSpec;4] = [
|
pub const ECHO_OPTS: [OptSpec; 4] = [
|
||||||
OptSpec { opt: Opt::Short('n'), takes_arg: false },
|
OptSpec {
|
||||||
OptSpec { opt: Opt::Short('E'), takes_arg: false },
|
opt: Opt::Short('n'),
|
||||||
OptSpec { opt: Opt::Short('e'), takes_arg: false },
|
takes_arg: false,
|
||||||
OptSpec { opt: Opt::Short('p'), takes_arg: false },
|
},
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Short('E'),
|
||||||
|
takes_arg: false,
|
||||||
|
},
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Short('e'),
|
||||||
|
takes_arg: false,
|
||||||
|
},
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Short('p'),
|
||||||
|
takes_arg: false,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
bitflags! {
|
bitflags! {
|
||||||
@@ -40,13 +60,15 @@ pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
|
|||||||
borrow_fd(STDOUT_FILENO)
|
borrow_fd(STDOUT_FILENO)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut echo_output = prepare_echo_args(argv
|
let mut echo_output = prepare_echo_args(
|
||||||
|
argv
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|a| a.0) // Extract the String from the tuple of (String,Span)
|
.map(|a| a.0) // Extract the String from the tuple of (String,Span)
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
flags.contains(EchoFlags::USE_ESCAPE),
|
flags.contains(EchoFlags::USE_ESCAPE),
|
||||||
flags.contains(EchoFlags::USE_PROMPT)
|
flags.contains(EchoFlags::USE_PROMPT),
|
||||||
)?.join(" ");
|
)?
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
if !flags.contains(EchoFlags::NO_NEWLINE) && !echo_output.ends_with('\n') {
|
if !flags.contains(EchoFlags::NO_NEWLINE) && !echo_output.ends_with('\n') {
|
||||||
echo_output.push('\n')
|
echo_output.push('\n')
|
||||||
@@ -58,14 +80,18 @@ pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn prepare_echo_args(argv: Vec<String>, use_escape: bool, use_prompt: bool) -> ShResult<Vec<String>> {
|
pub fn prepare_echo_args(
|
||||||
|
argv: Vec<String>,
|
||||||
|
use_escape: bool,
|
||||||
|
use_prompt: bool,
|
||||||
|
) -> ShResult<Vec<String>> {
|
||||||
if !use_escape {
|
if !use_escape {
|
||||||
if use_prompt {
|
if use_prompt {
|
||||||
let expanded: ShResult<Vec<String>> = argv
|
let expanded: ShResult<Vec<String>> = argv
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|s| expand_prompt(s.as_str()))
|
.map(|s| expand_prompt(s.as_str()))
|
||||||
.collect();
|
.collect();
|
||||||
return expanded
|
return expanded;
|
||||||
}
|
}
|
||||||
return Ok(argv);
|
return Ok(argv);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||||
parse::{execute::prepare_argv, NdRule, Node},
|
parse::{NdRule, Node, execute::prepare_argv},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,7 +32,6 @@ pub fn flowctl(node: Node, kind: ShErrKind) -> ShResult<()> {
|
|||||||
code = status;
|
code = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let kind = match kind {
|
let kind = match kind {
|
||||||
LoopContinue(_) => LoopContinue(code),
|
LoopContinue(_) => LoopContinue(code),
|
||||||
LoopBreak(_) => LoopBreak(code),
|
LoopBreak(_) => LoopBreak(code),
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
jobs::{JobBldr, JobCmdFlags, JobID},
|
jobs::{JobBldr, JobCmdFlags, JobID},
|
||||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||||
parse::{lex::Span, NdRule, Node},
|
parse::{NdRule, Node, lex::Span},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
procio::{borrow_fd, IoStack},
|
procio::{IoStack, borrow_fd},
|
||||||
state::{self, read_jobs, write_jobs},
|
state::{self, read_jobs, write_jobs},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -168,7 +168,7 @@ pub fn jobs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
|
|||||||
ShErrKind::SyntaxErr,
|
ShErrKind::SyntaxErr,
|
||||||
"Invalid flag in jobs call",
|
"Invalid flag in jobs call",
|
||||||
span,
|
span,
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
flags |= flag
|
flags |= flag
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ use crate::{
|
|||||||
jobs::{ChildProc, JobBldr},
|
jobs::{ChildProc, JobBldr},
|
||||||
libsh::error::ShResult,
|
libsh::error::ShResult,
|
||||||
parse::{
|
parse::{
|
||||||
Redir, execute::prepare_argv, lex::{Span, Tk}
|
Redir,
|
||||||
|
execute::prepare_argv,
|
||||||
|
lex::{Span, Tk},
|
||||||
},
|
},
|
||||||
procio::{IoFrame, IoStack, RedirGuard},
|
procio::{IoFrame, IoStack, RedirGuard},
|
||||||
};
|
};
|
||||||
@@ -16,19 +18,17 @@ pub mod export;
|
|||||||
pub mod flowctl;
|
pub mod flowctl;
|
||||||
pub mod jobctl;
|
pub mod jobctl;
|
||||||
pub mod pwd;
|
pub mod pwd;
|
||||||
|
pub mod read;
|
||||||
pub mod shift;
|
pub mod shift;
|
||||||
pub mod shopt;
|
pub mod shopt;
|
||||||
pub mod source;
|
pub mod source;
|
||||||
pub mod test; // [[ ]] thing
|
pub mod test; // [[ ]] thing
|
||||||
pub mod read;
|
|
||||||
pub mod zoltraak;
|
|
||||||
pub mod trap;
|
pub mod trap;
|
||||||
|
pub mod zoltraak;
|
||||||
|
|
||||||
pub const BUILTINS: [&str; 21] = [
|
pub const BUILTINS: [&str; 21] = [
|
||||||
"echo", "cd", "read", "export", "pwd", "source",
|
"echo", "cd", "read", "export", "pwd", "source", "shift", "jobs", "fg", "bg", "alias", "unalias",
|
||||||
"shift", "jobs", "fg", "bg", "alias", "unalias",
|
"return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", "command", "trap",
|
||||||
"return", "break", "continue", "exit", "zoltraak",
|
|
||||||
"shopt", "builtin", "command", "trap"
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Sets up a builtin command
|
/// Sets up a builtin command
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::{
|
|||||||
libsh::error::ShResult,
|
libsh::error::ShResult,
|
||||||
parse::{NdRule, Node},
|
parse::{NdRule, Node},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
procio::{borrow_fd, IoStack},
|
procio::{IoStack, borrow_fd},
|
||||||
state,
|
state,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,50 @@
|
|||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
use nix::{errno::Errno, libc::{STDIN_FILENO, STDOUT_FILENO}, unistd::{isatty, read, write}};
|
use nix::{
|
||||||
|
errno::Errno,
|
||||||
|
libc::{STDIN_FILENO, STDOUT_FILENO},
|
||||||
|
unistd::{isatty, read, write},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{builtin::setup_builtin, getopt::{Opt, OptSpec, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node}, procio::{IoStack, borrow_fd}, prompt::readline::term::RawModeGuard, state::{self, VarFlags, read_vars, write_vars}};
|
use crate::{
|
||||||
|
builtin::setup_builtin,
|
||||||
|
getopt::{Opt, OptSpec, get_opts_from_tokens},
|
||||||
|
jobs::JobBldr,
|
||||||
|
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
|
||||||
|
parse::{NdRule, Node},
|
||||||
|
procio::{IoStack, borrow_fd},
|
||||||
|
prompt::readline::term::RawModeGuard,
|
||||||
|
state::{self, VarFlags, read_vars, write_vars},
|
||||||
|
};
|
||||||
|
|
||||||
pub const READ_OPTS: [OptSpec;7] = [
|
pub const READ_OPTS: [OptSpec; 7] = [
|
||||||
OptSpec { opt: Opt::Short('r'), takes_arg: false }, // don't allow backslash escapes
|
OptSpec {
|
||||||
OptSpec { opt: Opt::Short('s'), takes_arg: false }, // don't echo input
|
opt: Opt::Short('r'),
|
||||||
OptSpec { opt: Opt::Short('a'), takes_arg: false }, // read into array
|
takes_arg: false,
|
||||||
OptSpec { opt: Opt::Short('n'), takes_arg: false }, // read only N characters
|
}, // don't allow backslash escapes
|
||||||
OptSpec { opt: Opt::Short('t'), takes_arg: false }, // timeout
|
OptSpec {
|
||||||
OptSpec { opt: Opt::Short('p'), takes_arg: true }, // prompt
|
opt: Opt::Short('s'),
|
||||||
OptSpec { opt: Opt::Short('d'), takes_arg: true }, // read until delimiter
|
takes_arg: false,
|
||||||
|
}, // don't echo input
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Short('a'),
|
||||||
|
takes_arg: false,
|
||||||
|
}, // read into array
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Short('n'),
|
||||||
|
takes_arg: false,
|
||||||
|
}, // read only N characters
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Short('t'),
|
||||||
|
takes_arg: false,
|
||||||
|
}, // timeout
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Short('p'),
|
||||||
|
takes_arg: true,
|
||||||
|
}, // prompt
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Short('d'),
|
||||||
|
takes_arg: true,
|
||||||
|
}, // read until delimiter
|
||||||
];
|
];
|
||||||
|
|
||||||
bitflags! {
|
bitflags! {
|
||||||
@@ -33,8 +67,9 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S
|
|||||||
let blame = node.get_span().clone();
|
let blame = node.get_span().clone();
|
||||||
let NdRule::Command {
|
let NdRule::Command {
|
||||||
assignments: _,
|
assignments: _,
|
||||||
argv
|
argv,
|
||||||
} = node.class else {
|
} = node.class
|
||||||
|
else {
|
||||||
unreachable!()
|
unreachable!()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -52,14 +87,16 @@ 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![];
|
||||||
let mut escaped = false;
|
let mut escaped = false;
|
||||||
loop {
|
loop {
|
||||||
let mut buf = [0u8;1];
|
let mut buf = [0u8; 1];
|
||||||
match read(STDIN_FILENO, &mut buf) {
|
match read(STDIN_FILENO, &mut buf) {
|
||||||
Ok(0) => {
|
Ok(0) => {
|
||||||
state::set_status(1);
|
state::set_status(1);
|
||||||
let str_result = String::from_utf8(input.clone()).map_err(|e| ShErr::simple(
|
let str_result = String::from_utf8(input.clone()).map_err(|e| {
|
||||||
|
ShErr::simple(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
format!("read: Input was not valid UTF-8: {e}"),
|
format!("read: Input was not valid UTF-8: {e}"),
|
||||||
))?;
|
)
|
||||||
|
})?;
|
||||||
return Ok(str_result); // EOF
|
return Ok(str_result); // EOF
|
||||||
}
|
}
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
@@ -70,33 +107,36 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S
|
|||||||
// Delimiter reached, stop reading
|
// Delimiter reached, stop reading
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
} else if read_opts.flags.contains(ReadFlags::NO_ESCAPES) && buf[0] == b'\\' {
|
||||||
else if read_opts.flags.contains(ReadFlags::NO_ESCAPES)
|
|
||||||
&& buf[0] == b'\\' {
|
|
||||||
escaped = true;
|
escaped = true;
|
||||||
} else {
|
} else {
|
||||||
input.push(buf[0]);
|
input.push(buf[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(Errno::EINTR) => continue,
|
Err(Errno::EINTR) => continue,
|
||||||
Err(e) => return Err(ShErr::simple(
|
Err(e) => {
|
||||||
|
return Err(ShErr::simple(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
format!("read: Failed to read from stdin: {e}"),
|
format!("read: Failed to read from stdin: {e}"),
|
||||||
)),
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state::set_status(0);
|
state::set_status(0);
|
||||||
let str_result = String::from_utf8(input.clone()).map_err(|e| ShErr::simple(
|
let str_result = String::from_utf8(input.clone()).map_err(|e| {
|
||||||
|
ShErr::simple(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
format!("read: Input was not valid UTF-8: {e}"),
|
format!("read: Input was not valid UTF-8: {e}"),
|
||||||
))?;
|
)
|
||||||
|
})?;
|
||||||
Ok(str_result)
|
Ok(str_result)
|
||||||
}).blame(blame)?
|
})
|
||||||
|
.blame(blame)?
|
||||||
} else {
|
} else {
|
||||||
let mut input: Vec<u8> = vec![];
|
let mut input: Vec<u8> = vec![];
|
||||||
loop {
|
loop {
|
||||||
let mut buf = [0u8;1];
|
let mut buf = [0u8; 1];
|
||||||
match read(STDIN_FILENO, &mut buf) {
|
match read(STDIN_FILENO, &mut buf) {
|
||||||
Ok(0) => {
|
Ok(0) => {
|
||||||
state::set_status(1);
|
state::set_status(1);
|
||||||
@@ -109,16 +149,20 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S
|
|||||||
input.push(buf[0]);
|
input.push(buf[0]);
|
||||||
}
|
}
|
||||||
Err(Errno::EINTR) => continue,
|
Err(Errno::EINTR) => continue,
|
||||||
Err(e) => return Err(ShErr::simple(
|
Err(e) => {
|
||||||
|
return Err(ShErr::simple(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
format!("read: Failed to read from stdin: {e}"),
|
format!("read: Failed to read from stdin: {e}"),
|
||||||
)),
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
String::from_utf8(input).map_err(|e| ShErr::simple(
|
}
|
||||||
|
String::from_utf8(input).map_err(|e| {
|
||||||
|
ShErr::simple(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
format!("read: Input was not valid UTF-8: {e}"),
|
format!("read: Input was not valid UTF-8: {e}"),
|
||||||
))?
|
)
|
||||||
|
})?
|
||||||
};
|
};
|
||||||
|
|
||||||
if argv.is_empty() {
|
if argv.is_empty() {
|
||||||
@@ -128,7 +172,9 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S
|
|||||||
} 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"));
|
||||||
if field_sep.is_empty() { field_sep = " ".to_string() }
|
if field_sep.is_empty() {
|
||||||
|
field_sep = " ".to_string()
|
||||||
|
}
|
||||||
let mut remaining = input;
|
let mut remaining = input;
|
||||||
|
|
||||||
for (i, arg) in argv.iter().enumerate() {
|
for (i, arg) in argv.iter().enumerate() {
|
||||||
@@ -146,7 +192,8 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S
|
|||||||
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 that trim above is for
|
// note that this doesn't account for consecutive IFS characters, which is what
|
||||||
|
// 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));
|
||||||
@@ -174,7 +221,9 @@ pub fn get_read_flags(opts: Vec<Opt>) -> ShResult<ReadOpts> {
|
|||||||
Opt::Short('n') => read_opts.flags |= ReadFlags::N_CHARS,
|
Opt::Short('n') => read_opts.flags |= ReadFlags::N_CHARS,
|
||||||
Opt::Short('t') => read_opts.flags |= ReadFlags::TIMEOUT,
|
Opt::Short('t') => read_opts.flags |= ReadFlags::TIMEOUT,
|
||||||
Opt::ShortWithArg('p', prompt) => read_opts.prompt = Some(prompt),
|
Opt::ShortWithArg('p', prompt) => read_opts.prompt = Some(prompt),
|
||||||
Opt::ShortWithArg('d', delim) => read_opts.delim = delim.chars().map(|c| c as u8).next().unwrap_or(b'\n'),
|
Opt::ShortWithArg('d', delim) => {
|
||||||
|
read_opts.delim = delim.chars().map(|c| c as u8).next().unwrap_or(b'\n')
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(ShErr::simple(
|
return Err(ShErr::simple(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::{
|
|||||||
libsh::error::{ShResult, ShResultExt},
|
libsh::error::{ShResult, ShResultExt},
|
||||||
parse::{NdRule, Node},
|
parse::{NdRule, Node},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
procio::{borrow_fd, IoStack},
|
procio::{IoStack, borrow_fd},
|
||||||
state::write_shopts,
|
state::write_shopts,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use regex::Regex;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||||
parse::{ConjunctOp, NdRule, Node, TestCase, TEST_UNARY_OPS},
|
parse::{ConjunctOp, NdRule, Node, TEST_UNARY_OPS, TestCase},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -254,7 +254,7 @@ pub fn double_bracket_test(node: Node) -> ShResult<bool> {
|
|||||||
msg: "Expected a binary operator in this test call; found a unary operator".into(),
|
msg: "Expected a binary operator in this test call; found a unary operator".into(),
|
||||||
notes: vec![],
|
notes: vec![],
|
||||||
span: err_span,
|
span: err_span,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
TestOp::StringEq => rhs.trim() == lhs.trim(),
|
TestOp::StringEq => rhs.trim() == lhs.trim(),
|
||||||
TestOp::StringNeq => rhs.trim() != lhs.trim(),
|
TestOp::StringNeq => rhs.trim() != lhs.trim(),
|
||||||
|
|||||||
@@ -1,14 +1,25 @@
|
|||||||
use std::{fmt::Display, str::FromStr};
|
use std::{fmt::Display, str::FromStr};
|
||||||
|
|
||||||
use nix::{libc::{STDERR_FILENO, STDOUT_FILENO}, sys::signal::Signal, unistd::write};
|
use nix::{
|
||||||
|
libc::{STDERR_FILENO, STDOUT_FILENO},
|
||||||
|
sys::signal::Signal,
|
||||||
|
unistd::write,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{builtin::setup_builtin, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node}, procio::{IoStack, borrow_fd}, state::{self, read_logic, write_logic}};
|
use crate::{
|
||||||
|
builtin::setup_builtin,
|
||||||
|
jobs::JobBldr,
|
||||||
|
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||||
|
parse::{NdRule, Node},
|
||||||
|
procio::{IoStack, borrow_fd},
|
||||||
|
state::{self, read_logic, write_logic},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash)]
|
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash)]
|
||||||
pub enum TrapTarget {
|
pub enum TrapTarget {
|
||||||
Exit,
|
Exit,
|
||||||
Error,
|
Error,
|
||||||
Signal(Signal)
|
Signal(Signal),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for TrapTarget {
|
impl FromStr for TrapTarget {
|
||||||
@@ -52,7 +63,7 @@ impl FromStr for TrapTarget {
|
|||||||
return Err(ShErr::simple(
|
return Err(ShErr::simple(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
format!("invalid trap target '{}'", s),
|
format!("invalid trap target '{}'", s),
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,8 +74,7 @@ impl Display for TrapTarget {
|
|||||||
match self {
|
match self {
|
||||||
TrapTarget::Exit => write!(f, "EXIT"),
|
TrapTarget::Exit => write!(f, "EXIT"),
|
||||||
TrapTarget::Error => write!(f, "ERR"),
|
TrapTarget::Error => write!(f, "ERR"),
|
||||||
TrapTarget::Signal(s) => {
|
TrapTarget::Signal(s) => match s {
|
||||||
match s {
|
|
||||||
Signal::SIGHUP => write!(f, "HUP"),
|
Signal::SIGHUP => write!(f, "HUP"),
|
||||||
Signal::SIGINT => write!(f, "INT"),
|
Signal::SIGINT => write!(f, "INT"),
|
||||||
Signal::SIGQUIT => write!(f, "QUIT"),
|
Signal::SIGQUIT => write!(f, "QUIT"),
|
||||||
@@ -101,8 +111,7 @@ impl Display for TrapTarget {
|
|||||||
log::warn!("TrapTarget::fmt() : unrecognized signal {}", s);
|
log::warn!("TrapTarget::fmt() : unrecognized signal {}", s);
|
||||||
Err(std::fmt::Error)
|
Err(std::fmt::Error)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,7 +145,7 @@ pub fn trap(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
|
|||||||
let stderr = borrow_fd(STDERR_FILENO);
|
let stderr = borrow_fd(STDERR_FILENO);
|
||||||
write(stderr, b"usage: trap <COMMAND> [SIGNAL...]\n")?;
|
write(stderr, b"usage: trap <COMMAND> [SIGNAL...]\n")?;
|
||||||
state::set_status(1);
|
state::set_status(1);
|
||||||
return Ok(())
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut args = argv.into_iter();
|
let mut args = argv.into_iter();
|
||||||
|
|||||||
@@ -38,12 +38,30 @@ pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu
|
|||||||
unreachable!()
|
unreachable!()
|
||||||
};
|
};
|
||||||
let zolt_opts = [
|
let zolt_opts = [
|
||||||
OptSpec { opt: Opt::Long("dry-run".into()), takes_arg: false },
|
OptSpec {
|
||||||
OptSpec { opt: Opt::Long("confirm".into()), takes_arg: false },
|
opt: Opt::Long("dry-run".into()),
|
||||||
OptSpec { opt: Opt::Long("no-preserve-root".into()), takes_arg: false },
|
takes_arg: false,
|
||||||
OptSpec { opt: Opt::Short('r'), takes_arg: false },
|
},
|
||||||
OptSpec { opt: Opt::Short('f'), takes_arg: false },
|
OptSpec {
|
||||||
OptSpec { opt: Opt::Short('v'), takes_arg: false }
|
opt: Opt::Long("confirm".into()),
|
||||||
|
takes_arg: false,
|
||||||
|
},
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Long("no-preserve-root".into()),
|
||||||
|
takes_arg: false,
|
||||||
|
},
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Short('r'),
|
||||||
|
takes_arg: false,
|
||||||
|
},
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Short('f'),
|
||||||
|
takes_arg: false,
|
||||||
|
},
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Short('v'),
|
||||||
|
takes_arg: false,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
let mut flags = ZoltFlags::empty();
|
let mut flags = ZoltFlags::empty();
|
||||||
|
|
||||||
@@ -90,7 +108,6 @@ pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu
|
|||||||
|
|
||||||
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
|
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
|
||||||
|
|
||||||
|
|
||||||
for (arg, span) in argv {
|
for (arg, span) in argv {
|
||||||
if &arg == "/" && !flags.contains(ZoltFlags::NO_PRESERVE_ROOT) {
|
if &arg == "/" && !flags.contains(ZoltFlags::NO_PRESERVE_ROOT) {
|
||||||
return Err(
|
return Err(
|
||||||
@@ -109,7 +126,6 @@ pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
134
src/expand.rs
134
src/expand.rs
@@ -7,11 +7,13 @@ use regex::Regex;
|
|||||||
|
|
||||||
use crate::libsh::error::{ShErr, ShErrKind, ShResult};
|
use crate::libsh::error::{ShErr, ShErrKind, ShResult};
|
||||||
use crate::parse::execute::exec_input;
|
use crate::parse::execute::exec_input;
|
||||||
use crate::parse::lex::{is_field_sep, is_hard_sep, LexFlags, LexStream, Tk, TkFlags, TkRule};
|
use crate::parse::lex::{LexFlags, LexStream, Tk, TkFlags, TkRule, is_field_sep, is_hard_sep};
|
||||||
use crate::parse::{Redir, RedirType};
|
use crate::parse::{Redir, RedirType};
|
||||||
use crate::{jobs, prelude::*};
|
|
||||||
use crate::procio::{IoBuf, IoFrame, IoMode, IoStack};
|
use crate::procio::{IoBuf, IoFrame, IoMode, IoStack};
|
||||||
use crate::state::{LogTab, VarFlags, read_jobs, read_logic, read_vars, write_jobs, write_meta, write_vars};
|
use crate::state::{
|
||||||
|
LogTab, VarFlags, read_jobs, read_logic, read_vars, write_jobs, write_meta, write_vars,
|
||||||
|
};
|
||||||
|
use crate::{jobs, prelude::*};
|
||||||
|
|
||||||
const PARAMETERS: [char; 7] = ['@', '*', '#', '$', '?', '!', '0'];
|
const PARAMETERS: [char; 7] = ['@', '*', '#', '$', '?', '!', '0'];
|
||||||
|
|
||||||
@@ -30,8 +32,9 @@ pub const PROC_SUB_IN: char = '\u{fdd5}';
|
|||||||
/// Output process sub marker
|
/// Output process sub marker
|
||||||
pub const PROC_SUB_OUT: char = '\u{fdd6}';
|
pub const PROC_SUB_OUT: char = '\u{fdd6}';
|
||||||
/// Marker for null expansion
|
/// Marker for null expansion
|
||||||
/// This is used for when "$@" or "$*" are used in quotes and there are no arguments
|
/// This is used for when "$@" or "$*" are used in quotes and there are no
|
||||||
/// Without this marker, it would be handled like an empty string, which breaks some commands
|
/// arguments Without this marker, it would be handled like an empty string,
|
||||||
|
/// which breaks some commands
|
||||||
pub const NULL_EXPAND: char = '\u{fdd7}';
|
pub const NULL_EXPAND: char = '\u{fdd7}';
|
||||||
|
|
||||||
impl Tk {
|
impl Tk {
|
||||||
@@ -74,13 +77,14 @@ impl Expander {
|
|||||||
let has_leading_dot_slash = self.raw.starts_with("./");
|
let has_leading_dot_slash = self.raw.starts_with("./");
|
||||||
|
|
||||||
if let Ok(glob_exp) = expand_glob(&self.raw)
|
if let Ok(glob_exp) = expand_glob(&self.raw)
|
||||||
&& !glob_exp.is_empty() {
|
&& !glob_exp.is_empty()
|
||||||
|
{
|
||||||
self.raw = glob_exp;
|
self.raw = glob_exp;
|
||||||
}
|
}
|
||||||
|
|
||||||
if has_trailing_slash && !self.raw.ends_with('/') {
|
if has_trailing_slash && !self.raw.ends_with('/') {
|
||||||
// glob expansion can remove trailing slashes and leading dot-slashes, but we want to preserve them
|
// glob expansion can remove trailing slashes and leading dot-slashes, but we
|
||||||
// so that things like tab completion don't break
|
// want to preserve them so that things like tab completion don't break
|
||||||
self.raw.push('/');
|
self.raw.push('/');
|
||||||
}
|
}
|
||||||
if has_leading_dot_slash && !self.raw.starts_with("./") {
|
if has_leading_dot_slash && !self.raw.starts_with("./") {
|
||||||
@@ -132,7 +136,8 @@ impl Expander {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a string contains valid brace expansion patterns.
|
/// Check if a string contains valid brace expansion patterns.
|
||||||
/// Returns true if there's a valid {a,b} or {1..5} pattern at the outermost level.
|
/// Returns true if there's a valid {a,b} or {1..5} pattern at the outermost
|
||||||
|
/// level.
|
||||||
fn has_braces(s: &str) -> bool {
|
fn has_braces(s: &str) -> bool {
|
||||||
let mut chars = s.chars().peekable();
|
let mut chars = s.chars().peekable();
|
||||||
let mut depth = 0;
|
let mut depth = 0;
|
||||||
@@ -143,7 +148,9 @@ fn has_braces(s: &str) -> bool {
|
|||||||
|
|
||||||
while let Some(ch) = chars.next() {
|
while let Some(ch) = chars.next() {
|
||||||
match ch {
|
match ch {
|
||||||
'\\' => { chars.next(); } // skip escaped char
|
'\\' => {
|
||||||
|
chars.next();
|
||||||
|
} // skip escaped char
|
||||||
'\'' if cur_quote.is_none() => cur_quote = Some('\''),
|
'\'' if cur_quote.is_none() => cur_quote = Some('\''),
|
||||||
'\'' if cur_quote == Some('\'') => cur_quote = None,
|
'\'' if cur_quote == Some('\'') => cur_quote = None,
|
||||||
'"' if cur_quote.is_none() => cur_quote = Some('"'),
|
'"' if cur_quote.is_none() => cur_quote = Some('"'),
|
||||||
@@ -222,19 +229,23 @@ fn expand_one_brace(word: &str) -> ShResult<Vec<String>> {
|
|||||||
if parts.len() == 1 && parts[0] == inner {
|
if parts.len() == 1 && parts[0] == inner {
|
||||||
// Check if it's a range
|
// Check if it's a range
|
||||||
if let Some(range_parts) = try_expand_range(&inner) {
|
if let Some(range_parts) = try_expand_range(&inner) {
|
||||||
return Ok(range_parts
|
return Ok(
|
||||||
|
range_parts
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|p| format!("{}{}{}", prefix, p, suffix))
|
.map(|p| format!("{}{}{}", prefix, p, suffix))
|
||||||
.collect());
|
.collect(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Not a valid brace expression, return as-is with literal braces
|
// Not a valid brace expression, return as-is with literal braces
|
||||||
return Ok(vec![format!("{}{{{}}}{}", prefix, inner, suffix)]);
|
return Ok(vec![format!("{}{{{}}}{}", prefix, inner, suffix)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(parts
|
Ok(
|
||||||
|
parts
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|p| format!("{}{}{}", prefix, p, suffix))
|
.map(|p| format!("{}{}{}", prefix, p, suffix))
|
||||||
.collect())
|
.collect(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract prefix, inner, and suffix from a brace expression.
|
/// Extract prefix, inner, and suffix from a brace expression.
|
||||||
@@ -253,12 +264,22 @@ fn get_brace_parts(word: &str) -> Option<(String, String, String)> {
|
|||||||
prefix.push(next);
|
prefix.push(next);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'\'' if cur_quote.is_none() => { cur_quote = Some('\'');
|
'\'' if cur_quote.is_none() => {
|
||||||
prefix.push(ch); }
|
cur_quote = Some('\'');
|
||||||
'\'' if cur_quote == Some('\'') => { cur_quote = None;
|
prefix.push(ch);
|
||||||
prefix.push(ch); }
|
}
|
||||||
'"' if cur_quote.is_none() => { cur_quote = Some('"'); prefix.push(ch); }
|
'\'' if cur_quote == Some('\'') => {
|
||||||
'"' if cur_quote == Some('"') => { cur_quote = None; prefix.push(ch); }
|
cur_quote = None;
|
||||||
|
prefix.push(ch);
|
||||||
|
}
|
||||||
|
'"' if cur_quote.is_none() => {
|
||||||
|
cur_quote = Some('"');
|
||||||
|
prefix.push(ch);
|
||||||
|
}
|
||||||
|
'"' if cur_quote == Some('"') => {
|
||||||
|
cur_quote = None;
|
||||||
|
prefix.push(ch);
|
||||||
|
}
|
||||||
'{' if cur_quote.is_none() => {
|
'{' if cur_quote.is_none() => {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -279,10 +300,22 @@ fn get_brace_parts(word: &str) -> Option<(String, String, String)> {
|
|||||||
inner.push(next);
|
inner.push(next);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'\'' if cur_quote.is_none() => { cur_quote = Some('\''); inner.push(ch); }
|
'\'' if cur_quote.is_none() => {
|
||||||
'\'' if cur_quote == Some('\'') => { cur_quote = None; inner.push(ch); }
|
cur_quote = Some('\'');
|
||||||
'"' if cur_quote.is_none() => { cur_quote = Some('"'); inner.push(ch); }
|
inner.push(ch);
|
||||||
'"' if cur_quote == Some('"') => { cur_quote = None; inner.push(ch); }
|
}
|
||||||
|
'\'' if cur_quote == Some('\'') => {
|
||||||
|
cur_quote = None;
|
||||||
|
inner.push(ch);
|
||||||
|
}
|
||||||
|
'"' if cur_quote.is_none() => {
|
||||||
|
cur_quote = Some('"');
|
||||||
|
inner.push(ch);
|
||||||
|
}
|
||||||
|
'"' if cur_quote == Some('"') => {
|
||||||
|
cur_quote = None;
|
||||||
|
inner.push(ch);
|
||||||
|
}
|
||||||
'{' if cur_quote.is_none() => {
|
'{' if cur_quote.is_none() => {
|
||||||
depth += 1;
|
depth += 1;
|
||||||
inner.push(ch);
|
inner.push(ch);
|
||||||
@@ -326,10 +359,22 @@ fn split_brace_inner(inner: &str) -> Vec<String> {
|
|||||||
current.push(next);
|
current.push(next);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'\'' if cur_quote.is_none() => { cur_quote = Some('\''); current.push(ch); }
|
'\'' if cur_quote.is_none() => {
|
||||||
'\'' if cur_quote == Some('\'') => { cur_quote = None; current.push(ch); }
|
cur_quote = Some('\'');
|
||||||
'"' if cur_quote.is_none() => { cur_quote = Some('"'); current.push(ch); }
|
current.push(ch);
|
||||||
'"' if cur_quote == Some('"') => { cur_quote = None; current.push(ch); }
|
}
|
||||||
|
'\'' if cur_quote == Some('\'') => {
|
||||||
|
cur_quote = None;
|
||||||
|
current.push(ch);
|
||||||
|
}
|
||||||
|
'"' if cur_quote.is_none() => {
|
||||||
|
cur_quote = Some('"');
|
||||||
|
current.push(ch);
|
||||||
|
}
|
||||||
|
'"' if cur_quote == Some('"') => {
|
||||||
|
cur_quote = None;
|
||||||
|
current.push(ch);
|
||||||
|
}
|
||||||
'{' if cur_quote.is_none() => {
|
'{' if cur_quote.is_none() => {
|
||||||
depth += 1;
|
depth += 1;
|
||||||
current.push(ch);
|
current.push(ch);
|
||||||
@@ -364,15 +409,16 @@ fn try_expand_range(inner: &str) -> Option<Vec<String>> {
|
|||||||
let start = parts[0];
|
let start = parts[0];
|
||||||
let end = parts[1];
|
let end = parts[1];
|
||||||
let step: i32 = parts[2].parse().ok()?;
|
let step: i32 = parts[2].parse().ok()?;
|
||||||
if step == 0 { return None; }
|
if step == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
expand_range(start, end, step.unsigned_abs() as usize)
|
expand_range(start, end, step.unsigned_abs() as usize)
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn expand_range(start: &str, end: &str, step: usize) ->
|
fn expand_range(start: &str, end: &str, step: usize) -> Option<Vec<String>> {
|
||||||
Option<Vec<String>> {
|
|
||||||
// Try character range first
|
// Try character range first
|
||||||
if is_alpha_range_bound(start) && is_alpha_range_bound(end) {
|
if is_alpha_range_bound(start) && is_alpha_range_bound(end) {
|
||||||
let start_char = start.chars().next()? as u8;
|
let start_char = start.chars().next()? as u8;
|
||||||
@@ -405,8 +451,7 @@ Option<Vec<String>> {
|
|||||||
|
|
||||||
// Handle zero-padding
|
// Handle zero-padding
|
||||||
let pad_width = start.len().max(end.len());
|
let pad_width = start.len().max(end.len());
|
||||||
let needs_padding = start.starts_with('0') ||
|
let needs_padding = start.starts_with('0') || end.starts_with('0');
|
||||||
end.starts_with('0');
|
|
||||||
|
|
||||||
let (lo, hi) = if reverse {
|
let (lo, hi) = if reverse {
|
||||||
(end_num, start_num)
|
(end_num, start_num)
|
||||||
@@ -435,7 +480,6 @@ Option<Vec<String>> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn is_alpha_range_bound(word: &str) -> bool {
|
fn is_alpha_range_bound(word: &str) -> bool {
|
||||||
word.len() == 1 && word.chars().all(|c| c.is_ascii_alphabetic())
|
word.len() == 1 && word.chars().all(|c| c.is_ascii_alphabetic())
|
||||||
}
|
}
|
||||||
@@ -558,8 +602,8 @@ pub fn expand_glob(raw: &str) -> ShResult<String> {
|
|||||||
require_literal_leading_dot: !crate::state::read_shopts(|s| s.core.dotglob),
|
require_literal_leading_dot: !crate::state::read_shopts(|s| s.core.dotglob),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
for entry in
|
for entry in glob::glob_with(raw, opts)
|
||||||
glob::glob_with(raw, opts).map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid glob pattern"))?
|
.map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid glob pattern"))?
|
||||||
{
|
{
|
||||||
let entry =
|
let entry =
|
||||||
entry.map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid filename found in glob"))?;
|
entry.map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid filename found in glob"))?;
|
||||||
@@ -625,7 +669,7 @@ impl ArithTk {
|
|||||||
kind: ShErrKind::ParseErr,
|
kind: ShErrKind::ParseErr,
|
||||||
msg: "Invalid character in arithmetic substitution".into(),
|
msg: "Invalid character in arithmetic substitution".into(),
|
||||||
notes: vec![],
|
notes: vec![],
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -707,7 +751,7 @@ impl ArithTk {
|
|||||||
kind: ShErrKind::ParseErr,
|
kind: ShErrKind::ParseErr,
|
||||||
msg: "Unexpected token during evaluation".into(),
|
msg: "Unexpected token during evaluation".into(),
|
||||||
notes: vec![],
|
notes: vec![],
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -807,8 +851,10 @@ pub fn expand_proc_sub(raw: &str, is_input: bool) -> ShResult<String> {
|
|||||||
|
|
||||||
/// Get the command output of a given command input as a String
|
/// Get the command output of a given command input as a String
|
||||||
pub fn expand_cmd_sub(raw: &str) -> ShResult<String> {
|
pub fn expand_cmd_sub(raw: &str) -> ShResult<String> {
|
||||||
if raw.starts_with('(') && raw.ends_with(')')
|
if raw.starts_with('(')
|
||||||
&& let Ok(output) = expand_arithmetic(raw) {
|
&& raw.ends_with(')')
|
||||||
|
&& let Ok(output) = expand_arithmetic(raw)
|
||||||
|
{
|
||||||
return Ok(output); // It's actually an arithmetic sub
|
return Ok(output); // It's actually an arithmetic sub
|
||||||
}
|
}
|
||||||
let (rpipe, wpipe) = IoMode::get_pipes();
|
let (rpipe, wpipe) = IoMode::get_pipes();
|
||||||
@@ -829,7 +875,8 @@ pub fn expand_cmd_sub(raw: &str) -> ShResult<String> {
|
|||||||
ForkResult::Parent { child } => {
|
ForkResult::Parent { child } => {
|
||||||
std::mem::drop(cmd_sub_io_frame); // Closes the write pipe
|
std::mem::drop(cmd_sub_io_frame); // Closes the write pipe
|
||||||
|
|
||||||
// Read output first (before waiting) to avoid deadlock if child fills pipe buffer
|
// Read output first (before waiting) to avoid deadlock if child fills pipe
|
||||||
|
// buffer
|
||||||
loop {
|
loop {
|
||||||
match io_buf.fill_buffer() {
|
match io_buf.fill_buffer() {
|
||||||
Ok(()) => break,
|
Ok(()) => break,
|
||||||
@@ -851,9 +898,7 @@ pub fn expand_cmd_sub(raw: &str) -> ShResult<String> {
|
|||||||
jobs::take_term()?;
|
jobs::take_term()?;
|
||||||
|
|
||||||
match status {
|
match status {
|
||||||
WtStat::Exited(_, _) => {
|
WtStat::Exited(_, _) => Ok(io_buf.as_str()?.trim_end().to_string()),
|
||||||
Ok(io_buf.as_str()?.trim_end().to_string())
|
|
||||||
}
|
|
||||||
_ => Err(ShErr::simple(ShErrKind::InternalErr, "Command sub failed")),
|
_ => Err(ShErr::simple(ShErrKind::InternalErr, "Command sub failed")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1785,7 +1830,6 @@ fn tokenize_prompt(raw: &str) -> Vec<PromptTk> {
|
|||||||
let mut func_name = String::new();
|
let mut func_name = String::new();
|
||||||
let is_braced = chars.peek() == Some(&'{');
|
let is_braced = chars.peek() == Some(&'{');
|
||||||
while let Some(ch) = chars.peek() {
|
while let Some(ch) = chars.peek() {
|
||||||
|
|
||||||
match ch {
|
match ch {
|
||||||
'}' if is_braced => {
|
'}' if is_braced => {
|
||||||
chars.next();
|
chars.next();
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ pub type OptSet = Arc<[Opt]>;
|
|||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
pub enum Opt {
|
pub enum Opt {
|
||||||
Long(String),
|
Long(String),
|
||||||
LongWithArg(String,String),
|
LongWithArg(String, String),
|
||||||
Short(char),
|
Short(char),
|
||||||
ShortWithArg(char,String),
|
ShortWithArg(char, String),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct OptSpec {
|
pub struct OptSpec {
|
||||||
@@ -42,7 +42,7 @@ impl Display for Opt {
|
|||||||
Self::Long(opt) => write!(f, "--{}", opt),
|
Self::Long(opt) => write!(f, "--{}", opt),
|
||||||
Self::Short(opt) => write!(f, "-{}", opt),
|
Self::Short(opt) => write!(f, "-{}", opt),
|
||||||
Self::LongWithArg(opt, arg) => write!(f, "--{} {}", opt, arg),
|
Self::LongWithArg(opt, arg) => write!(f, "--{} {}", opt, arg),
|
||||||
Self::ShortWithArg(opt, arg) => write!(f, "-{} {}", opt, arg)
|
Self::ShortWithArg(opt, arg) => write!(f, "-{} {}", opt, arg),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,7 +87,8 @@ pub fn get_opts_from_tokens(tokens: Vec<Tk>, opt_specs: &[OptSpec]) -> (Vec<Tk>,
|
|||||||
for opt_spec in opt_specs {
|
for opt_spec in opt_specs {
|
||||||
if opt_spec.opt == opt {
|
if opt_spec.opt == opt {
|
||||||
if opt_spec.takes_arg {
|
if opt_spec.takes_arg {
|
||||||
let arg = tokens_iter.next()
|
let arg = tokens_iter
|
||||||
|
.next()
|
||||||
.map(|t| t.to_string())
|
.map(|t| t.to_string())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
|||||||
10
src/jobs.rs
10
src/jobs.rs
@@ -2,7 +2,11 @@ use crate::{
|
|||||||
libsh::{
|
libsh::{
|
||||||
error::ShResult,
|
error::ShResult,
|
||||||
term::{Style, Styled},
|
term::{Style, Styled},
|
||||||
}, prelude::*, procio::{IoMode, borrow_fd}, signal::{disable_reaping, enable_reaping}, state::{self, set_status, read_jobs, write_jobs}
|
},
|
||||||
|
prelude::*,
|
||||||
|
procio::{IoMode, borrow_fd},
|
||||||
|
signal::{disable_reaping, enable_reaping},
|
||||||
|
state::{self, read_jobs, set_status, write_jobs},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const SIG_EXIT_OFFSET: i32 = 128;
|
pub const SIG_EXIT_OFFSET: i32 = 128;
|
||||||
@@ -685,7 +689,9 @@ pub fn wait_fg(job: Job) -> ShResult<()> {
|
|||||||
}
|
}
|
||||||
// If job wasn't stopped (moved to bg), clear the fg slot
|
// If job wasn't stopped (moved to bg), clear the fg slot
|
||||||
if !was_stopped {
|
if !was_stopped {
|
||||||
write_jobs(|j| { j.take_fg(); });
|
write_jobs(|j| {
|
||||||
|
j.take_fg();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
take_term()?;
|
take_term()?;
|
||||||
set_status(code);
|
set_status(code);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use termios::{LocalFlags, Termios};
|
use termios::{LocalFlags, Termios};
|
||||||
|
|
||||||
use crate::{prelude::*};
|
use crate::prelude::*;
|
||||||
///
|
///
|
||||||
/// The previous state of the terminal options.
|
/// The previous state of the terminal options.
|
||||||
///
|
///
|
||||||
@@ -33,12 +33,14 @@ pub(crate) static mut SAVED_TERMIOS: Option<Option<Termios>> = None;
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct TermiosGuard {
|
pub struct TermiosGuard {
|
||||||
saved_termios: Option<Termios>
|
saved_termios: Option<Termios>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TermiosGuard {
|
impl TermiosGuard {
|
||||||
pub fn new(new_termios: Termios) -> Self {
|
pub fn new(new_termios: Termios) -> Self {
|
||||||
let mut new = Self { saved_termios: None };
|
let mut new = Self {
|
||||||
|
saved_termios: None,
|
||||||
|
};
|
||||||
|
|
||||||
if isatty(std::io::stdin().as_raw_fd()).unwrap() {
|
if isatty(std::io::stdin().as_raw_fd()).unwrap() {
|
||||||
let current_termios = termios::tcgetattr(std::io::stdin()).unwrap();
|
let current_termios = termios::tcgetattr(std::io::stdin()).unwrap();
|
||||||
@@ -48,7 +50,8 @@ impl TermiosGuard {
|
|||||||
std::io::stdin(),
|
std::io::stdin(),
|
||||||
nix::sys::termios::SetArg::TCSANOW,
|
nix::sys::termios::SetArg::TCSANOW,
|
||||||
&new_termios,
|
&new_termios,
|
||||||
).unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
new
|
new
|
||||||
@@ -66,11 +69,7 @@ impl Default for TermiosGuard {
|
|||||||
impl Drop for TermiosGuard {
|
impl Drop for TermiosGuard {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if let Some(saved) = &self.saved_termios {
|
if let Some(saved) = &self.saved_termios {
|
||||||
termios::tcsetattr(
|
termios::tcsetattr(std::io::stdin(), nix::sys::termios::SetArg::TCSANOW, saved).unwrap();
|
||||||
std::io::stdin(),
|
|
||||||
nix::sys::termios::SetArg::TCSANOW,
|
|
||||||
saved,
|
|
||||||
).unwrap();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,8 +83,7 @@ impl TkVecUtils<Tk> for Vec<Tk> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn debug_tokens(&self) {
|
fn debug_tokens(&self) {
|
||||||
for token in self {
|
for token in self {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
36
src/main.rs
36
src/main.rs
@@ -22,10 +22,10 @@ use std::os::fd::BorrowedFd;
|
|||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
|
use nix::errno::Errno;
|
||||||
use nix::libc::STDIN_FILENO;
|
use nix::libc::STDIN_FILENO;
|
||||||
use nix::poll::{PollFd, PollFlags, PollTimeout, poll};
|
use nix::poll::{PollFd, PollFlags, PollTimeout, poll};
|
||||||
use nix::unistd::read;
|
use nix::unistd::read;
|
||||||
use nix::errno::Errno;
|
|
||||||
|
|
||||||
use crate::builtin::trap::TrapTarget;
|
use crate::builtin::trap::TrapTarget;
|
||||||
use crate::libsh::error::{ShErr, ShErrKind, ShResult};
|
use crate::libsh::error::{ShErr, ShErrKind, ShResult};
|
||||||
@@ -87,7 +87,8 @@ fn main() -> ExitCode {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Exit))
|
if let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Exit))
|
||||||
&& let Err(e) = exec_input(trap, None, false) {
|
&& let Err(e) = exec_input(trap, None, false)
|
||||||
|
{
|
||||||
eprintln!("fern: error running EXIT trap: {e}");
|
eprintln!("fern: error running EXIT trap: {e}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,15 +100,24 @@ fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) -> ShResult<()> {
|
|||||||
if !path.is_file() {
|
if !path.is_file() {
|
||||||
eprintln!("fern: Failed to open input file: {}", path.display());
|
eprintln!("fern: Failed to open input file: {}", path.display());
|
||||||
QUIT_CODE.store(1, Ordering::SeqCst);
|
QUIT_CODE.store(1, Ordering::SeqCst);
|
||||||
return Err(ShErr::simple(ShErrKind::CleanExit(1), "input file not found"));
|
return Err(ShErr::simple(
|
||||||
|
ShErrKind::CleanExit(1),
|
||||||
|
"input file not found",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let Ok(input) = fs::read_to_string(path) else {
|
let Ok(input) = fs::read_to_string(path) else {
|
||||||
eprintln!("fern: Failed to read input file: {}", path.display());
|
eprintln!("fern: Failed to read input file: {}", path.display());
|
||||||
QUIT_CODE.store(1, Ordering::SeqCst);
|
QUIT_CODE.store(1, Ordering::SeqCst);
|
||||||
return Err(ShErr::simple(ShErrKind::CleanExit(1), "failed to read input file"));
|
return Err(ShErr::simple(
|
||||||
|
ShErrKind::CleanExit(1),
|
||||||
|
"failed to read input file",
|
||||||
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
write_vars(|v| v.cur_scope_mut().bpush_arg(path.to_string_lossy().to_string()));
|
write_vars(|v| {
|
||||||
|
v.cur_scope_mut()
|
||||||
|
.bpush_arg(path.to_string_lossy().to_string())
|
||||||
|
});
|
||||||
for arg in args {
|
for arg in args {
|
||||||
write_vars(|v| v.cur_scope_mut().bpush_arg(arg))
|
write_vars(|v| v.cur_scope_mut().bpush_arg(arg))
|
||||||
}
|
}
|
||||||
@@ -129,7 +139,10 @@ fn fern_interactive() -> ShResult<()> {
|
|||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to initialize readline: {e}");
|
eprintln!("Failed to initialize readline: {e}");
|
||||||
QUIT_CODE.store(1, Ordering::SeqCst);
|
QUIT_CODE.store(1, Ordering::SeqCst);
|
||||||
return Err(ShErr::simple(ShErrKind::CleanExit(1), "readline initialization failed"));
|
return Err(ShErr::simple(
|
||||||
|
ShErrKind::CleanExit(1),
|
||||||
|
"readline initialization failed",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -173,7 +186,10 @@ fn fern_interactive() -> ShResult<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if stdin has data
|
// Check if stdin has data
|
||||||
if fds[0].revents().is_some_and(|r| r.contains(PollFlags::POLLIN)) {
|
if fds[0]
|
||||||
|
.revents()
|
||||||
|
.is_some_and(|r| r.contains(PollFlags::POLLIN))
|
||||||
|
{
|
||||||
let mut buffer = [0u8; 1024];
|
let mut buffer = [0u8; 1024];
|
||||||
match read(STDIN_FILENO, &mut buffer) {
|
match read(STDIN_FILENO, &mut buffer) {
|
||||||
Ok(0) => {
|
Ok(0) => {
|
||||||
@@ -225,15 +241,13 @@ fn fern_interactive() -> ShResult<()> {
|
|||||||
Ok(ReadlineEvent::Pending) => {
|
Ok(ReadlineEvent::Pending) => {
|
||||||
// No complete input yet, keep polling
|
// No complete input yet, keep polling
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => match e.kind() {
|
||||||
match e.kind() {
|
|
||||||
ShErrKind::CleanExit(code) => {
|
ShErrKind::CleanExit(code) => {
|
||||||
QUIT_CODE.store(*code, Ordering::SeqCst);
|
QUIT_CODE.store(*code, Ordering::SeqCst);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
_ => eprintln!("{e}"),
|
_ => eprintln!("{e}"),
|
||||||
}
|
},
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,22 +2,33 @@ use std::collections::{HashSet, VecDeque};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
builtin::{
|
builtin::{
|
||||||
alias::{alias, unalias}, cd::cd, echo::echo, export::export, flowctl::flowctl, jobctl::{JobBehavior, continue_job, jobs}, pwd::pwd, read::read_builtin, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, zoltraak::zoltraak
|
alias::{alias, unalias},
|
||||||
|
cd::cd,
|
||||||
|
echo::echo,
|
||||||
|
export::export,
|
||||||
|
flowctl::flowctl,
|
||||||
|
jobctl::{JobBehavior, continue_job, jobs},
|
||||||
|
pwd::pwd,
|
||||||
|
read::read_builtin,
|
||||||
|
shift::shift,
|
||||||
|
shopt::shopt,
|
||||||
|
source::source,
|
||||||
|
test::double_bracket_test,
|
||||||
|
trap::{TrapTarget, trap},
|
||||||
|
zoltraak::zoltraak,
|
||||||
},
|
},
|
||||||
expand::expand_aliases,
|
expand::expand_aliases,
|
||||||
jobs::{ChildProc, JobStack, dispatch_job},
|
jobs::{ChildProc, JobStack, dispatch_job},
|
||||||
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
|
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
procio::{IoMode, IoStack},
|
procio::{IoMode, IoStack},
|
||||||
state::{
|
state::{self, ShFunc, VarFlags, read_logic, write_logic, write_vars},
|
||||||
self, ShFunc, VarFlags, read_logic, write_logic, write_vars
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
lex::{Span, Tk, TkFlags, KEYWORDS},
|
|
||||||
AssignKind, CaseNode, CondNode, ConjunctNode, ConjunctOp, LoopKind, NdFlags, NdRule, Node,
|
AssignKind, CaseNode, CondNode, ConjunctNode, ConjunctOp, LoopKind, NdFlags, NdRule, Node,
|
||||||
ParsedSrc, Redir, RedirType,
|
ParsedSrc, Redir, RedirType,
|
||||||
|
lex::{KEYWORDS, Span, Tk, TkFlags},
|
||||||
};
|
};
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
@@ -26,9 +37,8 @@ thread_local! {
|
|||||||
|
|
||||||
pub struct ScopeGuard;
|
pub struct ScopeGuard;
|
||||||
|
|
||||||
|
|
||||||
impl ScopeGuard {
|
impl ScopeGuard {
|
||||||
pub fn exclusive_scope(args: Option<Vec<(String,Span)>>) -> Self {
|
pub fn exclusive_scope(args: Option<Vec<(String, Span)>>) -> Self {
|
||||||
let argv = args.map(|a| a.into_iter().map(|(s, _)| s).collect::<Vec<_>>());
|
let argv = args.map(|a| a.into_iter().map(|(s, _)| s).collect::<Vec<_>>());
|
||||||
write_vars(|v| v.descend(argv));
|
write_vars(|v| v.descend(argv));
|
||||||
Self
|
Self
|
||||||
@@ -50,7 +60,7 @@ impl Drop for ScopeGuard {
|
|||||||
/// such as 'VAR=value <command> <args>'
|
/// such as 'VAR=value <command> <args>'
|
||||||
/// or for-loop variables
|
/// or for-loop variables
|
||||||
struct VarCtxGuard {
|
struct VarCtxGuard {
|
||||||
vars: HashSet<String>
|
vars: HashSet<String>,
|
||||||
}
|
}
|
||||||
impl VarCtxGuard {
|
impl VarCtxGuard {
|
||||||
fn new(vars: HashSet<String>) -> Self {
|
fn new(vars: HashSet<String>) -> Self {
|
||||||
@@ -129,7 +139,8 @@ pub fn exec_input(input: String, io_stack: Option<IoStack>, interactive: bool) -
|
|||||||
let result = dispatcher.begin_dispatch();
|
let result = dispatcher.begin_dispatch();
|
||||||
|
|
||||||
if state::get_status() != 0
|
if state::get_status() != 0
|
||||||
&& let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Error)) {
|
&& let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Error))
|
||||||
|
{
|
||||||
let saved_status = state::get_status();
|
let saved_status = state::get_status();
|
||||||
exec_input(trap, None, false)?;
|
exec_input(trap, None, false)?;
|
||||||
state::set_status(saved_status);
|
state::set_status(saved_status);
|
||||||
@@ -188,9 +199,12 @@ impl Dispatcher {
|
|||||||
self.exec_builtin(node)
|
self.exec_builtin(node)
|
||||||
} else if is_subsh(node.get_command().cloned()) {
|
} else if is_subsh(node.get_command().cloned()) {
|
||||||
self.exec_subsh(node)
|
self.exec_subsh(node)
|
||||||
} else if crate::state::read_shopts(|s| s.core.autocd) && Path::new(cmd.span.as_str()).is_dir() {
|
} else if crate::state::read_shopts(|s| s.core.autocd) && Path::new(cmd.span.as_str()).is_dir()
|
||||||
|
{
|
||||||
let dir = cmd.span.as_str().to_string();
|
let dir = cmd.span.as_str().to_string();
|
||||||
let stack = IoStack { stack: self.io_stack.clone() };
|
let stack = IoStack {
|
||||||
|
stack: self.io_stack.clone(),
|
||||||
|
};
|
||||||
exec_input(format!("cd {dir}"), Some(stack), self.interactive)
|
exec_input(format!("cd {dir}"), Some(stack), self.interactive)
|
||||||
} else {
|
} else {
|
||||||
self.exec_cmd(node)
|
self.exec_cmd(node)
|
||||||
@@ -340,9 +354,7 @@ impl Dispatcher {
|
|||||||
unreachable!()
|
unreachable!()
|
||||||
};
|
};
|
||||||
self.io_stack.append_to_frame(brc_grp.redirs);
|
self.io_stack.append_to_frame(brc_grp.redirs);
|
||||||
let _guard = self.io_stack
|
let _guard = self.io_stack.pop_frame().redirect()?;
|
||||||
.pop_frame()
|
|
||||||
.redirect()?;
|
|
||||||
|
|
||||||
for node in body {
|
for node in body {
|
||||||
let blame = node.get_span();
|
let blame = node.get_span();
|
||||||
@@ -361,9 +373,7 @@ impl Dispatcher {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.io_stack.append_to_frame(case_stmt.redirs);
|
self.io_stack.append_to_frame(case_stmt.redirs);
|
||||||
let _guard = self.io_stack
|
let _guard = self.io_stack.pop_frame().redirect()?;
|
||||||
.pop_frame()
|
|
||||||
.redirect()?;
|
|
||||||
|
|
||||||
let exp_pattern = pattern.clone().expand()?;
|
let exp_pattern = pattern.clone().expand()?;
|
||||||
let pattern_raw = exp_pattern
|
let pattern_raw = exp_pattern
|
||||||
@@ -402,9 +412,7 @@ impl Dispatcher {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.io_stack.append_to_frame(loop_stmt.redirs);
|
self.io_stack.append_to_frame(loop_stmt.redirs);
|
||||||
let _guard = self.io_stack
|
let _guard = self.io_stack.pop_frame().redirect()?;
|
||||||
.pop_frame()
|
|
||||||
.redirect()?;
|
|
||||||
|
|
||||||
let CondNode { cond, body } = cond_node;
|
let CondNode { cond, body } = cond_node;
|
||||||
'outer: loop {
|
'outer: loop {
|
||||||
@@ -445,32 +453,31 @@ impl Dispatcher {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let to_expanded_strings = |tks: Vec<Tk>| -> ShResult<Vec<String>> {
|
let to_expanded_strings = |tks: Vec<Tk>| -> ShResult<Vec<String>> {
|
||||||
Ok(tks.into_iter()
|
Ok(
|
||||||
|
tks
|
||||||
|
.into_iter()
|
||||||
.map(|tk| tk.expand().map(|tk| tk.get_words()))
|
.map(|tk| tk.expand().map(|tk| tk.get_words()))
|
||||||
.collect::<ShResult<Vec<Vec<String>>>>()?
|
.collect::<ShResult<Vec<Vec<String>>>>()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
.collect::<Vec<_>>())
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Expand all array variables
|
// Expand all array variables
|
||||||
let arr: Vec<String> = to_expanded_strings(arr)?;
|
let arr: Vec<String> = to_expanded_strings(arr)?;
|
||||||
let vars: Vec<String> = to_expanded_strings(vars)?;
|
let vars: Vec<String> = to_expanded_strings(vars)?;
|
||||||
|
|
||||||
let mut for_guard = VarCtxGuard::new(
|
let mut for_guard = VarCtxGuard::new(vars.iter().map(|v| v.to_string()).collect());
|
||||||
vars.iter().map(|v| v.to_string()).collect()
|
|
||||||
);
|
|
||||||
|
|
||||||
self.io_stack.append_to_frame(for_stmt.redirs);
|
self.io_stack.append_to_frame(for_stmt.redirs);
|
||||||
let _guard = self.io_stack
|
let _guard = self.io_stack.pop_frame().redirect()?;
|
||||||
.pop_frame()
|
|
||||||
.redirect()?;
|
|
||||||
|
|
||||||
'outer: for chunk in arr.chunks(vars.len()) {
|
'outer: for chunk in arr.chunks(vars.len()) {
|
||||||
let empty = String::new();
|
let empty = String::new();
|
||||||
let chunk_iter = vars.iter().zip(
|
let chunk_iter = vars
|
||||||
chunk.iter().chain(std::iter::repeat(&empty)),
|
.iter()
|
||||||
);
|
.zip(chunk.iter().chain(std::iter::repeat(&empty)));
|
||||||
|
|
||||||
for (var, val) in chunk_iter {
|
for (var, val) in chunk_iter {
|
||||||
write_vars(|v| v.set_var(&var.to_string(), &val.to_string(), VarFlags::NONE));
|
write_vars(|v| v.set_var(&var.to_string(), &val.to_string(), VarFlags::NONE));
|
||||||
@@ -506,9 +513,7 @@ impl Dispatcher {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.io_stack.append_to_frame(if_stmt.redirs);
|
self.io_stack.append_to_frame(if_stmt.redirs);
|
||||||
let _guard = self.io_stack
|
let _guard = self.io_stack.pop_frame().redirect()?;
|
||||||
.pop_frame()
|
|
||||||
.redirect()?;
|
|
||||||
|
|
||||||
let mut matched = false;
|
let mut matched = false;
|
||||||
for node in cond_nodes {
|
for node in cond_nodes {
|
||||||
@@ -562,11 +567,7 @@ impl Dispatcher {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn exec_builtin(&mut self, mut cmd: Node) -> ShResult<()> {
|
fn exec_builtin(&mut self, mut cmd: Node) -> ShResult<()> {
|
||||||
let NdRule::Command {
|
let NdRule::Command { assignments, argv } = &mut cmd.class else {
|
||||||
assignments,
|
|
||||||
argv,
|
|
||||||
} = &mut cmd.class
|
|
||||||
else {
|
|
||||||
unreachable!()
|
unreachable!()
|
||||||
};
|
};
|
||||||
let env_vars = self.set_assignments(mem::take(assignments), AssignBehavior::Export)?;
|
let env_vars = self.set_assignments(mem::take(assignments), AssignBehavior::Export)?;
|
||||||
@@ -646,9 +647,7 @@ impl Dispatcher {
|
|||||||
|
|
||||||
let exec_args = ExecArgs::new(argv)?;
|
let exec_args = ExecArgs::new(argv)?;
|
||||||
|
|
||||||
let _guard = self.io_stack
|
let _guard = self.io_stack.pop_frame().redirect()?;
|
||||||
.pop_frame()
|
|
||||||
.redirect()?;
|
|
||||||
|
|
||||||
let job = self.job_stack.curr_job_mut().unwrap();
|
let job = self.job_stack.curr_job_mut().unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -324,7 +324,8 @@ impl LexStream {
|
|||||||
let can_be_subshell = chars.peek() == Some(&'(');
|
let can_be_subshell = chars.peek() == Some(&'(');
|
||||||
|
|
||||||
if self.flags.contains(LexFlags::IN_CASE)
|
if self.flags.contains(LexFlags::IN_CASE)
|
||||||
&& let Some(count) = case_pat_lookahead(chars.clone()) {
|
&& let Some(count) = case_pat_lookahead(chars.clone())
|
||||||
|
{
|
||||||
pos += count;
|
pos += count;
|
||||||
let casepat_tk = self.get_token(self.cursor..pos, TkRule::CasePattern);
|
let casepat_tk = self.get_token(self.cursor..pos, TkRule::CasePattern);
|
||||||
self.cursor = pos;
|
self.cursor = pos;
|
||||||
@@ -740,7 +741,10 @@ impl Iterator for LexStream {
|
|||||||
}
|
}
|
||||||
self.get_token(ch_idx..self.cursor, TkRule::Sep)
|
self.get_token(ch_idx..self.cursor, TkRule::Sep)
|
||||||
}
|
}
|
||||||
'#' if !self.flags.contains(LexFlags::INTERACTIVE) || crate::state::read_shopts(|s| s.core.interactive_comments) => {
|
'#'
|
||||||
|
if !self.flags.contains(LexFlags::INTERACTIVE)
|
||||||
|
|| crate::state::read_shopts(|s| s.core.interactive_comments) =>
|
||||||
|
{
|
||||||
let ch_idx = self.cursor;
|
let ch_idx = self.cursor;
|
||||||
self.cursor += 1;
|
self.cursor += 1;
|
||||||
|
|
||||||
|
|||||||
@@ -1335,7 +1335,12 @@ impl ParseStream {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
match tk.class {
|
match tk.class {
|
||||||
TkRule::EOI | TkRule::Pipe | TkRule::And | TkRule::BraceGrpEnd | TkRule::Or | TkRule::Bg => break,
|
TkRule::EOI
|
||||||
|
| TkRule::Pipe
|
||||||
|
| TkRule::And
|
||||||
|
| TkRule::BraceGrpEnd
|
||||||
|
| TkRule::Or
|
||||||
|
| TkRule::Bg => break,
|
||||||
TkRule::Sep => {
|
TkRule::Sep => {
|
||||||
node_tks.push(tk.clone());
|
node_tks.push(tk.clone());
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -19,17 +19,17 @@ pub use std::os::unix::io::{AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd,
|
|||||||
pub use bitflags::bitflags;
|
pub use bitflags::bitflags;
|
||||||
pub use nix::{
|
pub use nix::{
|
||||||
errno::Errno,
|
errno::Errno,
|
||||||
fcntl::{open, OFlag},
|
fcntl::{OFlag, open},
|
||||||
libc::{self, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO},
|
libc::{self, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO},
|
||||||
sys::{
|
sys::{
|
||||||
signal::{self, kill, killpg, pthread_sigmask, signal, SigHandler, SigSet, SigmaskHow, Signal},
|
signal::{self, SigHandler, SigSet, SigmaskHow, Signal, kill, killpg, pthread_sigmask, signal},
|
||||||
stat::Mode,
|
stat::Mode,
|
||||||
termios::{self},
|
termios::{self},
|
||||||
wait::{waitpid, WaitPidFlag as WtFlag, WaitStatus as WtStat},
|
wait::{WaitPidFlag as WtFlag, WaitStatus as WtStat, waitpid},
|
||||||
},
|
},
|
||||||
unistd::{
|
unistd::{
|
||||||
close, dup, dup2, execvpe, fork, getpgid, getpgrp, isatty, pipe, read, setpgid, tcgetpgrp,
|
ForkResult, Pid, close, dup, dup2, execvpe, fork, getpgid, getpgrp, isatty, pipe, read,
|
||||||
tcsetpgrp, write, ForkResult, Pid,
|
setpgid, tcgetpgrp, tcsetpgrp, write,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,13 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
expand::Expander, libsh::{
|
expand::Expander,
|
||||||
|
libsh::{
|
||||||
error::{ShErr, ShErrKind, ShResult},
|
error::{ShErr, ShErrKind, ShResult},
|
||||||
utils::RedirVecUtils,
|
utils::RedirVecUtils,
|
||||||
}, parse::{Redir, RedirType, get_redir_file}, prelude::*
|
},
|
||||||
|
parse::{Redir, RedirType, get_redir_file},
|
||||||
|
prelude::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Credit to fish-shell for many of the implementation ideas present in this
|
// Credit to fish-shell for many of the implementation ideas present in this
|
||||||
@@ -70,15 +73,10 @@ impl IoMode {
|
|||||||
}
|
}
|
||||||
pub fn open_file(mut self) -> ShResult<Self> {
|
pub fn open_file(mut self) -> ShResult<Self> {
|
||||||
if let IoMode::File { tgt_fd, path, mode } = self {
|
if let IoMode::File { tgt_fd, path, mode } = self {
|
||||||
let path_raw = path
|
let path_raw = path.as_os_str().to_str().unwrap_or_default().to_string();
|
||||||
.as_os_str()
|
|
||||||
.to_str()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let expanded_path = Expander::from_raw(&path_raw)?
|
let expanded_path = Expander::from_raw(&path_raw)?.expand()?.join(" "); // should just be one string, will have to find some way to handle a return of
|
||||||
.expand()?
|
// multiple
|
||||||
.join(" "); // should just be one string, will have to find some way to handle a return of multiple
|
|
||||||
|
|
||||||
let expanded_pathbuf = PathBuf::from(expanded_path);
|
let expanded_pathbuf = PathBuf::from(expanded_path);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
pub mod readline;
|
pub mod readline;
|
||||||
pub mod statusline;
|
pub mod statusline;
|
||||||
|
|
||||||
|
|
||||||
use crate::{expand::expand_prompt, libsh::error::ShResult, prelude::*};
|
use crate::{expand::expand_prompt, libsh::error::ShResult, prelude::*};
|
||||||
|
|
||||||
/// Initialize the line editor
|
/// Initialize the line editor
|
||||||
|
|||||||
@@ -1,20 +1,25 @@
|
|||||||
use std::{env, os::unix::fs::PermissionsExt, path::PathBuf, sync::Arc};
|
use std::{env, os::unix::fs::PermissionsExt, path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
use crate::{builtin::BUILTINS, libsh::error::{ShErr, ShErrKind, ShResult}, parse::lex::{self, LexFlags, Tk, TkFlags}, prompt::readline::{Marker, annotate_input, annotate_input_recursive, get_insertions, markers::{self, is_marker}}, state::{read_logic, read_vars}};
|
use crate::{
|
||||||
|
builtin::BUILTINS,
|
||||||
|
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||||
|
parse::lex::{self, LexFlags, Tk, TkFlags},
|
||||||
|
prompt::readline::{
|
||||||
|
Marker, annotate_input, annotate_input_recursive, get_insertions,
|
||||||
|
markers::{self, is_marker},
|
||||||
|
},
|
||||||
|
state::{read_logic, read_vars},
|
||||||
|
};
|
||||||
|
|
||||||
pub enum CompCtx {
|
pub enum CompCtx {
|
||||||
CmdName,
|
CmdName,
|
||||||
FileName
|
FileName,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum CompResult {
|
pub enum CompResult {
|
||||||
NoMatch,
|
NoMatch,
|
||||||
Single {
|
Single { result: String },
|
||||||
result: String
|
Many { candidates: Vec<String> },
|
||||||
},
|
|
||||||
Many {
|
|
||||||
candidates: Vec<String>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CompResult {
|
impl CompResult {
|
||||||
@@ -22,7 +27,9 @@ impl CompResult {
|
|||||||
if candidates.is_empty() {
|
if candidates.is_empty() {
|
||||||
Self::NoMatch
|
Self::NoMatch
|
||||||
} else if candidates.len() == 1 {
|
} else if candidates.len() == 1 {
|
||||||
Self::Single { result: candidates[0].clone() }
|
Self::Single {
|
||||||
|
result: candidates[0].clone(),
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Self::Many { candidates }
|
Self::Many { candidates }
|
||||||
}
|
}
|
||||||
@@ -55,7 +62,6 @@ impl Completer {
|
|||||||
|
|
||||||
pub fn get_completion_context(&self, line: &str, cursor_pos: usize) -> (Vec<Marker>, usize) {
|
pub fn get_completion_context(&self, line: &str, cursor_pos: usize) -> (Vec<Marker>, usize) {
|
||||||
let annotated = annotate_input_recursive(line);
|
let annotated = annotate_input_recursive(line);
|
||||||
log::debug!("Annotated input for completion context: {:?}", annotated);
|
|
||||||
let mut ctx = vec![markers::NULL];
|
let mut ctx = vec![markers::NULL];
|
||||||
let mut last_priority = 0;
|
let mut last_priority = 0;
|
||||||
let mut ctx_start = 0;
|
let mut ctx_start = 0;
|
||||||
@@ -63,10 +69,8 @@ impl Completer {
|
|||||||
|
|
||||||
for ch in annotated.chars() {
|
for ch in annotated.chars() {
|
||||||
match ch {
|
match ch {
|
||||||
_ if is_marker(ch) => {
|
_ if is_marker(ch) => match ch {
|
||||||
match ch {
|
|
||||||
markers::COMMAND | markers::BUILTIN => {
|
markers::COMMAND | markers::BUILTIN => {
|
||||||
log::debug!("Found command marker at position {}", pos);
|
|
||||||
if last_priority < 2 {
|
if last_priority < 2 {
|
||||||
if last_priority > 0 {
|
if last_priority > 0 {
|
||||||
ctx.pop();
|
ctx.pop();
|
||||||
@@ -77,7 +81,6 @@ impl Completer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
markers::VAR_SUB => {
|
markers::VAR_SUB => {
|
||||||
log::debug!("Found variable substitution marker at position {}", pos);
|
|
||||||
if last_priority < 3 {
|
if last_priority < 3 {
|
||||||
if last_priority > 0 {
|
if last_priority > 0 {
|
||||||
ctx.pop();
|
ctx.pop();
|
||||||
@@ -88,15 +91,13 @@ impl Completer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
markers::ARG | markers::ASSIGNMENT => {
|
markers::ARG | markers::ASSIGNMENT => {
|
||||||
log::debug!("Found argument/assignment marker at position {}", pos);
|
|
||||||
if last_priority < 1 {
|
if last_priority < 1 {
|
||||||
ctx_start = pos;
|
ctx_start = pos;
|
||||||
ctx.push(markers::ARG);
|
ctx.push(markers::ARG);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
},
|
||||||
}
|
|
||||||
_ => {
|
_ => {
|
||||||
last_priority = 0; // reset priority on normal characters
|
last_priority = 0; // reset priority on normal characters
|
||||||
pos += 1; // we hit a normal character, advance our position
|
pos += 1; // we hit a normal character, advance our position
|
||||||
@@ -118,7 +119,12 @@ impl Completer {
|
|||||||
self.active = false;
|
self.active = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn complete(&mut self, line: String, cursor_pos: usize, direction: i32) -> ShResult<Option<String>> {
|
pub fn complete(
|
||||||
|
&mut self,
|
||||||
|
line: String,
|
||||||
|
cursor_pos: usize,
|
||||||
|
direction: i32,
|
||||||
|
) -> ShResult<Option<String>> {
|
||||||
if self.active {
|
if self.active {
|
||||||
Ok(Some(self.cycle_completion(direction)))
|
Ok(Some(self.cycle_completion(direction)))
|
||||||
} else {
|
} else {
|
||||||
@@ -160,12 +166,11 @@ impl Completer {
|
|||||||
|
|
||||||
Ok(Some(self.get_completed_line()))
|
Ok(Some(self.get_completed_line()))
|
||||||
}
|
}
|
||||||
CompResult::NoMatch => Ok(None)
|
CompResult::NoMatch => Ok(None),
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn extract_var_name(text: &str) -> Option<(String,usize,usize)> {
|
pub fn extract_var_name(text: &str) -> Option<(String, usize, usize)> {
|
||||||
let mut chars = text.chars().peekable();
|
let mut chars = text.chars().peekable();
|
||||||
let mut name = String::new();
|
let mut name = String::new();
|
||||||
let mut reading_name = false;
|
let mut reading_name = false;
|
||||||
@@ -220,19 +225,24 @@ impl Completer {
|
|||||||
|
|
||||||
let selected = &self.candidates[self.selected_idx];
|
let selected = &self.candidates[self.selected_idx];
|
||||||
let (start, end) = self.token_span;
|
let (start, end) = self.token_span;
|
||||||
format!("{}{}{}", &self.original_input[..start], selected, &self.original_input[end..])
|
format!(
|
||||||
|
"{}{}{}",
|
||||||
|
&self.original_input[..start],
|
||||||
|
selected,
|
||||||
|
&self.original_input[end..]
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_candidates(&mut self, line: String, cursor_pos: usize) -> ShResult<CompResult> {
|
pub fn get_candidates(&mut self, line: String, cursor_pos: usize) -> ShResult<CompResult> {
|
||||||
let source = Arc::new(line.clone());
|
let source = Arc::new(line.clone());
|
||||||
let tokens = lex::LexStream::new(source, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>()?;
|
let tokens =
|
||||||
|
lex::LexStream::new(source, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>()?;
|
||||||
|
|
||||||
let Some(mut cur_token) = tokens.into_iter().find(|tk| {
|
let Some(mut cur_token) = tokens.into_iter().find(|tk| {
|
||||||
let start = tk.span.start;
|
let start = tk.span.start;
|
||||||
let end = tk.span.end;
|
let end = tk.span.end;
|
||||||
(start..=end).contains(&cursor_pos)
|
(start..=end).contains(&cursor_pos)
|
||||||
}) else {
|
}) else {
|
||||||
log::debug!("No token found at cursor position");
|
|
||||||
let candidates = Self::complete_filename("./"); // Default to filename completion if no token is found
|
let candidates = Self::complete_filename("./"); // Default to filename completion if no token is found
|
||||||
let end_pos = line.len();
|
let end_pos = line.len();
|
||||||
self.token_span = (end_pos, end_pos);
|
self.token_span = (end_pos, end_pos);
|
||||||
@@ -241,11 +251,12 @@ impl Completer {
|
|||||||
|
|
||||||
self.token_span = (cur_token.span.start, cur_token.span.end);
|
self.token_span = (cur_token.span.start, cur_token.span.end);
|
||||||
|
|
||||||
|
|
||||||
// Look for marker at the START of what we're completing, not at cursor
|
// Look for marker at the START of what we're completing, not at cursor
|
||||||
let (mut ctx, token_start) = self.get_completion_context(&line, cursor_pos);
|
let (mut ctx, token_start) = self.get_completion_context(&line, cursor_pos);
|
||||||
self.token_span.0 = token_start; // Update start of token span based on context
|
self.token_span.0 = token_start; // Update start of token span based on context
|
||||||
cur_token.span.set_range(self.token_span.0..self.token_span.1); // Update token span to reflect context
|
cur_token
|
||||||
|
.span
|
||||||
|
.set_range(self.token_span.0..self.token_span.1); // Update token span to reflect context
|
||||||
|
|
||||||
// If token contains '=', only complete after the '='
|
// If token contains '=', only complete after the '='
|
||||||
let token_str = cur_token.span.as_str();
|
let token_str = cur_token.span.as_str();
|
||||||
@@ -256,13 +267,13 @@ impl Completer {
|
|||||||
|
|
||||||
if ctx.last().is_some_and(|m| *m == markers::VAR_SUB) {
|
if ctx.last().is_some_and(|m| *m == markers::VAR_SUB) {
|
||||||
let var_sub = &cur_token.as_str();
|
let var_sub = &cur_token.as_str();
|
||||||
if let Some((var_name,start,end)) = Self::extract_var_name(var_sub) {
|
if let Some((var_name, start, end)) = Self::extract_var_name(var_sub) {
|
||||||
log::debug!("Extracted variable name for completion: {}", var_name);
|
|
||||||
if read_vars(|v| v.get_var(&var_name)).is_empty() {
|
if read_vars(|v| v.get_var(&var_name)).is_empty() {
|
||||||
// if we are here, we have a variable substitution that isn't complete
|
// if we are here, we have a variable substitution that isn't complete
|
||||||
// so let's try to complete it
|
// so let's try to complete it
|
||||||
let ret: ShResult<CompResult> = read_vars(|v| {
|
let ret: ShResult<CompResult> = read_vars(|v| {
|
||||||
let var_matches = v.flatten_vars()
|
let var_matches = v
|
||||||
|
.flatten_vars()
|
||||||
.keys()
|
.keys()
|
||||||
.filter(|k| k.starts_with(&var_name) && *k != &var_name)
|
.filter(|k| k.starts_with(&var_name) && *k != &var_name)
|
||||||
.map(|k| k.to_string())
|
.map(|k| k.to_string())
|
||||||
@@ -272,7 +283,9 @@ impl Completer {
|
|||||||
let name_start = cur_token.span.start + start;
|
let name_start = cur_token.span.start + start;
|
||||||
let name_end = cur_token.span.start + end;
|
let name_end = cur_token.span.start + end;
|
||||||
self.token_span = (name_start, name_end);
|
self.token_span = (name_start, name_end);
|
||||||
cur_token.span.set_range(self.token_span.0..self.token_span.1);
|
cur_token
|
||||||
|
.span
|
||||||
|
.set_range(self.token_span.0..self.token_span.1);
|
||||||
Ok(CompResult::from_candidates(var_matches))
|
Ok(CompResult::from_candidates(var_matches))
|
||||||
} else {
|
} else {
|
||||||
Ok(CompResult::NoMatch)
|
Ok(CompResult::NoMatch)
|
||||||
@@ -296,20 +309,12 @@ impl Completer {
|
|||||||
let expanded = expanded_words.join("\\ ");
|
let expanded = expanded_words.join("\\ ");
|
||||||
|
|
||||||
let mut candidates = match ctx.pop() {
|
let mut candidates = match ctx.pop() {
|
||||||
Some(markers::COMMAND) => {
|
Some(markers::COMMAND) => Self::complete_command(&expanded)?,
|
||||||
log::debug!("Completing command: {}", &expanded);
|
Some(markers::ARG) => Self::complete_filename(&expanded),
|
||||||
Self::complete_command(&expanded)?
|
Some(_) => {
|
||||||
}
|
|
||||||
Some(markers::ARG) => {
|
|
||||||
log::debug!("Completing filename: {}", &expanded);
|
|
||||||
Self::complete_filename(&expanded)
|
|
||||||
}
|
|
||||||
Some(m) => {
|
|
||||||
log::warn!("Unknown marker {:?} in completion context", m);
|
|
||||||
return Ok(CompResult::NoMatch);
|
return Ok(CompResult::NoMatch);
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
log::warn!("No marker found in completion context");
|
|
||||||
return Ok(CompResult::NoMatch);
|
return Ok(CompResult::NoMatch);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -321,10 +326,11 @@ impl Completer {
|
|||||||
// /path/to/some_path/file.txt
|
// /path/to/some_path/file.txt
|
||||||
// and instead returns
|
// and instead returns
|
||||||
// $SOME_PATH/file.txt
|
// $SOME_PATH/file.txt
|
||||||
candidates = candidates.into_iter()
|
candidates = candidates
|
||||||
|
.into_iter()
|
||||||
.map(|c| match c.strip_prefix(&expanded) {
|
.map(|c| match c.strip_prefix(&expanded) {
|
||||||
Some(suffix) => format!("{raw_tk}{suffix}"),
|
Some(suffix) => format!("{raw_tk}{suffix}"),
|
||||||
None => c
|
None => c,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -341,16 +347,23 @@ impl Completer {
|
|||||||
let paths = path.split(':').map(PathBuf::from).collect::<Vec<_>>();
|
let paths = path.split(':').map(PathBuf::from).collect::<Vec<_>>();
|
||||||
for path in paths {
|
for path in paths {
|
||||||
// Skip directories that don't exist (common in PATH)
|
// Skip directories that don't exist (common in PATH)
|
||||||
let Ok(entries) = std::fs::read_dir(path) else { continue; };
|
let Ok(entries) = std::fs::read_dir(path) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
let Ok(entry) = entry else { continue; };
|
let Ok(entry) = entry else {
|
||||||
let Ok(meta) = entry.metadata() else { continue; };
|
continue;
|
||||||
|
};
|
||||||
|
let Ok(meta) = entry.metadata() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
let file_name = entry.file_name().to_string_lossy().to_string();
|
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
|
||||||
if meta.is_file()
|
if meta.is_file()
|
||||||
&& (meta.permissions().mode() & 0o111) != 0
|
&& (meta.permissions().mode() & 0o111) != 0
|
||||||
&& file_name.starts_with(start) {
|
&& file_name.starts_with(start)
|
||||||
|
{
|
||||||
candidates.push(file_name);
|
candidates.push(file_name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -390,13 +403,7 @@ impl Completer {
|
|||||||
|
|
||||||
fn complete_filename(start: &str) -> Vec<String> {
|
fn complete_filename(start: &str) -> Vec<String> {
|
||||||
let mut candidates = vec![];
|
let mut candidates = vec![];
|
||||||
|
let has_dotslash = start.starts_with("./");
|
||||||
// If completing after '=', only use the part after it
|
|
||||||
let start = if let Some(eq_pos) = start.rfind('=') {
|
|
||||||
&start[eq_pos + 1..]
|
|
||||||
} else {
|
|
||||||
start
|
|
||||||
};
|
|
||||||
|
|
||||||
// Split path into directory and filename parts
|
// Split path into directory and filename parts
|
||||||
// Use "." if start is empty (e.g., after "foo=")
|
// Use "." if start is empty (e.g., after "foo=")
|
||||||
@@ -405,9 +412,13 @@ impl Completer {
|
|||||||
// Completing inside a directory: "src/" → dir="src/", prefix=""
|
// Completing inside a directory: "src/" → dir="src/", prefix=""
|
||||||
(path, "")
|
(path, "")
|
||||||
} else if let Some(parent) = path.parent()
|
} else if let Some(parent) = path.parent()
|
||||||
&& !parent.as_os_str().is_empty() {
|
&& !parent.as_os_str().is_empty()
|
||||||
|
{
|
||||||
// Has directory component: "src/ma" → dir="src", prefix="ma"
|
// Has directory component: "src/ma" → dir="src", prefix="ma"
|
||||||
(parent.to_path_buf(), path.file_name().unwrap().to_str().unwrap_or(""))
|
(
|
||||||
|
parent.to_path_buf(),
|
||||||
|
path.file_name().unwrap().to_str().unwrap_or(""),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
// No directory: "fil" → dir=".", prefix="fil"
|
// No directory: "fil" → dir=".", prefix="fil"
|
||||||
(PathBuf::from("."), start)
|
(PathBuf::from("."), start)
|
||||||
@@ -435,7 +446,12 @@ impl Completer {
|
|||||||
full_path.push(""); // adds trailing /
|
full_path.push(""); // adds trailing /
|
||||||
}
|
}
|
||||||
|
|
||||||
candidates.push(full_path.to_string_lossy().to_string());
|
let mut path_raw = full_path.to_string_lossy().to_string();
|
||||||
|
if path_raw.starts_with("./") && !has_dotslash {
|
||||||
|
path_raw = path_raw.trim_start_matches("./").to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates.push(path_raw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,3 +459,9 @@ impl Completer {
|
|||||||
candidates
|
candidates
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for Completer {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
use std::{env, os::unix::fs::PermissionsExt, path::{Path, PathBuf}};
|
use std::{
|
||||||
|
env,
|
||||||
|
os::unix::fs::PermissionsExt,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{libsh::term::{Style, StyleSet, Styled}, prompt::readline::{annotate_input, markers}, state::{read_logic, read_shopts}};
|
use crate::{
|
||||||
|
libsh::term::{Style, StyleSet, Styled},
|
||||||
|
prompt::readline::{annotate_input, markers},
|
||||||
|
state::{read_logic, read_shopts},
|
||||||
|
};
|
||||||
|
|
||||||
/// Syntax highlighter for shell input using Unicode marker-based annotation
|
/// Syntax highlighter for shell input using Unicode marker-based annotation
|
||||||
///
|
///
|
||||||
/// The highlighter processes annotated input strings containing invisible Unicode markers
|
/// The highlighter processes annotated input strings containing invisible
|
||||||
/// (U+FDD0-U+FDEF range) that indicate syntax elements. It generates ANSI escape codes
|
/// Unicode markers (U+FDD0-U+FDEF range) that indicate syntax elements. It
|
||||||
/// for terminal display while maintaining a style stack for proper color restoration
|
/// generates ANSI escape codes for terminal display while maintaining a style
|
||||||
/// in nested constructs (e.g., variables inside strings inside command substitutions).
|
/// stack for proper color restoration in nested constructs (e.g., variables
|
||||||
|
/// inside strings inside command substitutions).
|
||||||
pub struct Highlighter {
|
pub struct Highlighter {
|
||||||
input: String,
|
input: String,
|
||||||
output: String,
|
output: String,
|
||||||
@@ -45,29 +54,26 @@ impl Highlighter {
|
|||||||
let mut input_chars = input.chars().peekable();
|
let mut input_chars = input.chars().peekable();
|
||||||
while let Some(ch) = input_chars.next() {
|
while let Some(ch) = input_chars.next() {
|
||||||
match ch {
|
match ch {
|
||||||
markers::STRING_DQ_END |
|
markers::STRING_DQ_END
|
||||||
markers::STRING_SQ_END |
|
| markers::STRING_SQ_END
|
||||||
markers::VAR_SUB_END |
|
| markers::VAR_SUB_END
|
||||||
markers::CMD_SUB_END |
|
| markers::CMD_SUB_END
|
||||||
markers::PROC_SUB_END |
|
| markers::PROC_SUB_END
|
||||||
markers::SUBSH_END => self.pop_style(),
|
| markers::SUBSH_END => self.pop_style(),
|
||||||
|
|
||||||
markers::CMD_SEP |
|
markers::CMD_SEP | markers::RESET => self.clear_styles(),
|
||||||
markers::RESET => self.clear_styles(),
|
|
||||||
|
|
||||||
|
markers::STRING_DQ | markers::STRING_SQ | markers::KEYWORD => {
|
||||||
markers::STRING_DQ |
|
self.push_style(Style::Yellow)
|
||||||
markers::STRING_SQ |
|
}
|
||||||
markers::KEYWORD => self.push_style(Style::Yellow),
|
|
||||||
markers::BUILTIN => self.push_style(Style::Green),
|
markers::BUILTIN => self.push_style(Style::Green),
|
||||||
markers::CASE_PAT => self.push_style(Style::Blue),
|
markers::CASE_PAT => self.push_style(Style::Blue),
|
||||||
markers::ARG => self.push_style(Style::White),
|
|
||||||
markers::COMMENT => self.push_style(Style::BrightBlack),
|
markers::COMMENT => self.push_style(Style::BrightBlack),
|
||||||
|
|
||||||
markers::GLOB => self.push_style(Style::Blue),
|
markers::GLOB => self.push_style(Style::Blue),
|
||||||
|
|
||||||
markers::REDIRECT |
|
markers::REDIRECT | markers::OPERATOR => self.push_style(Style::Magenta | Style::Bold),
|
||||||
markers::OPERATOR => self.push_style(Style::Magenta | Style::Bold),
|
|
||||||
|
|
||||||
markers::ASSIGNMENT => {
|
markers::ASSIGNMENT => {
|
||||||
let mut var_name = String::new();
|
let mut var_name = String::new();
|
||||||
@@ -92,14 +98,34 @@ impl Highlighter {
|
|||||||
self.pop_style();
|
self.pop_style();
|
||||||
}
|
}
|
||||||
|
|
||||||
markers::COMMAND => {
|
markers::ARG => {
|
||||||
let mut cmd_name = String::new();
|
let mut arg = String::new();
|
||||||
while let Some(ch) = input_chars.peek() {
|
let mut chars_clone = input_chars.clone();
|
||||||
if *ch == markers::RESET {
|
while let Some(ch) = chars_clone.next() {
|
||||||
|
if ch == markers::RESET {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
cmd_name.push(*ch);
|
arg.push(ch);
|
||||||
input_chars.next();
|
}
|
||||||
|
|
||||||
|
let style = if Self::is_filename(&arg) {
|
||||||
|
Style::White | Style::Underline
|
||||||
|
} else {
|
||||||
|
Style::White.into()
|
||||||
|
};
|
||||||
|
|
||||||
|
self.push_style(style);
|
||||||
|
self.last_was_reset = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
markers::COMMAND => {
|
||||||
|
let mut cmd_name = String::new();
|
||||||
|
let mut chars_clone = input_chars.clone();
|
||||||
|
while let Some(ch) = chars_clone.next() {
|
||||||
|
if ch == markers::RESET {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cmd_name.push(ch);
|
||||||
}
|
}
|
||||||
let style = if Self::is_valid(&cmd_name) {
|
let style = if Self::is_valid(&cmd_name) {
|
||||||
Style::Green.into()
|
Style::Green.into()
|
||||||
@@ -107,7 +133,6 @@ impl Highlighter {
|
|||||||
Style::Red | Style::Bold
|
Style::Red | Style::Bold
|
||||||
};
|
};
|
||||||
self.push_style(style);
|
self.push_style(style);
|
||||||
self.output.push_str(&cmd_name);
|
|
||||||
self.last_was_reset = false;
|
self.last_was_reset = false;
|
||||||
}
|
}
|
||||||
markers::CMD_SUB | markers::SUBSH | markers::PROC_SUB => {
|
markers::CMD_SUB | markers::SUBSH | markers::PROC_SUB => {
|
||||||
@@ -134,17 +159,19 @@ impl Highlighter {
|
|||||||
markers::CMD_SUB => "$(",
|
markers::CMD_SUB => "$(",
|
||||||
markers::SUBSH => "(",
|
markers::SUBSH => "(",
|
||||||
markers::PROC_SUB => {
|
markers::PROC_SUB => {
|
||||||
if inner.starts_with("<(") { "<(" }
|
if inner.starts_with("<(") {
|
||||||
else if inner.starts_with(">(") { ">(" }
|
"<("
|
||||||
else { "<(" } // fallback
|
} else if inner.starts_with(">(") {
|
||||||
|
">("
|
||||||
|
} else {
|
||||||
|
"<("
|
||||||
|
} // fallback
|
||||||
}
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let inner_content = if incomplete {
|
let inner_content = if incomplete {
|
||||||
inner
|
inner.strip_prefix(prefix).unwrap_or(&inner)
|
||||||
.strip_prefix(prefix)
|
|
||||||
.unwrap_or(&inner)
|
|
||||||
} else {
|
} else {
|
||||||
inner
|
inner
|
||||||
.strip_prefix(prefix)
|
.strip_prefix(prefix)
|
||||||
@@ -198,14 +225,16 @@ impl Highlighter {
|
|||||||
/// Extracts the highlighted output and resets the highlighter state
|
/// Extracts the highlighted output and resets the highlighter state
|
||||||
///
|
///
|
||||||
/// Clears the input buffer, style stack, and returns the generated output
|
/// Clears the input buffer, style stack, and returns the generated output
|
||||||
/// containing ANSI escape codes. The highlighter is ready for reuse after this.
|
/// containing ANSI escape codes. The highlighter is ready for reuse after
|
||||||
|
/// this.
|
||||||
pub fn take(&mut self) -> String {
|
pub fn take(&mut self) -> String {
|
||||||
self.input.clear();
|
self.input.clear();
|
||||||
self.clear_styles();
|
self.clear_styles();
|
||||||
std::mem::take(&mut self.output)
|
std::mem::take(&mut self.output)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if a command name is valid (exists in PATH, is a function, or is an alias)
|
/// Checks if a command name is valid (exists in PATH, is a function, or is an
|
||||||
|
/// alias)
|
||||||
///
|
///
|
||||||
/// Searches:
|
/// Searches:
|
||||||
/// 1. Current directory if command is a path
|
/// 1. Current directory if command is a path
|
||||||
@@ -222,9 +251,11 @@ impl Highlighter {
|
|||||||
// this is a directory and autocd is enabled
|
// this is a directory and autocd is enabled
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
let Ok(meta) = cmd_path.metadata() else { return false };
|
let Ok(meta) = cmd_path.metadata() else {
|
||||||
|
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
|
return meta.permissions().mode() & 0o111 == 0;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// they gave us a command name
|
// they gave us a command name
|
||||||
@@ -248,6 +279,49 @@ impl Highlighter {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_filename(arg: &str) -> bool {
|
||||||
|
let path = PathBuf::from(arg);
|
||||||
|
|
||||||
|
if path.exists() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(parent_dir) = path.parent()
|
||||||
|
&& let Ok(entries) = parent_dir.read_dir()
|
||||||
|
{
|
||||||
|
let files = entries
|
||||||
|
.filter_map(|e| e.ok())
|
||||||
|
.map(|e| e.file_name().to_string_lossy().to_string())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let Some(arg_filename) = PathBuf::from(arg)
|
||||||
|
.file_name()
|
||||||
|
.map(|s| s.to_string_lossy().to_string())
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
for file in files {
|
||||||
|
if file.starts_with(&arg_filename) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(this_dir) = env::current_dir()
|
||||||
|
&& let Ok(entries) = this_dir.read_dir()
|
||||||
|
{
|
||||||
|
let this_dir_files = entries
|
||||||
|
.filter_map(|e| e.ok())
|
||||||
|
.map(|e| e.file_name().to_string_lossy().to_string())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for file in this_dir_files {
|
||||||
|
if file.starts_with(arg) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
/// Emits a reset ANSI code to the output, with deduplication
|
/// Emits a reset ANSI code to the output, with deduplication
|
||||||
///
|
///
|
||||||
/// Only emits the reset if the last emitted code was not already a reset,
|
/// Only emits the reset if the last emitted code was not already a reset,
|
||||||
@@ -280,10 +354,11 @@ impl Highlighter {
|
|||||||
|
|
||||||
/// Pops a style from the stack and restores the previous style
|
/// Pops a style from the stack and restores the previous style
|
||||||
///
|
///
|
||||||
/// Used when exiting a syntax context. If there's a parent style on the stack,
|
/// Used when exiting a syntax context. If there's a parent style on the
|
||||||
/// it's re-emitted to restore the previous color. Otherwise, emits a reset.
|
/// stack, it's re-emitted to restore the previous color. Otherwise, emits a
|
||||||
/// This ensures colors are properly restored in nested constructs like
|
/// reset. This ensures colors are properly restored in nested constructs
|
||||||
/// `"string with $VAR"` where the string color resumes after the variable.
|
/// like `"string with $VAR"` where the string color resumes after the
|
||||||
|
/// variable.
|
||||||
pub fn pop_style(&mut self) {
|
pub fn pop_style(&mut self) {
|
||||||
self.style_stack.pop();
|
self.style_stack.pop();
|
||||||
if let Some(style) = self.style_stack.last().cloned() {
|
if let Some(style) = self.style_stack.last().cloned() {
|
||||||
@@ -302,13 +377,15 @@ impl Highlighter {
|
|||||||
self.emit_reset();
|
self.emit_reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Simple marker-to-ANSI replacement (unused in favor of stack-based highlighting)
|
/// Simple marker-to-ANSI replacement (unused in favor of stack-based
|
||||||
|
/// highlighting)
|
||||||
///
|
///
|
||||||
/// Performs direct string replacement of markers with ANSI codes, without
|
/// Performs direct string replacement of markers with ANSI codes, without
|
||||||
/// handling nesting or proper color restoration. Kept for reference but not
|
/// handling nesting or proper color restoration. Kept for reference but not
|
||||||
/// used in the current implementation.
|
/// used in the current implementation.
|
||||||
pub fn trivial_replace(&mut self) {
|
pub fn trivial_replace(&mut self) {
|
||||||
self.input = self.input
|
self.input = self
|
||||||
|
.input
|
||||||
.replace([markers::RESET, markers::ARG], "\x1b[0m")
|
.replace([markers::RESET, markers::ARG], "\x1b[0m")
|
||||||
.replace(markers::KEYWORD, "\x1b[33m")
|
.replace(markers::KEYWORD, "\x1b[33m")
|
||||||
.replace(markers::CASE_PAT, "\x1b[34m")
|
.replace(markers::CASE_PAT, "\x1b[34m")
|
||||||
|
|||||||
@@ -189,8 +189,8 @@ fn read_hist_file(path: &Path) -> ShResult<Vec<HistEntry>> {
|
|||||||
Ok(raw.parse::<HistEntries>()?.0)
|
Ok(raw.parse::<HistEntries>()?.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deduplicate entries, keeping only the most recent occurrence of each command.
|
/// Deduplicate entries, keeping only the most recent occurrence of each
|
||||||
/// Preserves chronological order (oldest to newest).
|
/// command. Preserves chronological order (oldest to newest).
|
||||||
fn dedupe_entries(entries: &[HistEntry]) -> Vec<HistEntry> {
|
fn dedupe_entries(entries: &[HistEntry]) -> Vec<HistEntry> {
|
||||||
let mut seen = HashSet::new();
|
let mut seen = HashSet::new();
|
||||||
// Iterate backwards (newest first), keeping first occurrence of each command
|
// Iterate backwards (newest first), keeping first occurrence of each command
|
||||||
@@ -207,7 +207,7 @@ fn dedupe_entries(entries: &[HistEntry]) -> Vec<HistEntry> {
|
|||||||
|
|
||||||
pub struct History {
|
pub struct History {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
pub pending: Option<String>,
|
pub pending: Option<(String, usize)>, // command, cursor_pos
|
||||||
entries: Vec<HistEntry>,
|
entries: Vec<HistEntry>,
|
||||||
search_mask: Vec<HistEntry>,
|
search_mask: Vec<HistEntry>,
|
||||||
no_matches: bool,
|
no_matches: bool,
|
||||||
@@ -270,14 +270,14 @@ impl History {
|
|||||||
self.cursor = self.search_mask.len();
|
self.cursor = self.search_mask.len();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_pending_cmd(&mut self, command: &str) {
|
pub fn update_pending_cmd(&mut self, buf: (&str, usize)) {
|
||||||
let cmd = command.to_string();
|
let cmd = buf.0.to_string();
|
||||||
let constraint = SearchConstraint {
|
let constraint = SearchConstraint {
|
||||||
kind: SearchKind::Prefix,
|
kind: SearchKind::Prefix,
|
||||||
term: cmd.clone(),
|
term: cmd.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.pending = Some(cmd);
|
self.pending = Some((cmd, buf.1));
|
||||||
self.constrain_entries(constraint);
|
self.constrain_entries(constraint);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,12 +328,14 @@ impl History {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn hint_entry(&self) -> Option<&HistEntry> {
|
pub fn hint_entry(&self) -> Option<&HistEntry> {
|
||||||
if self.no_matches { return None };
|
if self.no_matches {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
self.search_mask.last()
|
self.search_mask.last()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_hint(&self) -> Option<String> {
|
pub fn get_hint(&self) -> Option<String> {
|
||||||
if self.at_pending() && self.pending.as_ref().is_some_and(|p| !p.is_empty()) {
|
if self.at_pending() && self.pending.as_ref().is_some_and(|p| !p.0.is_empty()) {
|
||||||
let entry = self.hint_entry()?;
|
let entry = self.hint_entry()?;
|
||||||
Some(entry.command().to_string())
|
Some(entry.command().to_string())
|
||||||
} else {
|
} else {
|
||||||
@@ -342,9 +344,15 @@ impl History {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn scroll(&mut self, offset: isize) -> Option<&HistEntry> {
|
pub fn scroll(&mut self, offset: isize) -> Option<&HistEntry> {
|
||||||
self.cursor = self.cursor.saturating_add_signed(offset).clamp(0, self.search_mask.len());
|
self.cursor = self
|
||||||
|
.cursor
|
||||||
|
.saturating_add_signed(offset)
|
||||||
|
.clamp(0, self.search_mask.len());
|
||||||
|
|
||||||
log::debug!("Scrolling history by offset {offset} from cursor at index {}", self.cursor);
|
log::debug!(
|
||||||
|
"Scrolling history by offset {offset} from cursor at index {}",
|
||||||
|
self.cursor
|
||||||
|
);
|
||||||
self.search_mask.get(self.cursor)
|
self.search_mask.get(self.cursor)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,7 +386,8 @@ impl History {
|
|||||||
|
|
||||||
let last_file_entry = self
|
let last_file_entry = self
|
||||||
.entries
|
.entries
|
||||||
.iter().rfind(|ent| !ent.new)
|
.iter()
|
||||||
|
.rfind(|ent| !ent.new)
|
||||||
.map(|ent| ent.command.clone())
|
.map(|ent| ent.command.clone())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
|||||||
@@ -628,8 +628,9 @@ impl LineBuf {
|
|||||||
self.next_sentence_start_from_punctuation(pos).is_some()
|
self.next_sentence_start_from_punctuation(pos).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If position is at sentence-ending punctuation, returns the position of the next sentence start.
|
/// If position is at sentence-ending punctuation, returns the position of the
|
||||||
/// Handles closing delimiters (`)`, `]`, `"`, `'`) after punctuation.
|
/// next sentence start. Handles closing delimiters (`)`, `]`, `"`, `'`)
|
||||||
|
/// after punctuation.
|
||||||
#[allow(clippy::collapsible_if)]
|
#[allow(clippy::collapsible_if)]
|
||||||
pub fn next_sentence_start_from_punctuation(&self, pos: usize) -> Option<usize> {
|
pub fn next_sentence_start_from_punctuation(&self, pos: usize) -> Option<usize> {
|
||||||
if let Some(gr) = self.read_grapheme_at(pos) {
|
if let Some(gr) = self.read_grapheme_at(pos) {
|
||||||
@@ -956,7 +957,8 @@ impl LineBuf {
|
|||||||
let start = start.unwrap_or(0);
|
let start = start.unwrap_or(0);
|
||||||
|
|
||||||
if count > 1
|
if count > 1
|
||||||
&& let Some((_, new_end)) = self.text_obj_sentence(end, count - 1, bound) {
|
&& let Some((_, new_end)) = self.text_obj_sentence(end, count - 1, bound)
|
||||||
|
{
|
||||||
end = new_end;
|
end = new_end;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1363,7 +1365,12 @@ impl LineBuf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Find the start of the next word forward
|
/// Find the start of the next word forward
|
||||||
pub fn start_of_word_forward(&mut self, mut pos: usize, word: Word, include_last_char: bool) -> usize {
|
pub fn start_of_word_forward(
|
||||||
|
&mut self,
|
||||||
|
mut pos: usize,
|
||||||
|
word: Word,
|
||||||
|
include_last_char: bool,
|
||||||
|
) -> usize {
|
||||||
let default = self.grapheme_indices().len();
|
let default = self.grapheme_indices().len();
|
||||||
let mut indices_iter = (pos..self.cursor.max).peekable();
|
let mut indices_iter = (pos..self.cursor.max).peekable();
|
||||||
|
|
||||||
@@ -1390,8 +1397,7 @@ impl LineBuf {
|
|||||||
let on_whitespace = is_whitespace(&cur_char);
|
let on_whitespace = is_whitespace(&cur_char);
|
||||||
|
|
||||||
if !on_whitespace {
|
if !on_whitespace {
|
||||||
let Some(ws_pos) =
|
let Some(ws_pos) = indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace))
|
||||||
indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace))
|
|
||||||
else {
|
else {
|
||||||
return default;
|
return default;
|
||||||
};
|
};
|
||||||
@@ -1457,7 +1463,12 @@ impl LineBuf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Find the end of the previous word backward
|
/// Find the end of the previous word backward
|
||||||
pub fn end_of_word_backward(&mut self, mut pos: usize, word: Word, include_last_char: bool) -> usize {
|
pub fn end_of_word_backward(
|
||||||
|
&mut self,
|
||||||
|
mut pos: usize,
|
||||||
|
word: Word,
|
||||||
|
include_last_char: bool,
|
||||||
|
) -> usize {
|
||||||
let default = self.grapheme_indices().len();
|
let default = self.grapheme_indices().len();
|
||||||
let mut indices_iter = (0..pos).rev().peekable();
|
let mut indices_iter = (0..pos).rev().peekable();
|
||||||
|
|
||||||
@@ -1484,8 +1495,7 @@ impl LineBuf {
|
|||||||
let on_whitespace = is_whitespace(&cur_char);
|
let on_whitespace = is_whitespace(&cur_char);
|
||||||
|
|
||||||
if !on_whitespace {
|
if !on_whitespace {
|
||||||
let Some(ws_pos) =
|
let Some(ws_pos) = indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace))
|
||||||
indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace))
|
|
||||||
else {
|
else {
|
||||||
return default;
|
return default;
|
||||||
};
|
};
|
||||||
@@ -1742,11 +1752,7 @@ impl LineBuf {
|
|||||||
};
|
};
|
||||||
pos = next_ws_pos;
|
pos = next_ws_pos;
|
||||||
|
|
||||||
if pos == 0 {
|
if pos == 0 { pos } else { pos + 1 }
|
||||||
pos
|
|
||||||
} else {
|
|
||||||
pos + 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2575,7 +2581,8 @@ impl LineBuf {
|
|||||||
}
|
}
|
||||||
Verb::SwapVisualAnchor => {
|
Verb::SwapVisualAnchor => {
|
||||||
if let Some((start, end)) = self.select_range()
|
if let Some((start, end)) = self.select_range()
|
||||||
&& let Some(mut mode) = self.select_mode {
|
&& let Some(mut mode) = self.select_mode
|
||||||
|
{
|
||||||
mode.invert_anchor();
|
mode.invert_anchor();
|
||||||
let new_cursor_pos = match mode.anchor() {
|
let new_cursor_pos = match mode.anchor() {
|
||||||
SelectAnchor::Start => start,
|
SelectAnchor::Start => start,
|
||||||
@@ -2731,8 +2738,10 @@ impl LineBuf {
|
|||||||
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);
|
||||||
|
|
||||||
// Merge character inserts into one edit
|
// Merge character inserts into one edit
|
||||||
if edit_is_merging && cmd.verb.as_ref().is_none_or(|v| !v.1.is_char_insert())
|
if edit_is_merging
|
||||||
&& let Some(edit) = self.undo_stack.last_mut() {
|
&& cmd.verb.as_ref().is_none_or(|v| !v.1.is_char_insert())
|
||||||
|
&& let Some(edit) = self.undo_stack.last_mut()
|
||||||
|
{
|
||||||
edit.stop_merge();
|
edit.stop_merge();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2821,8 +2830,7 @@ impl LineBuf {
|
|||||||
self.saved_col = None;
|
self.saved_col = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_char_insert
|
if is_char_insert && let Some(edit) = self.undo_stack.last_mut() {
|
||||||
&& let Some(edit) = self.undo_stack.last_mut() {
|
|
||||||
edit.start_merge();
|
edit.start_merge();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2833,7 +2841,11 @@ impl LineBuf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_hint_text(&self) -> String {
|
pub fn get_hint_text(&self) -> String {
|
||||||
self.hint.clone().map(|h| h.styled(Style::BrightBlack)).unwrap_or_default()
|
self
|
||||||
|
.hint
|
||||||
|
.clone()
|
||||||
|
.map(|h| h.styled(Style::BrightBlack))
|
||||||
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,16 +2,22 @@ use history::History;
|
|||||||
use keys::{KeyCode, KeyEvent, ModKeys};
|
use keys::{KeyCode, KeyEvent, ModKeys};
|
||||||
use linebuf::{LineBuf, SelectAnchor, SelectMode};
|
use linebuf::{LineBuf, SelectAnchor, SelectMode};
|
||||||
use nix::libc::STDOUT_FILENO;
|
use nix::libc::STDOUT_FILENO;
|
||||||
use term::{get_win_size, KeyReader, Layout, LineWriter, PollReader, TermWriter};
|
use term::{KeyReader, Layout, LineWriter, PollReader, TermWriter, get_win_size};
|
||||||
use vicmd::{CmdFlags, Motion, MotionCmd, RegisterName, To, Verb, VerbCmd, ViCmd};
|
use vicmd::{CmdFlags, Motion, MotionCmd, RegisterName, Verb, VerbCmd, ViCmd};
|
||||||
use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual};
|
use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual};
|
||||||
|
|
||||||
use crate::{libsh::{
|
|
||||||
error::{ShErrKind, ShResult},
|
|
||||||
term::{Style, Styled},
|
|
||||||
}, parse::lex::{self, LexFlags, Tk, TkFlags, TkRule}, prompt::readline::{complete::{CompResult, Completer}, highlight::Highlighter}};
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
use crate::{
|
||||||
|
libsh::{
|
||||||
|
error::ShResult,
|
||||||
|
term::{Style, Styled},
|
||||||
|
},
|
||||||
|
parse::lex::{self, LexFlags, Tk, TkFlags, TkRule},
|
||||||
|
prompt::readline::{complete::Completer, highlight::Highlighter},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod complete;
|
||||||
|
pub mod highlight;
|
||||||
pub mod history;
|
pub mod history;
|
||||||
pub mod keys;
|
pub mod keys;
|
||||||
pub mod layout;
|
pub mod layout;
|
||||||
@@ -20,8 +26,6 @@ pub mod register;
|
|||||||
pub mod term;
|
pub mod term;
|
||||||
pub mod vicmd;
|
pub mod vicmd;
|
||||||
pub mod vimode;
|
pub mod vimode;
|
||||||
pub mod highlight;
|
|
||||||
pub mod complete;
|
|
||||||
|
|
||||||
pub mod markers {
|
pub mod markers {
|
||||||
use super::Marker;
|
use super::Marker;
|
||||||
@@ -58,35 +62,19 @@ pub mod markers {
|
|||||||
|
|
||||||
pub const NULL: Marker = '\u{fdef}';
|
pub const NULL: Marker = '\u{fdef}';
|
||||||
|
|
||||||
pub const END_MARKERS: [Marker;7] = [
|
pub const END_MARKERS: [Marker; 7] = [
|
||||||
VAR_SUB_END,
|
VAR_SUB_END,
|
||||||
CMD_SUB_END,
|
CMD_SUB_END,
|
||||||
PROC_SUB_END,
|
PROC_SUB_END,
|
||||||
STRING_DQ_END,
|
STRING_DQ_END,
|
||||||
STRING_SQ_END,
|
STRING_SQ_END,
|
||||||
SUBSH_END,
|
SUBSH_END,
|
||||||
RESET
|
RESET,
|
||||||
];
|
];
|
||||||
pub const TOKEN_LEVEL: [Marker;10] = [
|
pub const TOKEN_LEVEL: [Marker; 10] = [
|
||||||
SUBSH,
|
SUBSH, COMMAND, BUILTIN, ARG, KEYWORD, OPERATOR, REDIRECT, CMD_SEP, CASE_PAT, ASSIGNMENT,
|
||||||
COMMAND,
|
|
||||||
BUILTIN,
|
|
||||||
ARG,
|
|
||||||
KEYWORD,
|
|
||||||
OPERATOR,
|
|
||||||
REDIRECT,
|
|
||||||
CMD_SEP,
|
|
||||||
CASE_PAT,
|
|
||||||
ASSIGNMENT,
|
|
||||||
];
|
|
||||||
pub const SUB_TOKEN: [Marker;6] = [
|
|
||||||
VAR_SUB,
|
|
||||||
CMD_SUB,
|
|
||||||
PROC_SUB,
|
|
||||||
STRING_DQ,
|
|
||||||
STRING_SQ,
|
|
||||||
GLOB,
|
|
||||||
];
|
];
|
||||||
|
pub const SUB_TOKEN: [Marker; 6] = [VAR_SUB, CMD_SUB, PROC_SUB, STRING_DQ, STRING_SQ, GLOB];
|
||||||
|
|
||||||
pub fn is_marker(c: Marker) -> bool {
|
pub fn is_marker(c: Marker) -> bool {
|
||||||
TOKEN_LEVEL.contains(&c) || SUB_TOKEN.contains(&c) || END_MARKERS.contains(&c)
|
TOKEN_LEVEL.contains(&c) || SUB_TOKEN.contains(&c) || END_MARKERS.contains(&c)
|
||||||
@@ -145,14 +133,14 @@ impl FernVi {
|
|||||||
|
|
||||||
pub fn with_initial(mut self, initial: &str) -> Self {
|
pub fn with_initial(mut self, initial: &str) -> Self {
|
||||||
self.editor = LineBuf::new().with_initial(initial, 0);
|
self.editor = LineBuf::new().with_initial(initial, 0);
|
||||||
self.history.update_pending_cmd(self.editor.as_str());
|
self
|
||||||
|
.history
|
||||||
|
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Feed raw bytes from stdin into the reader's buffer
|
/// Feed raw bytes from stdin into the reader's buffer
|
||||||
pub fn feed_bytes(&mut self, bytes: &[u8]) {
|
pub fn feed_bytes(&mut self, bytes: &[u8]) {
|
||||||
let test_input = "echo \"hello $USER\" | grep $(whoami)";
|
|
||||||
let annotated = annotate_input(test_input);
|
|
||||||
self.reader.feed_bytes(bytes);
|
self.reader.feed_bytes(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,13 +173,14 @@ impl FernVi {
|
|||||||
|
|
||||||
// Process all available keys
|
// Process all available keys
|
||||||
while let Some(key) = self.reader.read_key()? {
|
while let Some(key) = self.reader.read_key()? {
|
||||||
|
|
||||||
if self.should_accept_hint(&key) {
|
if self.should_accept_hint(&key) {
|
||||||
self.editor.accept_hint();
|
self.editor.accept_hint();
|
||||||
if !self.history.at_pending() {
|
if !self.history.at_pending() {
|
||||||
self.history.reset_to_pending();
|
self.history.reset_to_pending();
|
||||||
}
|
}
|
||||||
self.history.update_pending_cmd(self.editor.as_str());
|
self
|
||||||
|
.history
|
||||||
|
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
|
||||||
self.needs_redraw = true;
|
self.needs_redraw = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -205,9 +194,14 @@ impl FernVi {
|
|||||||
let cursor_pos = self.editor.cursor_byte_pos();
|
let cursor_pos = self.editor.cursor_byte_pos();
|
||||||
|
|
||||||
match self.completer.complete(line, cursor_pos, direction)? {
|
match self.completer.complete(line, cursor_pos, direction)? {
|
||||||
Some(mut line) => {
|
Some(line) => {
|
||||||
let span_start = self.completer.token_span.0;
|
let span_start = self.completer.token_span.0;
|
||||||
let new_cursor = span_start + self.completer.selected_candidate().map(|c| c.len()).unwrap_or_default();
|
let new_cursor = span_start
|
||||||
|
+ self
|
||||||
|
.completer
|
||||||
|
.selected_candidate()
|
||||||
|
.map(|c| c.len())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
self.editor.set_buffer(line);
|
self.editor.set_buffer(line);
|
||||||
self.editor.cursor.set(new_cursor);
|
self.editor.cursor.set(new_cursor);
|
||||||
@@ -215,16 +209,18 @@ impl FernVi {
|
|||||||
if !self.history.at_pending() {
|
if !self.history.at_pending() {
|
||||||
self.history.reset_to_pending();
|
self.history.reset_to_pending();
|
||||||
}
|
}
|
||||||
self.history.update_pending_cmd(self.editor.as_str());
|
self
|
||||||
|
.history
|
||||||
|
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
|
||||||
let hint = self.history.get_hint();
|
let hint = self.history.get_hint();
|
||||||
self.editor.set_hint(hint);
|
self.editor.set_hint(hint);
|
||||||
}
|
}
|
||||||
None => {
|
None => match crate::state::read_shopts(|s| s.core.bell_style) {
|
||||||
match crate::state::read_shopts(|s| s.core.bell_style) {
|
crate::shopt::FernBellStyle::Audible => {
|
||||||
crate::shopt::FernBellStyle::Audible => { self.writer.flush_write("\x07")?; }
|
self.writer.flush_write("\x07")?;
|
||||||
|
}
|
||||||
crate::shopt::FernBellStyle::Visible | crate::shopt::FernBellStyle::Disable => {}
|
crate::shopt::FernBellStyle::Visible | crate::shopt::FernBellStyle::Disable => {}
|
||||||
}
|
},
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.needs_redraw = true;
|
self.needs_redraw = true;
|
||||||
@@ -252,8 +248,7 @@ impl FernVi {
|
|||||||
self.writer.flush_write("\n")?;
|
self.writer.flush_write("\n")?;
|
||||||
let buf = self.editor.take_buf();
|
let buf = self.editor.take_buf();
|
||||||
// Save command to history if auto_hist is enabled
|
// Save command to history if auto_hist is enabled
|
||||||
if crate::state::read_shopts(|s| s.core.auto_hist)
|
if crate::state::read_shopts(|s| s.core.auto_hist) && !buf.is_empty() {
|
||||||
&& !buf.is_empty() {
|
|
||||||
self.history.push(buf.clone());
|
self.history.push(buf.clone());
|
||||||
if let Err(e) = self.history.save() {
|
if let Err(e) = self.history.save() {
|
||||||
eprintln!("Failed to save history: {e}");
|
eprintln!("Failed to save history: {e}");
|
||||||
@@ -278,7 +273,9 @@ impl FernVi {
|
|||||||
let after = self.editor.as_str();
|
let after = self.editor.as_str();
|
||||||
|
|
||||||
if before != after {
|
if before != after {
|
||||||
self.history.update_pending_cmd(self.editor.as_str());
|
self
|
||||||
|
.history
|
||||||
|
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let hint = self.history.get_hint();
|
let hint = self.history.get_hint();
|
||||||
@@ -311,12 +308,8 @@ impl FernVi {
|
|||||||
let count = &cmd.motion().unwrap().0;
|
let count = &cmd.motion().unwrap().0;
|
||||||
let motion = &cmd.motion().unwrap().1;
|
let motion = &cmd.motion().unwrap().1;
|
||||||
let count = match motion {
|
let count = match motion {
|
||||||
Motion::LineUpCharwise => {
|
Motion::LineUpCharwise => -(*count as isize),
|
||||||
-(*count as isize)
|
Motion::LineDownCharwise => *count as isize,
|
||||||
}
|
|
||||||
Motion::LineDownCharwise => {
|
|
||||||
*count as isize
|
|
||||||
}
|
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
let entry = self.history.scroll(count);
|
let entry = self.history.scroll(count);
|
||||||
@@ -326,12 +319,13 @@ impl FernVi {
|
|||||||
let pending = self.editor.take_buf();
|
let pending = self.editor.take_buf();
|
||||||
self.editor.set_buffer(entry.command().to_string());
|
self.editor.set_buffer(entry.command().to_string());
|
||||||
if self.history.pending.is_none() {
|
if self.history.pending.is_none() {
|
||||||
self.history.pending = Some(pending);
|
self.history.pending = Some((pending, self.editor.cursor.get()));
|
||||||
}
|
}
|
||||||
self.editor.set_hint(None);
|
self.editor.set_hint(None);
|
||||||
} else if let Some(pending) = self.history.pending.take() {
|
} else if let Some(pending) = self.history.pending.take() {
|
||||||
log::info!("Setting buffer to pending command: {}", &pending);
|
log::info!("Setting buffer to pending command: {}", &pending.0);
|
||||||
self.editor.set_buffer(pending);
|
self.editor.set_buffer(pending.0);
|
||||||
|
self.editor.cursor.set(pending.1);
|
||||||
self.editor.set_hint(None);
|
self.editor.set_hint(None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -385,9 +379,7 @@ impl FernVi {
|
|||||||
self.writer.clear_rows(layout)?;
|
self.writer.clear_rows(layout)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self
|
self.writer.redraw(&self.prompt, &line, &new_layout)?;
|
||||||
.writer
|
|
||||||
.redraw(&self.prompt, &line, &new_layout)?;
|
|
||||||
|
|
||||||
self.writer.flush_write(&self.mode.cursor_style())?;
|
self.writer.flush_write(&self.mode.cursor_style())?;
|
||||||
|
|
||||||
@@ -553,8 +545,8 @@ impl FernVi {
|
|||||||
|
|
||||||
/// Annotates shell input with invisible Unicode markers for syntax highlighting
|
/// Annotates shell input with invisible Unicode markers for syntax highlighting
|
||||||
///
|
///
|
||||||
/// Takes raw shell input and inserts non-character markers (U+FDD0-U+FDEF range)
|
/// Takes raw shell input and inserts non-character markers (U+FDD0-U+FDEF
|
||||||
/// around syntax elements. These markers indicate:
|
/// range) around syntax elements. These markers indicate:
|
||||||
/// - Token-level context (commands, arguments, operators, keywords)
|
/// - Token-level context (commands, arguments, operators, keywords)
|
||||||
/// - Sub-token constructs (strings, variables, command substitutions, globs)
|
/// - Sub-token constructs (strings, variables, command substitutions, globs)
|
||||||
///
|
///
|
||||||
@@ -595,11 +587,9 @@ pub fn annotate_input_recursive(input: &str) -> String {
|
|||||||
let mut chars = annotated.char_indices().peekable();
|
let mut chars = annotated.char_indices().peekable();
|
||||||
let mut changes = vec![];
|
let mut changes = vec![];
|
||||||
|
|
||||||
while let Some((pos,ch)) = chars.next() {
|
while let Some((pos, ch)) = chars.next() {
|
||||||
match ch {
|
match ch {
|
||||||
markers::CMD_SUB |
|
markers::CMD_SUB | markers::SUBSH | markers::PROC_SUB => {
|
||||||
markers::SUBSH |
|
|
||||||
markers::PROC_SUB => {
|
|
||||||
let mut body = String::new();
|
let mut body = String::new();
|
||||||
let span_start = pos + ch.len_utf8();
|
let span_start = pos + ch.len_utf8();
|
||||||
let mut span_end = span_start;
|
let mut span_end = span_start;
|
||||||
@@ -607,9 +597,9 @@ pub fn annotate_input_recursive(input: &str) -> String {
|
|||||||
markers::CMD_SUB => markers::CMD_SUB_END,
|
markers::CMD_SUB => markers::CMD_SUB_END,
|
||||||
markers::SUBSH => markers::SUBSH_END,
|
markers::SUBSH => markers::SUBSH_END,
|
||||||
markers::PROC_SUB => markers::PROC_SUB_END,
|
markers::PROC_SUB => markers::PROC_SUB_END,
|
||||||
_ => unreachable!()
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
while let Some((sub_pos,sub_ch)) = chars.next() {
|
while let Some((sub_pos, sub_ch)) = chars.next() {
|
||||||
match sub_ch {
|
match sub_ch {
|
||||||
_ if sub_ch == closing_marker => {
|
_ if sub_ch == closing_marker => {
|
||||||
span_end = sub_pos;
|
span_end = sub_pos;
|
||||||
@@ -619,19 +609,17 @@ pub fn annotate_input_recursive(input: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let prefix = match ch {
|
let prefix = match ch {
|
||||||
markers::PROC_SUB => {
|
markers::PROC_SUB => match chars.peek().map(|(_, c)| *c) {
|
||||||
match chars.peek().map(|(_, c)| *c) {
|
|
||||||
Some('>') => ">(",
|
Some('>') => ">(",
|
||||||
Some('<') => "<(",
|
Some('<') => "<(",
|
||||||
_ => {
|
_ => {
|
||||||
log::error!("Unexpected character after PROC_SUB marker: expected '>' or '<'");
|
log::error!("Unexpected character after PROC_SUB marker: expected '>' or '<'");
|
||||||
"<("
|
"<("
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
|
||||||
markers::CMD_SUB => "$(",
|
markers::CMD_SUB => "$(",
|
||||||
markers::SUBSH => "(",
|
markers::SUBSH => "(",
|
||||||
_ => unreachable!()
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
body = body.trim_start_matches(prefix).to_string();
|
body = body.trim_start_matches(prefix).to_string();
|
||||||
@@ -667,8 +655,8 @@ pub fn get_insertions(input: &str) -> Vec<(usize, Marker)> {
|
|||||||
/// Maps token class to its corresponding marker character
|
/// Maps token class to its corresponding marker character
|
||||||
///
|
///
|
||||||
/// Returns the appropriate Unicode marker for token-level syntax elements.
|
/// Returns the appropriate Unicode marker for token-level syntax elements.
|
||||||
/// Token-level markers are derived directly from the lexer's token classification
|
/// Token-level markers are derived directly from the lexer's token
|
||||||
/// and represent complete tokens (operators, separators, etc.).
|
/// classification and represent complete tokens (operators, separators, etc.).
|
||||||
///
|
///
|
||||||
/// Returns `None` for:
|
/// Returns `None` for:
|
||||||
/// - String tokens (which need sub-token scanning for variables, quotes, etc.)
|
/// - String tokens (which need sub-token scanning for variables, quotes, etc.)
|
||||||
@@ -676,22 +664,16 @@ pub fn get_insertions(input: &str) -> Vec<(usize, Marker)> {
|
|||||||
/// - Unimplemented features (comments, brace groups)
|
/// - Unimplemented features (comments, brace groups)
|
||||||
pub fn marker_for(class: &TkRule) -> Option<Marker> {
|
pub fn marker_for(class: &TkRule) -> Option<Marker> {
|
||||||
match class {
|
match class {
|
||||||
TkRule::Pipe |
|
TkRule::Pipe | TkRule::ErrPipe | TkRule::And | TkRule::Or | TkRule::Bg => {
|
||||||
TkRule::ErrPipe |
|
Some(markers::OPERATOR)
|
||||||
TkRule::And |
|
}
|
||||||
TkRule::Or |
|
|
||||||
TkRule::Bg => Some(markers::OPERATOR),
|
|
||||||
TkRule::Sep => Some(markers::CMD_SEP),
|
TkRule::Sep => Some(markers::CMD_SEP),
|
||||||
TkRule::Redir => Some(markers::REDIRECT),
|
TkRule::Redir => Some(markers::REDIRECT),
|
||||||
TkRule::CasePattern => Some(markers::CASE_PAT),
|
TkRule::CasePattern => Some(markers::CASE_PAT),
|
||||||
TkRule::BraceGrpStart => todo!(),
|
TkRule::BraceGrpStart => todo!(),
|
||||||
TkRule::BraceGrpEnd => todo!(),
|
TkRule::BraceGrpEnd => todo!(),
|
||||||
TkRule::Comment => todo!(),
|
TkRule::Comment => todo!(),
|
||||||
TkRule::Expanded { exp: _ } |
|
TkRule::Expanded { exp: _ } | TkRule::EOI | TkRule::SOI | TkRule::Null | TkRule::Str => None,
|
||||||
TkRule::EOI |
|
|
||||||
TkRule::SOI |
|
|
||||||
TkRule::Null |
|
|
||||||
TkRule::Str => None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -702,23 +684,22 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
|
|||||||
// - END markers last (inserted last, ends up leftmost)
|
// - END markers last (inserted last, ends up leftmost)
|
||||||
// Result: [END][TOGGLE][RESET]
|
// Result: [END][TOGGLE][RESET]
|
||||||
let sort_insertions = |insertions: &mut Vec<(usize, Marker)>| {
|
let sort_insertions = |insertions: &mut Vec<(usize, Marker)>| {
|
||||||
insertions.sort_by(|a, b| {
|
insertions.sort_by(|a, b| match b.0.cmp(&a.0) {
|
||||||
match b.0.cmp(&a.0) {
|
|
||||||
std::cmp::Ordering::Equal => {
|
std::cmp::Ordering::Equal => {
|
||||||
let priority = |m: Marker| -> u8 {
|
let priority = |m: Marker| -> u8 {
|
||||||
match m {
|
match m {
|
||||||
markers::RESET => 0,
|
markers::RESET => 0,
|
||||||
markers::VAR_SUB |
|
markers::VAR_SUB
|
||||||
markers::VAR_SUB_END |
|
| markers::VAR_SUB_END
|
||||||
markers::CMD_SUB |
|
| markers::CMD_SUB
|
||||||
markers::CMD_SUB_END |
|
| markers::CMD_SUB_END
|
||||||
markers::PROC_SUB |
|
| markers::PROC_SUB
|
||||||
markers::PROC_SUB_END |
|
| markers::PROC_SUB_END
|
||||||
markers::STRING_DQ |
|
| markers::STRING_DQ
|
||||||
markers::STRING_DQ_END |
|
| markers::STRING_DQ_END
|
||||||
markers::STRING_SQ |
|
| markers::STRING_SQ
|
||||||
markers::STRING_SQ_END |
|
| markers::STRING_SQ_END
|
||||||
markers::SUBSH_END => 2,
|
| markers::SUBSH_END => 2,
|
||||||
markers::ARG => 3,
|
markers::ARG => 3,
|
||||||
_ => 1,
|
_ => 1,
|
||||||
}
|
}
|
||||||
@@ -726,7 +707,6 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
|
|||||||
priority(a.1).cmp(&priority(b.1))
|
priority(a.1).cmp(&priority(b.1))
|
||||||
}
|
}
|
||||||
other => other,
|
other => other,
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -738,17 +718,17 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
|
|||||||
let priority = |m: Marker| -> u8 {
|
let priority = |m: Marker| -> u8 {
|
||||||
match m {
|
match m {
|
||||||
markers::RESET => 0,
|
markers::RESET => 0,
|
||||||
markers::VAR_SUB |
|
markers::VAR_SUB
|
||||||
markers::VAR_SUB_END |
|
| markers::VAR_SUB_END
|
||||||
markers::CMD_SUB |
|
| markers::CMD_SUB
|
||||||
markers::CMD_SUB_END |
|
| markers::CMD_SUB_END
|
||||||
markers::PROC_SUB |
|
| markers::PROC_SUB
|
||||||
markers::PROC_SUB_END |
|
| markers::PROC_SUB_END
|
||||||
markers::STRING_DQ |
|
| markers::STRING_DQ
|
||||||
markers::STRING_DQ_END |
|
| markers::STRING_DQ_END
|
||||||
markers::STRING_SQ |
|
| markers::STRING_SQ
|
||||||
markers::STRING_SQ_END |
|
| markers::STRING_SQ_END
|
||||||
markers::SUBSH_END => 2,
|
| markers::SUBSH_END => 2,
|
||||||
markers::ARG => 3, // Lowest priority - processed first, overridden by sub-tokens
|
markers::ARG => 3, // Lowest priority - processed first, overridden by sub-tokens
|
||||||
_ => 1,
|
_ => 1,
|
||||||
}
|
}
|
||||||
@@ -769,9 +749,9 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
|
|||||||
|
|
||||||
let mut insertions: Vec<(usize, Marker)> = vec![];
|
let mut insertions: Vec<(usize, Marker)> = vec![];
|
||||||
|
|
||||||
|
|
||||||
if token.class != TkRule::Str
|
if token.class != TkRule::Str
|
||||||
&& let Some(marker) = marker_for(&token.class) {
|
&& let Some(marker) = marker_for(&token.class)
|
||||||
|
{
|
||||||
insertions.push((token.span.end, markers::RESET));
|
insertions.push((token.span.end, markers::RESET));
|
||||||
insertions.push((token.span.start, marker));
|
insertions.push((token.span.start, marker));
|
||||||
return insertions;
|
return insertions;
|
||||||
@@ -784,11 +764,8 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
|
|||||||
return insertions;
|
return insertions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let token_raw = token.span.as_str();
|
let token_raw = token.span.as_str();
|
||||||
let mut token_chars = token_raw
|
let mut token_chars = token_raw.char_indices().peekable();
|
||||||
.char_indices()
|
|
||||||
.peekable();
|
|
||||||
|
|
||||||
let span_start = token.span.start;
|
let span_start = token.span.start;
|
||||||
|
|
||||||
@@ -815,7 +792,7 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
|
|||||||
|
|
||||||
insertions.insert(0, (token.span.end, markers::RESET)); // reset at the end of the token
|
insertions.insert(0, (token.span.end, markers::RESET)); // reset at the end of the token
|
||||||
|
|
||||||
while let Some((i,ch)) = token_chars.peek() {
|
while let Some((i, ch)) = token_chars.peek() {
|
||||||
let index = *i; // we have to dereference this here because rustc is a very pedantic program
|
let index = *i; // we have to dereference this here because rustc is a very pedantic program
|
||||||
match ch {
|
match ch {
|
||||||
')' if cmd_sub_depth > 0 || proc_sub_depth > 0 => {
|
')' if cmd_sub_depth > 0 || proc_sub_depth > 0 => {
|
||||||
@@ -863,7 +840,8 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
|
|||||||
|| *br_ch == '+'
|
|| *br_ch == '+'
|
||||||
|| *br_ch == '='
|
|| *br_ch == '='
|
||||||
|| *br_ch == '/' // parameter expansion symbols
|
|| *br_ch == '/' // parameter expansion symbols
|
||||||
|| *br_ch == '?' {
|
|| *br_ch == '?'
|
||||||
|
{
|
||||||
token_chars.next();
|
token_chars.next();
|
||||||
} else if *br_ch == '}' {
|
} else if *br_ch == '}' {
|
||||||
token_chars.next(); // consume the closing brace
|
token_chars.next(); // consume the closing brace
|
||||||
@@ -910,7 +888,8 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
|
|||||||
'<' | '>' if !in_dub_qt && !in_sng_qt && cmd_sub_depth == 0 && proc_sub_depth == 0 => {
|
'<' | '>' if !in_dub_qt && !in_sng_qt && cmd_sub_depth == 0 && proc_sub_depth == 0 => {
|
||||||
token_chars.next();
|
token_chars.next();
|
||||||
if let Some((_, proc_sub_ch)) = token_chars.peek()
|
if let Some((_, proc_sub_ch)) = token_chars.peek()
|
||||||
&& *proc_sub_ch == '(' {
|
&& *proc_sub_ch == '('
|
||||||
|
{
|
||||||
proc_sub_depth += 1;
|
proc_sub_depth += 1;
|
||||||
token_chars.next(); // consume the paren
|
token_chars.next(); // consume the paren
|
||||||
if proc_sub_depth == 1 {
|
if proc_sub_depth == 1 {
|
||||||
|
|||||||
@@ -17,11 +17,15 @@ use unicode_segmentation::UnicodeSegmentation;
|
|||||||
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||||||
use vte::{Parser, Perform};
|
use vte::{Parser, Perform};
|
||||||
|
|
||||||
use crate::{prelude::*, procio::borrow_fd, state::{read_meta, write_meta}};
|
|
||||||
use crate::{
|
use crate::{
|
||||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||||
prompt::readline::keys::{KeyCode, ModKeys},
|
prompt::readline::keys::{KeyCode, ModKeys},
|
||||||
};
|
};
|
||||||
|
use crate::{
|
||||||
|
prelude::*,
|
||||||
|
procio::borrow_fd,
|
||||||
|
state::{read_meta, write_meta},
|
||||||
|
};
|
||||||
|
|
||||||
use super::{keys::KeyEvent, linebuf::LineBuf};
|
use super::{keys::KeyEvent, linebuf::LineBuf};
|
||||||
|
|
||||||
@@ -242,9 +246,7 @@ impl Read for TermBuffer {
|
|||||||
let result = nix::unistd::read(self.tty, buf);
|
let result = nix::unistd::read(self.tty, buf);
|
||||||
match result {
|
match result {
|
||||||
Ok(n) => Ok(n),
|
Ok(n) => Ok(n),
|
||||||
Err(Errno::EINTR) => {
|
Err(Errno::EINTR) => Err(Errno::EINTR.into()),
|
||||||
Err(Errno::EINTR.into())
|
|
||||||
}
|
|
||||||
Err(e) => Err(std::io::Error::from_raw_os_error(e as i32)),
|
Err(e) => Err(std::io::Error::from_raw_os_error(e as i32)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -281,14 +283,18 @@ impl RawModeGuard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_cooked_mode<F, R>(f: F) -> R
|
pub fn with_cooked_mode<F, R>(f: F) -> R
|
||||||
where F: FnOnce() -> R {
|
where
|
||||||
|
F: FnOnce() -> R,
|
||||||
|
{
|
||||||
let raw = tcgetattr(borrow_fd(STDIN_FILENO)).expect("Failed to get terminal attributes");
|
let raw = tcgetattr(borrow_fd(STDIN_FILENO)).expect("Failed to get terminal attributes");
|
||||||
let mut cooked = raw.clone();
|
let mut cooked = raw.clone();
|
||||||
cooked.local_flags |= termios::LocalFlags::ICANON | termios::LocalFlags::ECHO;
|
cooked.local_flags |= termios::LocalFlags::ICANON | termios::LocalFlags::ECHO;
|
||||||
cooked.input_flags |= termios::InputFlags::ICRNL;
|
cooked.input_flags |= termios::InputFlags::ICRNL;
|
||||||
tcsetattr(borrow_fd(STDIN_FILENO), termios::SetArg::TCSANOW, &cooked).expect("Failed to set cooked mode");
|
tcsetattr(borrow_fd(STDIN_FILENO), termios::SetArg::TCSANOW, &cooked)
|
||||||
|
.expect("Failed to set cooked mode");
|
||||||
let res = f();
|
let res = f();
|
||||||
tcsetattr(borrow_fd(STDIN_FILENO), termios::SetArg::TCSANOW, &raw).expect("Failed to restore raw mode");
|
tcsetattr(borrow_fd(STDIN_FILENO), termios::SetArg::TCSANOW, &raw)
|
||||||
|
.expect("Failed to restore raw mode");
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -333,9 +339,15 @@ impl KeyCollector {
|
|||||||
// CSI modifiers: param = 1 + (shift) + (alt*2) + (ctrl*4) + (meta*8)
|
// CSI modifiers: param = 1 + (shift) + (alt*2) + (ctrl*4) + (meta*8)
|
||||||
let bits = param.saturating_sub(1);
|
let bits = param.saturating_sub(1);
|
||||||
let mut mods = ModKeys::empty();
|
let mut mods = ModKeys::empty();
|
||||||
if bits & 1 != 0 { mods |= ModKeys::SHIFT; }
|
if bits & 1 != 0 {
|
||||||
if bits & 2 != 0 { mods |= ModKeys::ALT; }
|
mods |= ModKeys::SHIFT;
|
||||||
if bits & 4 != 0 { mods |= ModKeys::CTRL; }
|
}
|
||||||
|
if bits & 2 != 0 {
|
||||||
|
mods |= ModKeys::ALT;
|
||||||
|
}
|
||||||
|
if bits & 4 != 0 {
|
||||||
|
mods |= ModKeys::CTRL;
|
||||||
|
}
|
||||||
mods
|
mods
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -374,46 +386,72 @@ impl Perform for KeyCollector {
|
|||||||
self.push(event);
|
self.push(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn csi_dispatch(&mut self, params: &vte::Params, intermediates: &[u8], _ignore: bool, action: char) {
|
fn csi_dispatch(
|
||||||
let params: Vec<u16> = params.iter()
|
&mut self,
|
||||||
|
params: &vte::Params,
|
||||||
|
intermediates: &[u8],
|
||||||
|
_ignore: bool,
|
||||||
|
action: char,
|
||||||
|
) {
|
||||||
|
let params: Vec<u16> = params
|
||||||
|
.iter()
|
||||||
.map(|p| p.first().copied().unwrap_or(0))
|
.map(|p| p.first().copied().unwrap_or(0))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let event = match (intermediates, action) {
|
let event = match (intermediates, action) {
|
||||||
// Arrow keys: CSI A/B/C/D or CSI 1;mod A/B/C/D
|
// Arrow keys: CSI A/B/C/D or CSI 1;mod A/B/C/D
|
||||||
([], 'A') => {
|
([], 'A') => {
|
||||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
let mods = params
|
||||||
|
.get(1)
|
||||||
|
.map(|&m| Self::parse_modifiers(m))
|
||||||
|
.unwrap_or(ModKeys::empty());
|
||||||
KeyEvent(KeyCode::Up, mods)
|
KeyEvent(KeyCode::Up, mods)
|
||||||
}
|
}
|
||||||
([], 'B') => {
|
([], 'B') => {
|
||||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
let mods = params
|
||||||
|
.get(1)
|
||||||
|
.map(|&m| Self::parse_modifiers(m))
|
||||||
|
.unwrap_or(ModKeys::empty());
|
||||||
KeyEvent(KeyCode::Down, mods)
|
KeyEvent(KeyCode::Down, mods)
|
||||||
}
|
}
|
||||||
([], 'C') => {
|
([], 'C') => {
|
||||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
let mods = params
|
||||||
|
.get(1)
|
||||||
|
.map(|&m| Self::parse_modifiers(m))
|
||||||
|
.unwrap_or(ModKeys::empty());
|
||||||
KeyEvent(KeyCode::Right, mods)
|
KeyEvent(KeyCode::Right, mods)
|
||||||
}
|
}
|
||||||
([], 'D') => {
|
([], 'D') => {
|
||||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
let mods = params
|
||||||
|
.get(1)
|
||||||
|
.map(|&m| Self::parse_modifiers(m))
|
||||||
|
.unwrap_or(ModKeys::empty());
|
||||||
KeyEvent(KeyCode::Left, mods)
|
KeyEvent(KeyCode::Left, mods)
|
||||||
}
|
}
|
||||||
// Home/End: CSI H/F or CSI 1;mod H/F
|
// Home/End: CSI H/F or CSI 1;mod H/F
|
||||||
([], 'H') => {
|
([], 'H') => {
|
||||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
let mods = params
|
||||||
|
.get(1)
|
||||||
|
.map(|&m| Self::parse_modifiers(m))
|
||||||
|
.unwrap_or(ModKeys::empty());
|
||||||
KeyEvent(KeyCode::Home, mods)
|
KeyEvent(KeyCode::Home, mods)
|
||||||
}
|
}
|
||||||
([], 'F') => {
|
([], 'F') => {
|
||||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
let mods = params
|
||||||
|
.get(1)
|
||||||
|
.map(|&m| Self::parse_modifiers(m))
|
||||||
|
.unwrap_or(ModKeys::empty());
|
||||||
KeyEvent(KeyCode::End, mods)
|
KeyEvent(KeyCode::End, mods)
|
||||||
}
|
}
|
||||||
// Shift+Tab: CSI Z
|
// Shift+Tab: CSI Z
|
||||||
([], 'Z') => {
|
([], 'Z') => KeyEvent(KeyCode::Tab, ModKeys::SHIFT),
|
||||||
KeyEvent(KeyCode::Tab, ModKeys::SHIFT)
|
|
||||||
}
|
|
||||||
// Special keys with tilde: CSI num ~ or CSI num;mod ~
|
// Special keys with tilde: CSI num ~ or CSI num;mod ~
|
||||||
([], '~') => {
|
([], '~') => {
|
||||||
let key_num = params.first().copied().unwrap_or(0);
|
let key_num = params.first().copied().unwrap_or(0);
|
||||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
let mods = params
|
||||||
|
.get(1)
|
||||||
|
.map(|&m| Self::parse_modifiers(m))
|
||||||
|
.unwrap_or(ModKeys::empty());
|
||||||
let key = match key_num {
|
let key = match key_num {
|
||||||
1 | 7 => KeyCode::Home,
|
1 | 7 => KeyCode::Home,
|
||||||
2 => KeyCode::Insert,
|
2 => KeyCode::Insert,
|
||||||
@@ -473,7 +511,9 @@ impl PollReader {
|
|||||||
pub fn feed_bytes(&mut self, bytes: &[u8]) {
|
pub fn feed_bytes(&mut self, bytes: &[u8]) {
|
||||||
if bytes == [b'\x1b'] {
|
if bytes == [b'\x1b'] {
|
||||||
// Single escape byte - user pressed ESC key
|
// Single escape byte - user pressed ESC key
|
||||||
self.collector.push(KeyEvent(KeyCode::Esc, ModKeys::empty()));
|
self
|
||||||
|
.collector
|
||||||
|
.push(KeyEvent(KeyCode::Esc, ModKeys::empty()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -161,8 +161,10 @@ impl ViCmd {
|
|||||||
}
|
}
|
||||||
/// If a ViCmd has a linewise motion, but no verb, we change it to charwise
|
/// If a ViCmd has a linewise motion, but no verb, we change it to charwise
|
||||||
pub fn alter_line_motion_if_no_verb(&mut self) {
|
pub fn alter_line_motion_if_no_verb(&mut self) {
|
||||||
if self.is_line_motion() && self.verb.is_none()
|
if self.is_line_motion()
|
||||||
&& let Some(motion) = self.motion.as_mut() {
|
&& self.verb.is_none()
|
||||||
|
&& let Some(motion) = self.motion.as_mut()
|
||||||
|
{
|
||||||
match motion.1 {
|
match motion.1 {
|
||||||
Motion::LineUp => motion.1 = Motion::LineUpCharwise,
|
Motion::LineUp => motion.1 = Motion::LineUpCharwise,
|
||||||
Motion::LineDown => motion.1 = Motion::LineDownCharwise,
|
Motion::LineDown => motion.1 = Motion::LineDownCharwise,
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ impl ViNormal {
|
|||||||
return match obj {
|
return match obj {
|
||||||
TextObj::Sentence(_) | TextObj::Paragraph(_) => CmdState::Complete,
|
TextObj::Sentence(_) | TextObj::Paragraph(_) => CmdState::Complete,
|
||||||
_ => CmdState::Invalid,
|
_ => CmdState::Invalid,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
Some(_) => return CmdState::Complete,
|
Some(_) => return CmdState::Complete,
|
||||||
None => return CmdState::Pending,
|
None => return CmdState::Pending,
|
||||||
@@ -410,7 +410,7 @@ impl ViNormal {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'~' => {
|
'~' => {
|
||||||
chars_clone.next();
|
chars_clone.next();
|
||||||
@@ -445,7 +445,7 @@ impl ViNormal {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'x' => {
|
'x' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -454,7 +454,7 @@ impl ViNormal {
|
|||||||
motion: Some(MotionCmd(1, Motion::ForwardChar)),
|
motion: Some(MotionCmd(1, Motion::ForwardChar)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'X' => {
|
'X' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -463,7 +463,7 @@ impl ViNormal {
|
|||||||
motion: Some(MotionCmd(1, Motion::BackwardChar)),
|
motion: Some(MotionCmd(1, Motion::BackwardChar)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
's' => {
|
's' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -472,7 +472,7 @@ impl ViNormal {
|
|||||||
motion: Some(MotionCmd(1, Motion::ForwardChar)),
|
motion: Some(MotionCmd(1, Motion::ForwardChar)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'S' => {
|
'S' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -481,7 +481,7 @@ impl ViNormal {
|
|||||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'p' => {
|
'p' => {
|
||||||
chars = chars_clone;
|
chars = chars_clone;
|
||||||
@@ -516,7 +516,7 @@ impl ViNormal {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'~' => {
|
'~' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -525,7 +525,7 @@ impl ViNormal {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'u' => {
|
'u' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -534,7 +534,7 @@ impl ViNormal {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'v' => {
|
'v' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -543,7 +543,7 @@ impl ViNormal {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'V' => {
|
'V' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -552,7 +552,7 @@ impl ViNormal {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'o' => {
|
'o' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -561,7 +561,7 @@ impl ViNormal {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'O' => {
|
'O' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -570,7 +570,7 @@ impl ViNormal {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'a' => {
|
'a' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -579,7 +579,7 @@ impl ViNormal {
|
|||||||
motion: Some(MotionCmd(1, Motion::ForwardChar)),
|
motion: Some(MotionCmd(1, Motion::ForwardChar)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'A' => {
|
'A' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -588,7 +588,7 @@ impl ViNormal {
|
|||||||
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'i' => {
|
'i' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -597,7 +597,7 @@ impl ViNormal {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'I' => {
|
'I' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -606,7 +606,7 @@ impl ViNormal {
|
|||||||
motion: Some(MotionCmd(1, Motion::BeginningOfFirstWord)),
|
motion: Some(MotionCmd(1, Motion::BeginningOfFirstWord)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'J' => {
|
'J' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -615,7 +615,7 @@ impl ViNormal {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'y' => {
|
'y' => {
|
||||||
chars = chars_clone;
|
chars = chars_clone;
|
||||||
@@ -636,7 +636,7 @@ impl ViNormal {
|
|||||||
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'D' => {
|
'D' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -645,7 +645,7 @@ impl ViNormal {
|
|||||||
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'C' => {
|
'C' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -654,7 +654,7 @@ impl ViNormal {
|
|||||||
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'=' => {
|
'=' => {
|
||||||
chars = chars_clone;
|
chars = chars_clone;
|
||||||
@@ -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::WholeLine));
|
||||||
}
|
}
|
||||||
('W', Some(VerbCmd(_, Verb::Change))) => {
|
('W', Some(VerbCmd(_, Verb::Change))) => {
|
||||||
// Same with 'W'
|
// Same with 'W'
|
||||||
@@ -994,8 +994,7 @@ impl ViNormal {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if chars.peek().is_some() {
|
if chars.peek().is_some() {}
|
||||||
}
|
|
||||||
|
|
||||||
let verb_ref = verb.as_ref().map(|v| &v.1);
|
let verb_ref = verb.as_ref().map(|v| &v.1);
|
||||||
let motion_ref = motion.as_ref().map(|m| &m.1);
|
let motion_ref = motion.as_ref().map(|m| &m.1);
|
||||||
@@ -1185,7 +1184,7 @@ impl ViVisual {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'?' => {
|
'?' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -1194,7 +1193,7 @@ impl ViVisual {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
_ => break 'verb_parse None,
|
_ => break 'verb_parse None,
|
||||||
}
|
}
|
||||||
@@ -1209,7 +1208,7 @@ impl ViVisual {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'x' => {
|
'x' => {
|
||||||
chars = chars_clone;
|
chars = chars_clone;
|
||||||
@@ -1222,7 +1221,7 @@ impl ViVisual {
|
|||||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'Y' => {
|
'Y' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -1231,7 +1230,7 @@ impl ViVisual {
|
|||||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'D' => {
|
'D' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -1240,7 +1239,7 @@ impl ViVisual {
|
|||||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'R' | 'C' => {
|
'R' | 'C' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -1249,7 +1248,7 @@ impl ViVisual {
|
|||||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'>' => {
|
'>' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -1258,7 +1257,7 @@ impl ViVisual {
|
|||||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'<' => {
|
'<' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -1267,7 +1266,7 @@ impl ViVisual {
|
|||||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'=' => {
|
'=' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -1276,7 +1275,7 @@ impl ViVisual {
|
|||||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'p' | 'P' => {
|
'p' | 'P' => {
|
||||||
chars = chars_clone;
|
chars = chars_clone;
|
||||||
@@ -1299,7 +1298,7 @@ impl ViVisual {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'u' => {
|
'u' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -1308,7 +1307,7 @@ impl ViVisual {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'U' => {
|
'U' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -1317,7 +1316,7 @@ impl ViVisual {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'O' | 'o' => {
|
'O' | 'o' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -1326,7 +1325,7 @@ impl ViVisual {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'A' => {
|
'A' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -1335,7 +1334,7 @@ impl ViVisual {
|
|||||||
motion: Some(MotionCmd(1, Motion::ForwardChar)),
|
motion: Some(MotionCmd(1, Motion::ForwardChar)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'I' => {
|
'I' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -1344,7 +1343,7 @@ impl ViVisual {
|
|||||||
motion: Some(MotionCmd(1, Motion::BeginningOfLine)),
|
motion: Some(MotionCmd(1, Motion::BeginningOfLine)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'J' => {
|
'J' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -1353,7 +1352,7 @@ impl ViVisual {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'y' => {
|
'y' => {
|
||||||
chars = chars_clone;
|
chars = chars_clone;
|
||||||
@@ -1395,7 +1394,7 @@ impl ViVisual {
|
|||||||
| ('=', 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::WholeLine));
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -1652,8 +1651,7 @@ impl ViVisual {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if chars.peek().is_some() {
|
if chars.peek().is_some() {}
|
||||||
}
|
|
||||||
|
|
||||||
let verb_ref = verb.as_ref().map(|v| &v.1);
|
let verb_ref = verb.as_ref().map(|v| &v.1);
|
||||||
let motion_ref = motion.as_ref().map(|m| &m.1);
|
let motion_ref = motion.as_ref().map(|m| &m.1);
|
||||||
|
|||||||
10
src/shopt.rs
10
src/shopt.rs
@@ -117,7 +117,7 @@ impl ShOpts {
|
|||||||
Note::new("'shopt' takes arguments separated by periods to denote namespaces")
|
Note::new("'shopt' takes arguments separated by periods to denote namespaces")
|
||||||
.with_sub_notes(vec!["Example: 'shopt core.autocd=true'"]),
|
.with_sub_notes(vec!["Example: 'shopt core.autocd=true'"]),
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -263,7 +263,7 @@ impl ShOptCore {
|
|||||||
"max_recurse_depth",
|
"max_recurse_depth",
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -445,7 +445,9 @@ impl ShOptPrompt {
|
|||||||
ShErrKind::SyntaxErr,
|
ShErrKind::SyntaxErr,
|
||||||
format!("shopt: Unexpected 'prompt' option '{opt}'"),
|
format!("shopt: Unexpected 'prompt' option '{opt}'"),
|
||||||
)
|
)
|
||||||
.with_note(Note::new("options can be accessed like 'prompt.option_name'"))
|
.with_note(Note::new(
|
||||||
|
"options can be accessed like 'prompt.option_name'",
|
||||||
|
))
|
||||||
.with_note(
|
.with_note(
|
||||||
Note::new("'prompt' contains the following options").with_sub_notes(vec![
|
Note::new("'prompt' contains the following options").with_sub_notes(vec![
|
||||||
"trunc_prompt_path",
|
"trunc_prompt_path",
|
||||||
@@ -456,7 +458,7 @@ impl ShOptPrompt {
|
|||||||
"custom",
|
"custom",
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU64, Ordering};
|
|||||||
use nix::sys::signal::{SaFlags, SigAction, sigaction};
|
use nix::sys::signal::{SaFlags, SigAction, sigaction};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
builtin::trap::TrapTarget, jobs::{JobCmdFlags, JobID, take_term}, libsh::error::{ShErr, ShErrKind, ShResult}, parse::execute::exec_input, prelude::*, state::{read_jobs, read_logic, write_jobs, write_meta}
|
builtin::trap::TrapTarget,
|
||||||
|
jobs::{JobCmdFlags, JobID, take_term},
|
||||||
|
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||||
|
parse::execute::exec_input,
|
||||||
|
prelude::*,
|
||||||
|
state::{read_jobs, read_logic, write_jobs, write_meta},
|
||||||
};
|
};
|
||||||
|
|
||||||
static SIGNALS: AtomicU64 = AtomicU64::new(0);
|
static SIGNALS: AtomicU64 = AtomicU64::new(0);
|
||||||
@@ -12,7 +17,7 @@ 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 QUIT_CODE: AtomicI32 = AtomicI32::new(0);
|
pub static QUIT_CODE: AtomicI32 = AtomicI32::new(0);
|
||||||
|
|
||||||
const MISC_SIGNALS: [Signal;22] = [
|
const MISC_SIGNALS: [Signal; 22] = [
|
||||||
Signal::SIGILL,
|
Signal::SIGILL,
|
||||||
Signal::SIGTRAP,
|
Signal::SIGTRAP,
|
||||||
Signal::SIGABRT,
|
Signal::SIGABRT,
|
||||||
@@ -98,7 +103,6 @@ pub fn sig_setup() {
|
|||||||
|
|
||||||
let action = SigAction::new(SigHandler::Handler(handle_signal), flags, SigSet::empty());
|
let action = SigAction::new(SigHandler::Handler(handle_signal), flags, SigSet::empty());
|
||||||
|
|
||||||
|
|
||||||
let ignore = SigAction::new(SigHandler::SigIgn, flags, SigSet::empty());
|
let ignore = SigAction::new(SigHandler::SigIgn, flags, SigSet::empty());
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -269,8 +273,8 @@ pub fn child_exited(pid: Pid, status: WtStat) -> ShResult<()> {
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
}) && is_finished
|
||||||
&& is_finished {
|
{
|
||||||
if is_fg {
|
if is_fg {
|
||||||
take_term()?;
|
take_term()?;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
85
src/state.rs
85
src/state.rs
@@ -1,14 +1,25 @@
|
|||||||
use std::{
|
use std::{
|
||||||
cell::RefCell, collections::{HashMap, VecDeque}, fmt::Display, ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref}, str::FromStr, time::Duration
|
cell::RefCell,
|
||||||
|
collections::{HashMap, VecDeque},
|
||||||
|
fmt::Display,
|
||||||
|
ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref},
|
||||||
|
str::FromStr,
|
||||||
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use nix::unistd::{gethostname, getppid, User};
|
use nix::unistd::{User, gethostname, getppid};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
builtin::trap::TrapTarget, exec_input, jobs::JobTab, libsh::{
|
builtin::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::*, shopt::ShOpts
|
},
|
||||||
|
parse::{ConjunctNode, NdRule, Node, ParsedSrc},
|
||||||
|
prelude::*,
|
||||||
|
shopt::ShOpts,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Fern {
|
pub struct Fern {
|
||||||
@@ -49,7 +60,7 @@ pub enum ShellParam {
|
|||||||
Pos(usize),
|
Pos(usize),
|
||||||
AllArgs,
|
AllArgs,
|
||||||
AllArgsStr,
|
AllArgsStr,
|
||||||
ArgCount
|
ArgCount,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ShellParam {
|
impl ShellParam {
|
||||||
@@ -116,8 +127,12 @@ impl ScopeStack {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let mut new = Self::default();
|
let mut new = Self::default();
|
||||||
new.scopes.push(VarTab::new());
|
new.scopes.push(VarTab::new());
|
||||||
let shell_name = std::env::args().next().unwrap_or_else(|| "fern".to_string());
|
let shell_name = std::env::args()
|
||||||
new.global_params.insert(ShellParam::ShellName.to_string(), shell_name);
|
.next()
|
||||||
|
.unwrap_or_else(|| "fern".to_string());
|
||||||
|
new
|
||||||
|
.global_params
|
||||||
|
.insert(ShellParam::ShellName.to_string(), shell_name);
|
||||||
new
|
new
|
||||||
}
|
}
|
||||||
pub fn descend(&mut self, argv: Option<Vec<String>>) {
|
pub fn descend(&mut self, argv: Option<Vec<String>>) {
|
||||||
@@ -208,7 +223,9 @@ impl ScopeStack {
|
|||||||
std::env::var(var_name).unwrap_or_default()
|
std::env::var(var_name).unwrap_or_default()
|
||||||
}
|
}
|
||||||
pub fn get_param(&self, param: ShellParam) -> String {
|
pub fn get_param(&self, param: ShellParam) -> String {
|
||||||
if param.is_global() && let Some(val) = self.global_params.get(¶m.to_string()) {
|
if param.is_global()
|
||||||
|
&& let Some(val) = self.global_params.get(¶m.to_string())
|
||||||
|
{
|
||||||
return val.clone();
|
return val.clone();
|
||||||
}
|
}
|
||||||
for scope in self.scopes.iter().rev() {
|
for scope in self.scopes.iter().rev() {
|
||||||
@@ -224,16 +241,12 @@ impl ScopeStack {
|
|||||||
/// Therefore, these are global state and we use the global scope
|
/// Therefore, these are global state and we use the global scope
|
||||||
pub fn set_param(&mut self, param: ShellParam, val: &str) {
|
pub fn set_param(&mut self, param: ShellParam, val: &str) {
|
||||||
match param {
|
match param {
|
||||||
ShellParam::ShPid |
|
ShellParam::ShPid | ShellParam::Status | ShellParam::LastJob | ShellParam::ShellName => {
|
||||||
ShellParam::Status |
|
self
|
||||||
ShellParam::LastJob |
|
.global_params
|
||||||
ShellParam::ShellName => {
|
.insert(param.to_string(), val.to_string());
|
||||||
self.global_params.insert(param.to_string(), val.to_string());
|
|
||||||
}
|
}
|
||||||
ShellParam::Pos(_) |
|
ShellParam::Pos(_) | ShellParam::AllArgs | ShellParam::AllArgsStr | ShellParam::ArgCount => {
|
||||||
ShellParam::AllArgs |
|
|
||||||
ShellParam::AllArgsStr |
|
|
||||||
ShellParam::ArgCount => {
|
|
||||||
if let Some(scope) = self.scopes.first_mut() {
|
if let Some(scope) = self.scopes.first_mut() {
|
||||||
scope.set_param(param, val);
|
scope.set_param(param, val);
|
||||||
}
|
}
|
||||||
@@ -339,10 +352,10 @@ impl LogTab {
|
|||||||
pub struct VarFlags(u8);
|
pub struct VarFlags(u8);
|
||||||
|
|
||||||
impl VarFlags {
|
impl VarFlags {
|
||||||
pub const NONE : Self = Self(0);
|
pub const NONE: Self = Self(0);
|
||||||
pub const EXPORT : Self = Self(1 << 0);
|
pub const EXPORT: Self = Self(1 << 0);
|
||||||
pub const LOCAL : Self = Self(1 << 1);
|
pub const LOCAL: Self = Self(1 << 1);
|
||||||
pub const READONLY : Self = Self(1 << 2);
|
pub const READONLY: Self = Self(1 << 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BitOr for VarFlags {
|
impl BitOr for VarFlags {
|
||||||
@@ -426,7 +439,7 @@ impl Display for VarKind {
|
|||||||
VarKind::AssocArr(items) => {
|
VarKind::AssocArr(items) => {
|
||||||
let mut item_iter = items.iter().peekable();
|
let mut item_iter = items.iter().peekable();
|
||||||
while let Some(item) = item_iter.next() {
|
while let Some(item) = item_iter.next() {
|
||||||
let (k,v) = item;
|
let (k, v) = item;
|
||||||
write!(f, "{k}={v}")?;
|
write!(f, "{k}={v}")?;
|
||||||
if item_iter.peek().is_some() {
|
if item_iter.peek().is_some() {
|
||||||
write!(f, " ")?;
|
write!(f, " ")?;
|
||||||
@@ -446,10 +459,7 @@ pub struct Var {
|
|||||||
|
|
||||||
impl Var {
|
impl Var {
|
||||||
pub fn new(kind: VarKind, flags: VarFlags) -> Self {
|
pub fn new(kind: VarKind, flags: VarFlags) -> Self {
|
||||||
Self {
|
Self { flags, kind }
|
||||||
flags,
|
|
||||||
kind
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
pub fn kind(&self) -> &VarKind {
|
pub fn kind(&self) -> &VarKind {
|
||||||
&self.kind
|
&self.kind
|
||||||
@@ -575,7 +585,10 @@ impl VarTab {
|
|||||||
self.bpush_arg(env::current_exe().unwrap().to_str().unwrap().to_string());
|
self.bpush_arg(env::current_exe().unwrap().to_str().unwrap().to_string());
|
||||||
}
|
}
|
||||||
fn update_arg_params(&mut self) {
|
fn update_arg_params(&mut self) {
|
||||||
self.set_param(ShellParam::AllArgs, &self.sh_argv.clone().to_vec()[1..].join(" "));
|
self.set_param(
|
||||||
|
ShellParam::AllArgs,
|
||||||
|
&self.sh_argv.clone().to_vec()[1..].join(" "),
|
||||||
|
);
|
||||||
self.set_param(ShellParam::ArgCount, &(self.sh_argv.len() - 1).to_string());
|
self.set_param(ShellParam::ArgCount, &(self.sh_argv.len() - 1).to_string());
|
||||||
}
|
}
|
||||||
/// Push an arg to the front of the arg deque
|
/// Push an arg to the front of the arg deque
|
||||||
@@ -664,20 +677,16 @@ impl VarTab {
|
|||||||
}
|
}
|
||||||
pub fn get_param(&self, param: ShellParam) -> String {
|
pub fn get_param(&self, param: ShellParam) -> String {
|
||||||
match param {
|
match param {
|
||||||
ShellParam::Pos(n) => {
|
ShellParam::Pos(n) => self
|
||||||
self
|
|
||||||
.sh_argv()
|
.sh_argv()
|
||||||
.get(n)
|
.get(n)
|
||||||
.map(|s| s.to_string())
|
.map(|s| s.to_string())
|
||||||
.unwrap_or_default()
|
.unwrap_or_default(),
|
||||||
}
|
ShellParam::Status => self
|
||||||
ShellParam::Status => {
|
|
||||||
self
|
|
||||||
.params
|
.params
|
||||||
.get(&ShellParam::Status)
|
.get(&ShellParam::Status)
|
||||||
.map(|s| s.to_string())
|
.map(|s| s.to_string())
|
||||||
.unwrap_or("0".into())
|
.unwrap_or("0".into()),
|
||||||
}
|
|
||||||
_ => self
|
_ => self
|
||||||
.params
|
.params
|
||||||
.get(¶m)
|
.get(¶m)
|
||||||
@@ -695,7 +704,7 @@ pub struct MetaTab {
|
|||||||
runtime_stop: Option<Instant>,
|
runtime_stop: Option<Instant>,
|
||||||
|
|
||||||
// pending system messages
|
// pending system messages
|
||||||
system_msg: Vec<String>
|
system_msg: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MetaTab {
|
impl MetaTab {
|
||||||
@@ -788,7 +797,9 @@ pub fn get_shopt(path: &str) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_status() -> i32 {
|
pub fn get_status() -> i32 {
|
||||||
read_vars(|v| v.get_param(ShellParam::Status)).parse::<i32>().unwrap()
|
read_vars(|v| v.get_param(ShellParam::Status))
|
||||||
|
.parse::<i32>()
|
||||||
|
.unwrap()
|
||||||
}
|
}
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn set_status(code: i32) {
|
pub fn set_status(code: i32) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use tempfile::TempDir;
|
|||||||
|
|
||||||
use crate::prompt::readline::complete::Completer;
|
use crate::prompt::readline::complete::Completer;
|
||||||
use crate::prompt::readline::markers;
|
use crate::prompt::readline::markers;
|
||||||
use crate::state::{write_logic, write_vars, VarFlags};
|
use crate::state::{VarFlags, write_logic, write_vars};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
@@ -77,8 +77,9 @@ fn complete_command_builtin() {
|
|||||||
assert!(completer.candidates.iter().any(|c| c == "export"));
|
assert!(completer.candidates.iter().any(|c| c == "export"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: Disabled - ShFunc constructor requires parsed AST which is complex to set up in tests
|
// NOTE: Disabled - ShFunc constructor requires parsed AST which is complex to
|
||||||
// TODO: Re-enable once we have a helper to create test functions
|
// set up in tests TODO: Re-enable once we have a helper to create test
|
||||||
|
// functions
|
||||||
/*
|
/*
|
||||||
#[test]
|
#[test]
|
||||||
fn complete_command_function() {
|
fn complete_command_function() {
|
||||||
@@ -191,7 +192,12 @@ fn complete_filename_with_slash() {
|
|||||||
|
|
||||||
// Should complete files in subdir/
|
// Should complete files in subdir/
|
||||||
if result.is_some() {
|
if result.is_some() {
|
||||||
assert!(completer.candidates.iter().any(|c| c.contains("nested.txt")));
|
assert!(
|
||||||
|
completer
|
||||||
|
.candidates
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.contains("nested.txt"))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,11 +328,17 @@ fn context_detection_command_position() {
|
|||||||
|
|
||||||
// At the beginning - command context
|
// At the beginning - command context
|
||||||
let (ctx, _) = completer.get_completion_context("ech", 3);
|
let (ctx, _) = completer.get_completion_context("ech", 3);
|
||||||
assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context at start");
|
assert!(
|
||||||
|
ctx.last() == Some(&markers::COMMAND),
|
||||||
|
"Should be in command context at start"
|
||||||
|
);
|
||||||
|
|
||||||
// After whitespace - still command if no command yet
|
// After whitespace - still command if no command yet
|
||||||
let (ctx, _) = completer.get_completion_context(" ech", 5);
|
let (ctx, _) = completer.get_completion_context(" ech", 5);
|
||||||
assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context after whitespace");
|
assert!(
|
||||||
|
ctx.last() == Some(&markers::COMMAND),
|
||||||
|
"Should be in command context after whitespace"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -335,10 +347,16 @@ fn context_detection_argument_position() {
|
|||||||
|
|
||||||
// After a complete command - argument context
|
// After a complete command - argument context
|
||||||
let (ctx, _) = completer.get_completion_context("echo hello", 10);
|
let (ctx, _) = completer.get_completion_context("echo hello", 10);
|
||||||
assert!(ctx.last() != Some(&markers::COMMAND), "Should be in argument context after command");
|
assert!(
|
||||||
|
ctx.last() != Some(&markers::COMMAND),
|
||||||
|
"Should be in argument context after command"
|
||||||
|
);
|
||||||
|
|
||||||
let (ctx, _) = completer.get_completion_context("ls -la /tmp", 11);
|
let (ctx, _) = completer.get_completion_context("ls -la /tmp", 11);
|
||||||
assert!(ctx.last() != Some(&markers::COMMAND), "Should be in argument context");
|
assert!(
|
||||||
|
ctx.last() != Some(&markers::COMMAND),
|
||||||
|
"Should be in argument context"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -347,11 +365,17 @@ fn context_detection_nested_command_sub() {
|
|||||||
|
|
||||||
// Inside $() - should be command context
|
// Inside $() - should be command context
|
||||||
let (ctx, _) = completer.get_completion_context("echo \"$(ech", 11);
|
let (ctx, _) = completer.get_completion_context("echo \"$(ech", 11);
|
||||||
assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context inside $()");
|
assert!(
|
||||||
|
ctx.last() == Some(&markers::COMMAND),
|
||||||
|
"Should be in command context inside $()"
|
||||||
|
);
|
||||||
|
|
||||||
// After command in $() - argument context
|
// After command in $() - argument context
|
||||||
let (ctx, _) = completer.get_completion_context("echo \"$(echo hell", 17);
|
let (ctx, _) = completer.get_completion_context("echo \"$(echo hell", 17);
|
||||||
assert!(ctx.last() != Some(&markers::COMMAND), "Should be in argument context inside $()");
|
assert!(
|
||||||
|
ctx.last() != Some(&markers::COMMAND),
|
||||||
|
"Should be in argument context inside $()"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -360,7 +384,10 @@ fn context_detection_pipe() {
|
|||||||
|
|
||||||
// After pipe - command context
|
// After pipe - command context
|
||||||
let (ctx, _) = completer.get_completion_context("ls | gre", 8);
|
let (ctx, _) = completer.get_completion_context("ls | gre", 8);
|
||||||
assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context after pipe");
|
assert!(
|
||||||
|
ctx.last() == Some(&markers::COMMAND),
|
||||||
|
"Should be in command context after pipe"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -369,11 +396,17 @@ fn context_detection_command_sep() {
|
|||||||
|
|
||||||
// After semicolon - command context
|
// After semicolon - command context
|
||||||
let (ctx, _) = completer.get_completion_context("echo foo; l", 11);
|
let (ctx, _) = completer.get_completion_context("echo foo; l", 11);
|
||||||
assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context after semicolon");
|
assert!(
|
||||||
|
ctx.last() == Some(&markers::COMMAND),
|
||||||
|
"Should be in command context after semicolon"
|
||||||
|
);
|
||||||
|
|
||||||
// After && - command context
|
// After && - command context
|
||||||
let (ctx, _) = completer.get_completion_context("true && l", 9);
|
let (ctx, _) = completer.get_completion_context("true && l", 9);
|
||||||
assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context after &&");
|
assert!(
|
||||||
|
ctx.last() == Some(&markers::COMMAND),
|
||||||
|
"Should be in command context after &&"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -382,11 +415,19 @@ fn context_detection_variable_substitution() {
|
|||||||
|
|
||||||
// $VAR at argument position - VAR_SUB should take priority over ARG
|
// $VAR at argument position - VAR_SUB should take priority over ARG
|
||||||
let (ctx, _) = completer.get_completion_context("echo $HOM", 9);
|
let (ctx, _) = completer.get_completion_context("echo $HOM", 9);
|
||||||
assert_eq!(ctx.last(), Some(&markers::VAR_SUB), "Should be in var_sub context for $HOM");
|
assert_eq!(
|
||||||
|
ctx.last(),
|
||||||
|
Some(&markers::VAR_SUB),
|
||||||
|
"Should be in var_sub context for $HOM"
|
||||||
|
);
|
||||||
|
|
||||||
// $VAR at command position - VAR_SUB should take priority over COMMAND
|
// $VAR at command position - VAR_SUB should take priority over COMMAND
|
||||||
let (ctx, _) = completer.get_completion_context("$HOM", 4);
|
let (ctx, _) = completer.get_completion_context("$HOM", 4);
|
||||||
assert_eq!(ctx.last(), Some(&markers::VAR_SUB), "Should be in var_sub context for bare $HOM");
|
assert_eq!(
|
||||||
|
ctx.last(),
|
||||||
|
Some(&markers::VAR_SUB),
|
||||||
|
"Should be in var_sub context for bare $HOM"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -395,7 +436,11 @@ fn context_detection_variable_in_double_quotes() {
|
|||||||
|
|
||||||
// $VAR inside double quotes
|
// $VAR inside double quotes
|
||||||
let (ctx, _) = completer.get_completion_context("echo \"$HOM", 10);
|
let (ctx, _) = completer.get_completion_context("echo \"$HOM", 10);
|
||||||
assert_eq!(ctx.last(), Some(&markers::VAR_SUB), "Should be in var_sub context inside double quotes");
|
assert_eq!(
|
||||||
|
ctx.last(),
|
||||||
|
Some(&markers::VAR_SUB),
|
||||||
|
"Should be in var_sub context inside double quotes"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -404,7 +449,11 @@ fn context_detection_stack_base_is_null() {
|
|||||||
|
|
||||||
// Empty input - only NULL on the stack
|
// Empty input - only NULL on the stack
|
||||||
let (ctx, _) = completer.get_completion_context("", 0);
|
let (ctx, _) = completer.get_completion_context("", 0);
|
||||||
assert_eq!(ctx, vec![markers::NULL], "Empty input should only have NULL marker");
|
assert_eq!(
|
||||||
|
ctx,
|
||||||
|
vec![markers::NULL],
|
||||||
|
"Empty input should only have NULL marker"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -431,11 +480,19 @@ fn context_detection_priority_ordering() {
|
|||||||
// COMMAND (priority 2) should override ARG (priority 1)
|
// COMMAND (priority 2) should override ARG (priority 1)
|
||||||
// After a pipe, the next token is a command even though it looks like an arg
|
// After a pipe, the next token is a command even though it looks like an arg
|
||||||
let (ctx, _) = completer.get_completion_context("echo foo | gr", 13);
|
let (ctx, _) = completer.get_completion_context("echo foo | gr", 13);
|
||||||
assert_eq!(ctx.last(), Some(&markers::COMMAND), "COMMAND should win over ARG after pipe");
|
assert_eq!(
|
||||||
|
ctx.last(),
|
||||||
|
Some(&markers::COMMAND),
|
||||||
|
"COMMAND should win over ARG after pipe"
|
||||||
|
);
|
||||||
|
|
||||||
// VAR_SUB (priority 3) should override COMMAND (priority 2)
|
// VAR_SUB (priority 3) should override COMMAND (priority 2)
|
||||||
let (ctx, _) = completer.get_completion_context("$PA", 3);
|
let (ctx, _) = completer.get_completion_context("$PA", 3);
|
||||||
assert_eq!(ctx.last(), Some(&markers::VAR_SUB), "VAR_SUB should win over COMMAND");
|
assert_eq!(
|
||||||
|
ctx.last(),
|
||||||
|
Some(&markers::VAR_SUB),
|
||||||
|
"VAR_SUB should win over COMMAND"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -647,7 +704,12 @@ fn complete_special_characters_in_filename() {
|
|||||||
|
|
||||||
if result.is_some() {
|
if result.is_some() {
|
||||||
// Should handle special chars in filenames
|
// Should handle special chars in filenames
|
||||||
assert!(completer.candidates.iter().any(|c| c.contains("file-with-dash") || c.contains("file_with_underscore")));
|
assert!(
|
||||||
|
completer
|
||||||
|
.candidates
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.contains("file-with-dash") || c.contains("file_with_underscore"))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use crate::expand::{perform_param_expansion, DUB_QUOTE, VAR_SUB};
|
use crate::expand::{DUB_QUOTE, VAR_SUB, perform_param_expansion};
|
||||||
use crate::state::VarFlags;
|
use crate::state::VarFlags;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -295,7 +295,10 @@ fn param_expansion_replacesuffix() {
|
|||||||
fn dquote_escape_dollar() {
|
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!(!result.contains(VAR_SUB), "Escaped $ should not become VAR_SUB");
|
assert!(
|
||||||
|
!result.contains(VAR_SUB),
|
||||||
|
"Escaped $ should not become VAR_SUB"
|
||||||
|
);
|
||||||
assert!(result.contains('$'), "Literal $ should be preserved");
|
assert!(result.contains('$'), "Literal $ should be preserved");
|
||||||
assert!(!result.contains('\\'), "Backslash should be stripped");
|
assert!(!result.contains('\\'), "Backslash should be stripped");
|
||||||
}
|
}
|
||||||
@@ -304,47 +307,54 @@ 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()
|
let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
|
||||||
.filter(|&c| c != DUB_QUOTE)
|
assert_eq!(
|
||||||
.collect();
|
inner, "\\",
|
||||||
assert_eq!(inner, "\\", "Double backslash should produce single backslash");
|
"Double backslash should produce single backslash"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
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()
|
let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
|
||||||
.filter(|&c| c != DUB_QUOTE)
|
assert!(
|
||||||
.collect();
|
inner.contains('"'),
|
||||||
assert!(inner.contains('"'), "Escaped quote should produce literal quote");
|
"Escaped quote should produce literal quote"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
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()
|
let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
|
||||||
.filter(|&c| c != DUB_QUOTE)
|
assert_eq!(
|
||||||
.collect();
|
inner, "`",
|
||||||
assert_eq!(inner, "`", "Escaped backtick should produce literal backtick");
|
"Escaped backtick should produce literal backtick"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
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()
|
let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
|
||||||
.filter(|&c| c != DUB_QUOTE)
|
assert_eq!(
|
||||||
.collect();
|
inner, "\\a",
|
||||||
assert_eq!(inner, "\\a", "Backslash before non-special char should be preserved");
|
"Backslash before non-special char should be preserved"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dquote_unescaped_dollar_expands() {
|
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!(result.contains(VAR_SUB), "Unescaped $ should become VAR_SUB");
|
assert!(
|
||||||
|
result.contains(VAR_SUB),
|
||||||
|
"Unescaped $ should become VAR_SUB"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -354,9 +364,7 @@ fn dquote_mixed_escapes() {
|
|||||||
assert!(!result.contains(VAR_SUB), "Escaped $ should not expand");
|
assert!(!result.contains(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()
|
let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
|
||||||
.filter(|&c| c != 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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use crate::prompt::readline::{
|
use crate::prompt::readline::{
|
||||||
annotate_input, annotate_input_recursive, markers,
|
annotate_input, annotate_input_recursive, highlight::Highlighter, markers,
|
||||||
highlight::Highlighter,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -17,7 +16,10 @@ fn find_marker(annotated: &str, marker: char) -> Option<usize> {
|
|||||||
|
|
||||||
/// Helper to check if markers appear in the correct order
|
/// Helper to check if markers appear in the correct order
|
||||||
fn marker_before(annotated: &str, first: char, second: char) -> bool {
|
fn marker_before(annotated: &str, first: char, second: char) -> bool {
|
||||||
if let (Some(pos1), Some(pos2)) = (find_marker(annotated, first), find_marker(annotated, second)) {
|
if let (Some(pos1), Some(pos2)) = (
|
||||||
|
find_marker(annotated, first),
|
||||||
|
find_marker(annotated, second),
|
||||||
|
) {
|
||||||
pos1 < pos2
|
pos1 < pos2
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
@@ -51,7 +53,8 @@ fn annotate_builtin_command() {
|
|||||||
// Should mark "export" as BUILTIN
|
// Should mark "export" as BUILTIN
|
||||||
assert!(has_marker(&annotated, markers::BUILTIN));
|
assert!(has_marker(&annotated, markers::BUILTIN));
|
||||||
|
|
||||||
// Should mark assignment (or ARG if assignment isn't specifically marked separately)
|
// Should mark assignment (or ARG if assignment isn't specifically marked
|
||||||
|
// separately)
|
||||||
assert!(has_marker(&annotated, markers::ASSIGNMENT) || has_marker(&annotated, markers::ARG));
|
assert!(has_marker(&annotated, markers::ASSIGNMENT) || has_marker(&annotated, markers::ARG));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +152,11 @@ fn annotate_variable_in_string() {
|
|||||||
assert!(has_marker(&annotated, markers::VAR_SUB));
|
assert!(has_marker(&annotated, markers::VAR_SUB));
|
||||||
|
|
||||||
// VAR_SUB should be inside STRING_DQ
|
// VAR_SUB should be inside STRING_DQ
|
||||||
assert!(marker_before(&annotated, markers::STRING_DQ, markers::VAR_SUB));
|
assert!(marker_before(
|
||||||
|
&annotated,
|
||||||
|
markers::STRING_DQ,
|
||||||
|
markers::VAR_SUB
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -239,7 +246,10 @@ fn annotate_recursive_nested_command_sub() {
|
|||||||
|
|
||||||
// Should have multiple CMD_SUB markers (nested)
|
// Should have multiple CMD_SUB markers (nested)
|
||||||
let cmd_sub_count = annotated.chars().filter(|&c| c == markers::CMD_SUB).count();
|
let cmd_sub_count = annotated.chars().filter(|&c| c == markers::CMD_SUB).count();
|
||||||
assert!(cmd_sub_count >= 2, "Should have at least 2 CMD_SUB markers for nested substitutions");
|
assert!(
|
||||||
|
cmd_sub_count >= 2,
|
||||||
|
"Should have at least 2 CMD_SUB markers for nested substitutions"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -251,7 +261,10 @@ fn annotate_recursive_command_sub_with_args() {
|
|||||||
// Just check that we have command-type markers
|
// Just check that we have command-type markers
|
||||||
let builtin_count = annotated.chars().filter(|&c| c == markers::BUILTIN).count();
|
let builtin_count = annotated.chars().filter(|&c| c == markers::BUILTIN).count();
|
||||||
let command_count = annotated.chars().filter(|&c| c == markers::COMMAND).count();
|
let command_count = annotated.chars().filter(|&c| c == markers::COMMAND).count();
|
||||||
assert!(builtin_count + command_count >= 2, "Expected at least 2 command markers (BUILTIN or COMMAND)");
|
assert!(
|
||||||
|
builtin_count + command_count >= 2,
|
||||||
|
"Expected at least 2 command markers (BUILTIN or COMMAND)"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -297,7 +310,10 @@ fn annotate_recursive_deeply_nested() {
|
|||||||
let annotated = annotate_input_recursive(input);
|
let annotated = annotate_input_recursive(input);
|
||||||
|
|
||||||
// Should have multiple STRING_DQ and CMD_SUB markers
|
// Should have multiple STRING_DQ and CMD_SUB markers
|
||||||
let string_count = annotated.chars().filter(|&c| c == markers::STRING_DQ).count();
|
let string_count = annotated
|
||||||
|
.chars()
|
||||||
|
.filter(|&c| c == markers::STRING_DQ)
|
||||||
|
.count();
|
||||||
let cmd_sub_count = annotated.chars().filter(|&c| c == markers::CMD_SUB).count();
|
let cmd_sub_count = annotated.chars().filter(|&c| c == markers::CMD_SUB).count();
|
||||||
|
|
||||||
assert!(string_count >= 2, "Should have multiple STRING_DQ markers");
|
assert!(string_count >= 2, "Should have multiple STRING_DQ markers");
|
||||||
@@ -314,7 +330,11 @@ fn marker_priority_var_in_string() {
|
|||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// STRING_DQ should come before VAR_SUB (outer before inner)
|
// STRING_DQ should come before VAR_SUB (outer before inner)
|
||||||
assert!(marker_before(&annotated, markers::STRING_DQ, markers::VAR_SUB));
|
assert!(marker_before(
|
||||||
|
&annotated,
|
||||||
|
markers::STRING_DQ,
|
||||||
|
markers::VAR_SUB
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -351,7 +371,10 @@ fn highlighter_produces_ansi_codes() {
|
|||||||
let output = highlighter.take();
|
let output = highlighter.take();
|
||||||
|
|
||||||
// Should contain ANSI escape codes
|
// Should contain ANSI escape codes
|
||||||
assert!(output.contains("\x1b["), "Output should contain ANSI escape sequences");
|
assert!(
|
||||||
|
output.contains("\x1b["),
|
||||||
|
"Output should contain ANSI escape sequences"
|
||||||
|
);
|
||||||
|
|
||||||
// Should still contain the original text
|
// Should still contain the original text
|
||||||
assert!(output.contains("echo"));
|
assert!(output.contains("echo"));
|
||||||
@@ -401,7 +424,8 @@ fn highlighter_preserves_text_content() {
|
|||||||
let output = highlighter.take();
|
let output = highlighter.take();
|
||||||
|
|
||||||
// Remove ANSI codes to check text content
|
// Remove ANSI codes to check text content
|
||||||
let text_only: String = output.chars()
|
let text_only: String = output
|
||||||
|
.chars()
|
||||||
.filter(|c| !c.is_control() && *c != '\x1b')
|
.filter(|c| !c.is_control() && *c != '\x1b')
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -518,7 +542,11 @@ fn annotate_special_variables() {
|
|||||||
|
|
||||||
// Should mark positional parameters
|
// Should mark positional parameters
|
||||||
let var_count = annotated.chars().filter(|&c| c == markers::VAR_SUB).count();
|
let var_count = annotated.chars().filter(|&c| c == markers::VAR_SUB).count();
|
||||||
assert!(var_count >= 5, "Expected at least 5 VAR_SUB markers, found {}", var_count);
|
assert!(
|
||||||
|
var_count >= 5,
|
||||||
|
"Expected at least 5 VAR_SUB markers, found {}",
|
||||||
|
var_count
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -539,7 +567,10 @@ fn annotate_complex_pipeline() {
|
|||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// Should have multiple OPERATOR markers for pipes
|
// Should have multiple OPERATOR markers for pipes
|
||||||
let operator_count = annotated.chars().filter(|&c| c == markers::OPERATOR).count();
|
let operator_count = annotated
|
||||||
|
.chars()
|
||||||
|
.filter(|&c| c == markers::OPERATOR)
|
||||||
|
.count();
|
||||||
assert!(operator_count >= 4);
|
assert!(operator_count >= 4);
|
||||||
|
|
||||||
// Should have multiple COMMAND markers
|
// Should have multiple COMMAND markers
|
||||||
@@ -577,7 +608,10 @@ fn annotate_multiple_redirects() {
|
|||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// Should have multiple REDIRECT markers
|
// Should have multiple REDIRECT markers
|
||||||
let redirect_count = annotated.chars().filter(|&c| c == markers::REDIRECT).count();
|
let redirect_count = annotated
|
||||||
|
.chars()
|
||||||
|
.filter(|&c| c == markers::REDIRECT)
|
||||||
|
.count();
|
||||||
assert!(redirect_count >= 2);
|
assert!(redirect_count >= 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ use super::*;
|
|||||||
use crate::expand::{expand_aliases, unescape_str};
|
use crate::expand::{expand_aliases, unescape_str};
|
||||||
use crate::libsh::error::{Note, ShErr, ShErrKind};
|
use crate::libsh::error::{Note, ShErr, ShErrKind};
|
||||||
use crate::parse::{
|
use crate::parse::{
|
||||||
|
NdRule, Node, ParseStream,
|
||||||
lex::{LexFlags, LexStream, Tk, TkRule},
|
lex::{LexFlags, LexStream, Tk, TkRule},
|
||||||
node_operation, NdRule, Node, ParseStream,
|
node_operation,
|
||||||
};
|
};
|
||||||
use crate::state::{write_logic, write_vars};
|
use crate::state::{write_logic, write_vars};
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
libsh::{error::ShErr, term::{Style, Styled}},
|
libsh::{
|
||||||
|
error::ShErr,
|
||||||
|
term::{Style, Styled},
|
||||||
|
},
|
||||||
prompt::readline::{
|
prompt::readline::{
|
||||||
|
FernVi,
|
||||||
history::History,
|
history::History,
|
||||||
keys::{KeyCode, KeyEvent, ModKeys},
|
keys::{KeyCode, KeyEvent, ModKeys},
|
||||||
linebuf::LineBuf,
|
linebuf::LineBuf,
|
||||||
term::{raw_mode, KeyReader, LineWriter},
|
term::{KeyReader, LineWriter, raw_mode},
|
||||||
vimode::{ViInsert, ViMode, ViNormal},
|
vimode::{ViInsert, ViMode, ViNormal},
|
||||||
FernVi,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -173,8 +176,9 @@ impl LineWriter for TestWriter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: FernVi structure has changed significantly and readline() method no longer exists
|
// NOTE: FernVi structure has changed significantly and readline() method no
|
||||||
// These test helpers are disabled until they can be properly updated
|
// longer exists These test helpers are disabled until they can be properly
|
||||||
|
// updated
|
||||||
/*
|
/*
|
||||||
impl FernVi {
|
impl FernVi {
|
||||||
pub fn new_test(prompt: Option<String>, input: &str, initial: &str) -> Self {
|
pub fn new_test(prompt: Option<String>, input: &str, initial: &str) -> Self {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::parse::{
|
use crate::parse::{
|
||||||
|
NdRule, Node, ParseStream, Redir, RedirType,
|
||||||
lex::{LexFlags, LexStream},
|
lex::{LexFlags, LexStream},
|
||||||
Node, NdRule, ParseStream, RedirType, Redir,
|
|
||||||
};
|
};
|
||||||
use crate::procio::{IoFrame, IoMode, IoStack};
|
use crate::procio::{IoFrame, IoMode, IoStack};
|
||||||
|
|
||||||
@@ -16,9 +16,7 @@ fn parse_command(input: &str) -> Node {
|
|||||||
.flatten()
|
.flatten()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let mut nodes = ParseStream::new(tokens)
|
let mut nodes = ParseStream::new(tokens).flatten().collect::<Vec<_>>();
|
||||||
.flatten()
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
assert_eq!(nodes.len(), 1, "Expected exactly one node");
|
assert_eq!(nodes.len(), 1, "Expected exactly one node");
|
||||||
let top_node = nodes.remove(0);
|
let top_node = nodes.remove(0);
|
||||||
@@ -27,24 +25,41 @@ fn parse_command(input: &str) -> Node {
|
|||||||
// Structure is typically: Conjunction -> Pipeline -> Command
|
// Structure is typically: Conjunction -> Pipeline -> Command
|
||||||
match top_node.class {
|
match top_node.class {
|
||||||
NdRule::Conjunction { elements } => {
|
NdRule::Conjunction { elements } => {
|
||||||
let first_element = elements.into_iter().next().expect("Expected at least one conjunction element");
|
let first_element = elements
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.expect("Expected at least one conjunction element");
|
||||||
match first_element.cmd.class {
|
match first_element.cmd.class {
|
||||||
NdRule::Pipeline { cmds, .. } => {
|
NdRule::Pipeline { cmds, .. } => {
|
||||||
let mut commands = cmds;
|
let mut commands = cmds;
|
||||||
assert_eq!(commands.len(), 1, "Expected exactly one command in pipeline");
|
assert_eq!(
|
||||||
|
commands.len(),
|
||||||
|
1,
|
||||||
|
"Expected exactly one command in pipeline"
|
||||||
|
);
|
||||||
commands.remove(0)
|
commands.remove(0)
|
||||||
}
|
}
|
||||||
NdRule::Command { .. } => *first_element.cmd,
|
NdRule::Command { .. } => *first_element.cmd,
|
||||||
_ => panic!("Expected Command or Pipeline node, got {:?}", first_element.cmd.class),
|
_ => panic!(
|
||||||
|
"Expected Command or Pipeline node, got {:?}",
|
||||||
|
first_element.cmd.class
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NdRule::Pipeline { cmds, .. } => {
|
NdRule::Pipeline { cmds, .. } => {
|
||||||
let mut commands = cmds;
|
let mut commands = cmds;
|
||||||
assert_eq!(commands.len(), 1, "Expected exactly one command in pipeline");
|
assert_eq!(
|
||||||
|
commands.len(),
|
||||||
|
1,
|
||||||
|
"Expected exactly one command in pipeline"
|
||||||
|
);
|
||||||
commands.remove(0)
|
commands.remove(0)
|
||||||
}
|
}
|
||||||
NdRule::Command { .. } => top_node,
|
NdRule::Command { .. } => top_node,
|
||||||
_ => panic!("Expected Conjunction, Pipeline, or Command node, got {:?}", top_node.class),
|
_ => panic!(
|
||||||
|
"Expected Conjunction, Pipeline, or Command node, got {:?}",
|
||||||
|
top_node.class
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +114,13 @@ fn parse_stderr_to_stdout() {
|
|||||||
assert_eq!(node.redirs.len(), 1);
|
assert_eq!(node.redirs.len(), 1);
|
||||||
let redir = &node.redirs[0];
|
let redir = &node.redirs[0];
|
||||||
|
|
||||||
assert!(matches!(redir.io_mode, IoMode::Fd { tgt_fd: 2, src_fd: 1 }));
|
assert!(matches!(
|
||||||
|
redir.io_mode,
|
||||||
|
IoMode::Fd {
|
||||||
|
tgt_fd: 2,
|
||||||
|
src_fd: 1
|
||||||
|
}
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -109,7 +130,13 @@ fn parse_stdout_to_stderr() {
|
|||||||
assert_eq!(node.redirs.len(), 1);
|
assert_eq!(node.redirs.len(), 1);
|
||||||
let redir = &node.redirs[0];
|
let redir = &node.redirs[0];
|
||||||
|
|
||||||
assert!(matches!(redir.io_mode, IoMode::Fd { tgt_fd: 1, src_fd: 2 }));
|
assert!(matches!(
|
||||||
|
redir.io_mode,
|
||||||
|
IoMode::Fd {
|
||||||
|
tgt_fd: 1,
|
||||||
|
src_fd: 2
|
||||||
|
}
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -120,15 +147,24 @@ fn parse_multiple_redirects() {
|
|||||||
|
|
||||||
// Input redirect
|
// Input redirect
|
||||||
assert!(matches!(node.redirs[0].class, RedirType::Input));
|
assert!(matches!(node.redirs[0].class, RedirType::Input));
|
||||||
assert!(matches!(node.redirs[0].io_mode, IoMode::File { tgt_fd: 0, .. }));
|
assert!(matches!(
|
||||||
|
node.redirs[0].io_mode,
|
||||||
|
IoMode::File { tgt_fd: 0, .. }
|
||||||
|
));
|
||||||
|
|
||||||
// Stdout redirect
|
// Stdout redirect
|
||||||
assert!(matches!(node.redirs[1].class, RedirType::Output));
|
assert!(matches!(node.redirs[1].class, RedirType::Output));
|
||||||
assert!(matches!(node.redirs[1].io_mode, IoMode::File { tgt_fd: 1, .. }));
|
assert!(matches!(
|
||||||
|
node.redirs[1].io_mode,
|
||||||
|
IoMode::File { tgt_fd: 1, .. }
|
||||||
|
));
|
||||||
|
|
||||||
// Stderr redirect
|
// Stderr redirect
|
||||||
assert!(matches!(node.redirs[2].class, RedirType::Output));
|
assert!(matches!(node.redirs[2].class, RedirType::Output));
|
||||||
assert!(matches!(node.redirs[2].io_mode, IoMode::File { tgt_fd: 2, .. }));
|
assert!(matches!(
|
||||||
|
node.redirs[2].io_mode,
|
||||||
|
IoMode::File { tgt_fd: 2, .. }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -149,7 +185,13 @@ fn parse_custom_fd_dup() {
|
|||||||
assert_eq!(node.redirs.len(), 1);
|
assert_eq!(node.redirs.len(), 1);
|
||||||
let redir = &node.redirs[0];
|
let redir = &node.redirs[0];
|
||||||
|
|
||||||
assert!(matches!(redir.io_mode, IoMode::Fd { tgt_fd: 3, src_fd: 4 }));
|
assert!(matches!(
|
||||||
|
redir.io_mode,
|
||||||
|
IoMode::Fd {
|
||||||
|
tgt_fd: 3,
|
||||||
|
src_fd: 4
|
||||||
|
}
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -187,11 +229,20 @@ fn parse_redirect_order_preserved() {
|
|||||||
assert_eq!(node.redirs.len(), 2);
|
assert_eq!(node.redirs.len(), 2);
|
||||||
|
|
||||||
// First redirect: 2>&1
|
// First redirect: 2>&1
|
||||||
assert!(matches!(node.redirs[0].io_mode, IoMode::Fd { tgt_fd: 2, src_fd: 1 }));
|
assert!(matches!(
|
||||||
|
node.redirs[0].io_mode,
|
||||||
|
IoMode::Fd {
|
||||||
|
tgt_fd: 2,
|
||||||
|
src_fd: 1
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
// Second redirect: > file.txt
|
// Second redirect: > file.txt
|
||||||
assert!(matches!(node.redirs[1].class, RedirType::Output));
|
assert!(matches!(node.redirs[1].class, RedirType::Output));
|
||||||
assert!(matches!(node.redirs[1].io_mode, IoMode::File { tgt_fd: 1, .. }));
|
assert!(matches!(
|
||||||
|
node.redirs[1].io_mode,
|
||||||
|
IoMode::File { tgt_fd: 1, .. }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -241,10 +292,7 @@ fn iostack_never_empties() {
|
|||||||
fn iostack_push_to_frame() {
|
fn iostack_push_to_frame() {
|
||||||
let mut stack = IoStack::new();
|
let mut stack = IoStack::new();
|
||||||
|
|
||||||
let redir = crate::parse::Redir::new(
|
let redir = crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output);
|
||||||
IoMode::fd(1, 2),
|
|
||||||
RedirType::Output,
|
|
||||||
);
|
|
||||||
|
|
||||||
stack.push_to_frame(redir);
|
stack.push_to_frame(redir);
|
||||||
assert_eq!(stack.curr_frame().len(), 1);
|
assert_eq!(stack.curr_frame().len(), 1);
|
||||||
@@ -299,7 +347,10 @@ fn iostack_flatten() {
|
|||||||
|
|
||||||
// Push new frame with redir
|
// Push new frame with redir
|
||||||
let mut frame2 = IoFrame::new();
|
let mut frame2 = IoFrame::new();
|
||||||
frame2.push(crate::parse::Redir::new(IoMode::fd(2, 1), RedirType::Output));
|
frame2.push(crate::parse::Redir::new(
|
||||||
|
IoMode::fd(2, 1),
|
||||||
|
RedirType::Output,
|
||||||
|
));
|
||||||
stack.push_frame(frame2);
|
stack.push_frame(frame2);
|
||||||
|
|
||||||
// Push third frame with redir
|
// Push third frame with redir
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ fn scopestack_new() {
|
|||||||
let stack = ScopeStack::new();
|
let stack = ScopeStack::new();
|
||||||
|
|
||||||
// Should start with one global scope
|
// Should start with one global scope
|
||||||
assert!(stack.var_exists("PATH") || !stack.var_exists("PATH")); // Just check it doesn't panic
|
assert!(stack.var_exists("PATH") || !stack.var_exists("PATH")); // Just check
|
||||||
|
// it doesn't
|
||||||
|
// panic
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -59,7 +61,11 @@ fn scopestack_variable_shadowing() {
|
|||||||
stack.ascend();
|
stack.ascend();
|
||||||
|
|
||||||
// Global should be restored
|
// Global should be restored
|
||||||
assert_eq!(stack.get_var("VAR"), "global", "Global should be unchanged after ascend");
|
assert_eq!(
|
||||||
|
stack.get_var("VAR"),
|
||||||
|
"global",
|
||||||
|
"Global should be unchanged after ascend"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -147,9 +153,13 @@ fn scopestack_descend_with_args() {
|
|||||||
|
|
||||||
// In local scope, positional params come from the VarTab created during descend
|
// In local scope, positional params come from the VarTab created during descend
|
||||||
// VarTab::new() initializes with process args, then our args are appended
|
// VarTab::new() initializes with process args, then our args are appended
|
||||||
// So we check that SOME positional parameter exists (implementation detail may vary)
|
// So we check that SOME positional parameter exists (implementation detail may
|
||||||
|
// vary)
|
||||||
let local_param = stack.get_param(ShellParam::Pos(1));
|
let local_param = stack.get_param(ShellParam::Pos(1));
|
||||||
assert!(!local_param.is_empty(), "Should have positional parameters in local scope");
|
assert!(
|
||||||
|
!local_param.is_empty(),
|
||||||
|
"Should have positional parameters in local scope"
|
||||||
|
);
|
||||||
|
|
||||||
// Ascend back
|
// Ascend back
|
||||||
stack.ascend();
|
stack.ascend();
|
||||||
@@ -239,13 +249,19 @@ fn scopestack_var_exists() {
|
|||||||
assert!(stack.var_exists("EXISTS"));
|
assert!(stack.var_exists("EXISTS"));
|
||||||
|
|
||||||
stack.descend(None);
|
stack.descend(None);
|
||||||
assert!(stack.var_exists("EXISTS"), "Global var should be visible in local scope");
|
assert!(
|
||||||
|
stack.var_exists("EXISTS"),
|
||||||
|
"Global var should be visible in local scope"
|
||||||
|
);
|
||||||
|
|
||||||
stack.set_var("LOCAL", "yes", VarFlags::LOCAL);
|
stack.set_var("LOCAL", "yes", VarFlags::LOCAL);
|
||||||
assert!(stack.var_exists("LOCAL"));
|
assert!(stack.var_exists("LOCAL"));
|
||||||
|
|
||||||
stack.ascend();
|
stack.ascend();
|
||||||
assert!(!stack.var_exists("LOCAL"), "Local var should not exist after ascend");
|
assert!(
|
||||||
|
!stack.var_exists("LOCAL"),
|
||||||
|
"Local var should not exist after ascend"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -333,11 +349,14 @@ fn logtab_multiple_aliases() {
|
|||||||
assert_eq!(logtab.aliases().len(), 3);
|
assert_eq!(logtab.aliases().len(), 3);
|
||||||
assert_eq!(logtab.get_alias("ll"), Some("ls -la".to_string()));
|
assert_eq!(logtab.get_alias("ll"), Some("ls -la".to_string()));
|
||||||
assert_eq!(logtab.get_alias("la"), Some("ls -A".to_string()));
|
assert_eq!(logtab.get_alias("la"), Some("ls -A".to_string()));
|
||||||
assert_eq!(logtab.get_alias("grep"), Some("grep --color=auto".to_string()));
|
assert_eq!(
|
||||||
|
logtab.get_alias("grep"),
|
||||||
|
Some("grep --color=auto".to_string())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: Function tests are limited because ShFunc requires complex setup (parsed AST)
|
// Note: Function tests are limited because ShFunc requires complex setup
|
||||||
// We'll test the basic storage/retrieval mechanics
|
// (parsed AST) We'll test the basic storage/retrieval mechanics
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn logtab_funcs_empty_initially() {
|
fn logtab_funcs_empty_initially() {
|
||||||
@@ -427,7 +446,10 @@ fn vartab_positional_params() {
|
|||||||
// Now sh_argv should be: [exe_path, test_arg1, test_arg2]
|
// Now sh_argv should be: [exe_path, test_arg1, test_arg2]
|
||||||
// Pos(0) = exe_path, Pos(1) = test_arg1, Pos(2) = test_arg2
|
// Pos(0) = exe_path, Pos(1) = test_arg1, Pos(2) = test_arg2
|
||||||
let final_len = vartab.sh_argv().len();
|
let final_len = vartab.sh_argv().len();
|
||||||
assert!(final_len > initial_len || final_len >= 1, "Should have arguments");
|
assert!(
|
||||||
|
final_len > initial_len || final_len >= 1,
|
||||||
|
"Should have arguments"
|
||||||
|
);
|
||||||
|
|
||||||
// Just verify we can retrieve the last args we pushed
|
// Just verify we can retrieve the last args we pushed
|
||||||
let last_idx = final_len - 1;
|
let last_idx = final_len - 1;
|
||||||
@@ -517,13 +539,34 @@ fn shellparam_is_global() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn shellparam_from_str() {
|
fn shellparam_from_str() {
|
||||||
assert!(matches!("?".parse::<ShellParam>().unwrap(), ShellParam::Status));
|
assert!(matches!(
|
||||||
assert!(matches!("$".parse::<ShellParam>().unwrap(), ShellParam::ShPid));
|
"?".parse::<ShellParam>().unwrap(),
|
||||||
assert!(matches!("!".parse::<ShellParam>().unwrap(), ShellParam::LastJob));
|
ShellParam::Status
|
||||||
assert!(matches!("0".parse::<ShellParam>().unwrap(), ShellParam::ShellName));
|
));
|
||||||
assert!(matches!("@".parse::<ShellParam>().unwrap(), ShellParam::AllArgs));
|
assert!(matches!(
|
||||||
assert!(matches!("*".parse::<ShellParam>().unwrap(), ShellParam::AllArgsStr));
|
"$".parse::<ShellParam>().unwrap(),
|
||||||
assert!(matches!("#".parse::<ShellParam>().unwrap(), ShellParam::ArgCount));
|
ShellParam::ShPid
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
"!".parse::<ShellParam>().unwrap(),
|
||||||
|
ShellParam::LastJob
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
"0".parse::<ShellParam>().unwrap(),
|
||||||
|
ShellParam::ShellName
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
"@".parse::<ShellParam>().unwrap(),
|
||||||
|
ShellParam::AllArgs
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
"*".parse::<ShellParam>().unwrap(),
|
||||||
|
ShellParam::AllArgsStr
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
"#".parse::<ShellParam>().unwrap(),
|
||||||
|
ShellParam::ArgCount
|
||||||
|
));
|
||||||
|
|
||||||
match "1".parse::<ShellParam>().unwrap() {
|
match "1".parse::<ShellParam>().unwrap() {
|
||||||
ShellParam::Pos(n) => assert_eq!(n, 1),
|
ShellParam::Pos(n) => assert_eq!(n, 1),
|
||||||
|
|||||||
Reference in New Issue
Block a user