Early work on tab completion
This commit is contained in:
@@ -1,6 +1,4 @@
|
||||
use std::os::fd::AsRawFd;
|
||||
|
||||
use crate::{expand::{arithmetic::expand_arith_string, tilde::expand_tilde_string, vars::{expand_string, expand_var}}, prelude::*};
|
||||
use crate::{expand::{arithmetic::expand_arith_string, tilde::expand_tilde_string, vars::expand_string}, prelude::*};
|
||||
use shellenv::jobs::{ChildProc, JobBldr};
|
||||
|
||||
pub mod shellcmd;
|
||||
@@ -23,6 +21,7 @@ pub fn exec_input<S: Into<String>>(input: S, shenv: &mut ShEnv) -> ShResult<()>
|
||||
|
||||
let parse_time = std::time::Instant::now();
|
||||
let syn_tree = Parser::new(token_stream,shenv).parse()?;
|
||||
log!(TRACE,syn_tree);
|
||||
log!(INFO, "Parsing done in {:?}", parse_time.elapsed());
|
||||
if !shenv.ctx().flags().contains(ExecFlags::IN_FUNC) {
|
||||
shenv.save_io()?;
|
||||
@@ -79,8 +78,6 @@ fn exec_list(list: Vec<(Option<CmdGuard>, Node)>, shenv: &mut ShEnv) -> ShResult
|
||||
while let Some(cmd_info) = list.fpop() {
|
||||
let guard = cmd_info.0;
|
||||
let cmd = cmd_info.1;
|
||||
let span = cmd.span();
|
||||
let cmd_raw = cmd.as_raw(shenv);
|
||||
|
||||
if let Some(guard) = guard {
|
||||
let code = shenv.get_code();
|
||||
@@ -122,7 +119,9 @@ fn dispatch_command(mut node: Node, shenv: &mut ShEnv) -> ShResult<()> {
|
||||
let mut is_subsh = false;
|
||||
let mut is_assign = false;
|
||||
if let NdRule::Command { ref mut argv, redirs: _ } = node.rule_mut() {
|
||||
if !shenv.ctx().flags().contains(ExecFlags::NO_EXPAND) {
|
||||
*argv = expand_argv(argv.to_vec(), shenv)?;
|
||||
}
|
||||
let cmd = argv.first().unwrap().as_raw(shenv);
|
||||
if shenv.logic().get_function(&cmd).is_some() {
|
||||
is_func = true;
|
||||
@@ -130,7 +129,9 @@ fn dispatch_command(mut node: Node, shenv: &mut ShEnv) -> ShResult<()> {
|
||||
is_builtin = true;
|
||||
}
|
||||
} else if let NdRule::Subshell { body: _, ref mut argv, redirs: _ } = node.rule_mut() {
|
||||
if !shenv.ctx().flags().contains(ExecFlags::NO_EXPAND) {
|
||||
*argv = expand_argv(argv.to_vec(), shenv)?;
|
||||
}
|
||||
is_subsh = true;
|
||||
} else if let NdRule::Assignment { assignments: _, cmd: _ } = node.rule() {
|
||||
is_assign = true;
|
||||
@@ -508,5 +509,6 @@ fn prep_execve(argv: Vec<Token>, shenv: &mut ShEnv) -> (Vec<String>, Vec<String>
|
||||
envp.push(formatted);
|
||||
}
|
||||
log!(TRACE, argv_s);
|
||||
log!(DEBUG, argv_s);
|
||||
(argv_s, envp)
|
||||
}
|
||||
|
||||
@@ -48,6 +48,15 @@ pub fn expand_aliases(tokens: Vec<Token>, shenv: &mut ShEnv) -> Vec<Token> {
|
||||
is_command = true;
|
||||
processed.push(token.clone());
|
||||
}
|
||||
TkRule::Case | TkRule::For => {
|
||||
processed.push(token.clone());
|
||||
while let Some(token) = stream.next() {
|
||||
processed.push(token.clone());
|
||||
if token.rule() == TkRule::Sep {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
TkRule::Ident if is_command => {
|
||||
is_command = false;
|
||||
let mut alias_tokens = expand_alias(token.clone(), shenv);
|
||||
|
||||
1
src/filefilefile.txt
Normal file
1
src/filefilefile.txt
Normal file
@@ -0,0 +1 @@
|
||||
foobar
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{fmt::Display, os::fd::AsRawFd};
|
||||
use std::{fmt::Display, os::{fd::AsRawFd, unix::fs::PermissionsExt}};
|
||||
|
||||
use nix::sys::termios;
|
||||
|
||||
@@ -6,10 +6,42 @@ use crate::prelude::*;
|
||||
|
||||
pub const SIG_EXIT_OFFSET: i32 = 128;
|
||||
|
||||
pub fn get_path_cmds() -> ShResult<Vec<String>> {
|
||||
let mut cmds = vec![];
|
||||
let path_var = std::env::var("PATH")?;
|
||||
let paths = path_var.split(':');
|
||||
|
||||
for path in paths {
|
||||
let path = PathBuf::from(&path);
|
||||
if path.is_dir() {
|
||||
let path_files = std::fs::read_dir(&path)?;
|
||||
for file in path_files {
|
||||
let file_path = file?.path();
|
||||
if file_path.is_file() {
|
||||
if let Ok(meta) = std::fs::metadata(&file_path) {
|
||||
let perms = meta.permissions();
|
||||
if perms.mode() & 0o111 != 0 {
|
||||
let file_name = file_path.file_name().unwrap();
|
||||
cmds.push(file_name.to_str().unwrap().to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(cmds)
|
||||
}
|
||||
|
||||
pub fn get_bin_path(command: &str, shenv: &ShEnv) -> Option<PathBuf> {
|
||||
let env = shenv.vars().env();
|
||||
let path_var = env.get("PATH")?;
|
||||
let mut paths = path_var.split(':');
|
||||
|
||||
let script_check = PathBuf::from(command);
|
||||
if script_check.is_file() {
|
||||
return Some(script_check)
|
||||
}
|
||||
while let Some(raw_path) = paths.next() {
|
||||
let mut path = PathBuf::from(raw_path);
|
||||
path.push(command);
|
||||
|
||||
68
src/main.rs
68
src/main.rs
@@ -1,4 +1,4 @@
|
||||
#![allow(unused_unsafe)]
|
||||
#![allow(static_mut_refs,unused_unsafe)]
|
||||
|
||||
pub mod libsh;
|
||||
pub mod shellenv;
|
||||
@@ -20,6 +20,14 @@ use crate::prelude::*;
|
||||
|
||||
pub static mut SAVED_TERMIOS: Option<Option<Termios>> = None;
|
||||
|
||||
bitflags! {
|
||||
pub struct FernFlags: u32 {
|
||||
const NO_RC = 0b000001;
|
||||
const NO_HIST = 0b000010;
|
||||
const INTERACTIVE = 0b000100;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save_termios() {
|
||||
unsafe {
|
||||
SAVED_TERMIOS = Some(if isatty(std::io::stdin().as_raw_fd()).unwrap() {
|
||||
@@ -34,6 +42,8 @@ pub fn save_termios() {
|
||||
}
|
||||
pub fn get_saved_termios() -> Option<Termios> {
|
||||
unsafe {
|
||||
// This is only used when the shell exits so it's fine
|
||||
// SAVED_TERMIOS is only mutated once at the start as well
|
||||
SAVED_TERMIOS.clone().flatten()
|
||||
}
|
||||
}
|
||||
@@ -44,22 +54,68 @@ fn set_termios() {
|
||||
termios::tcsetattr(std::io::stdin(), nix::sys::termios::SetArg::TCSANOW, &termios).unwrap();
|
||||
}
|
||||
}
|
||||
fn parse_args(shenv: &mut ShEnv) {
|
||||
let mut args = std::env::args().skip(1);
|
||||
let mut script_path: Option<PathBuf> = None;
|
||||
let mut command: Option<String> = None;
|
||||
let mut flags = FernFlags::empty();
|
||||
|
||||
log!(DEBUG, args);
|
||||
while let Some(mut arg) = args.next() {
|
||||
log!(DEBUG, arg);
|
||||
if arg.starts_with("--") {
|
||||
arg = arg.strip_prefix("--").unwrap().to_string();
|
||||
match arg.as_str() {
|
||||
"no-rc" => flags |= FernFlags::NO_RC,
|
||||
"no-hist" => flags |= FernFlags::NO_HIST,
|
||||
_ => eprintln!("Warning - Unrecognized option: {arg}")
|
||||
}
|
||||
} else if arg.starts_with('-') {
|
||||
arg = arg.strip_prefix('-').unwrap().to_string();
|
||||
match arg.as_str() {
|
||||
"c" => command = args.next(),
|
||||
_ => eprintln!("Warning - Unrecognized option: {arg}")
|
||||
}
|
||||
} else {
|
||||
let path_check = PathBuf::from(&arg);
|
||||
if path_check.is_file() {
|
||||
script_path = Some(path_check);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !flags.contains(FernFlags::NO_RC) {
|
||||
let _ = shenv.source_rc().eprint();
|
||||
}
|
||||
|
||||
if let Some(cmd) = command {
|
||||
let input = clean_string(cmd);
|
||||
let _ = exec_input(input, shenv).eprint();
|
||||
|
||||
} else if let Some(script) = script_path {
|
||||
let _ = shenv.source_file(script).eprint();
|
||||
|
||||
} else {
|
||||
interactive(shenv);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main() {
|
||||
sig_setup();
|
||||
save_termios();
|
||||
set_termios();
|
||||
let mut shenv = ShEnv::new();
|
||||
if let Err(e) = shenv.source_rc() {
|
||||
eprintln!("Error sourcing rc file: {}", e.to_string());
|
||||
}
|
||||
|
||||
parse_args(&mut shenv);
|
||||
}
|
||||
|
||||
fn interactive(shenv: &mut ShEnv) {
|
||||
loop {
|
||||
log!(TRACE, "Entered loop");
|
||||
match prompt::read_line(&mut shenv) {
|
||||
match prompt::read_line(shenv) {
|
||||
Ok(line) => {
|
||||
shenv.meta_mut().start_timer();
|
||||
let _ = exec_input(line, &mut shenv).eprint();
|
||||
let _ = exec_input(line, shenv).eprint();
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("{}",e);
|
||||
|
||||
@@ -71,7 +71,7 @@ impl<'a> Lexer<'a> {
|
||||
rule = TkRule::Ident
|
||||
|
||||
// If we are in a command right now, after this we are in arguments
|
||||
} else if self.is_command && rule != TkRule::Whitespace && !KEYWORDS.contains(&rule) {
|
||||
} else if self.is_command && !matches!(rule, TkRule::Comment | TkRule::Whitespace) && !KEYWORDS.contains(&rule) {
|
||||
self.is_command = false;
|
||||
}
|
||||
// If we see a separator like && or ;, we are now in a command again
|
||||
@@ -295,7 +295,7 @@ impl TkRule {
|
||||
}
|
||||
|
||||
tkrule_def!(Comment, |input: &str| {
|
||||
let mut chars = input.chars();
|
||||
let mut chars = input.chars().peekable();
|
||||
let mut len = 0;
|
||||
|
||||
if let Some('#') = chars.next() {
|
||||
@@ -304,6 +304,14 @@ tkrule_def!(Comment, |input: &str| {
|
||||
let chlen = ch.len_utf8();
|
||||
len += chlen;
|
||||
if ch == '\n' {
|
||||
while let Some(ch) = chars.peek() {
|
||||
if *ch == '\n' {
|
||||
len += 1;
|
||||
chars.next();
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -743,31 +751,53 @@ tkrule_def!(SQuote, |input: &str| {
|
||||
// Double quoted strings
|
||||
let mut chars = input.chars();
|
||||
let mut len = 0;
|
||||
let mut quoted = false;
|
||||
let mut quote_count = 0;
|
||||
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
'\\' => {
|
||||
chars.next();
|
||||
len += 2;
|
||||
}
|
||||
'\'' if !quoted => {
|
||||
len += 1;
|
||||
if let Some(ch) = chars.next() {
|
||||
let chlen = ch.len_utf8();
|
||||
len += chlen;
|
||||
quoted = true;
|
||||
}
|
||||
'\'' if quoted => {
|
||||
}
|
||||
'\'' => {
|
||||
let chlen = ch.len_utf8();
|
||||
len += chlen;
|
||||
quote_count += 1;
|
||||
}
|
||||
' ' | '\t' | ';' | '\n' if quote_count % 2 == 0 => {
|
||||
if quote_count > 0 {
|
||||
if quote_count % 2 == 0 {
|
||||
return Some(len)
|
||||
}
|
||||
_ if !quoted => {
|
||||
} else {
|
||||
return None
|
||||
}
|
||||
} else {
|
||||
return None
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let chlen = ch.len_utf8();
|
||||
len += chlen;
|
||||
}
|
||||
}
|
||||
}
|
||||
match len {
|
||||
0 => None,
|
||||
_ => {
|
||||
if quote_count > 0 {
|
||||
if quote_count % 2 == 0 {
|
||||
return Some(len)
|
||||
} else {
|
||||
return None
|
||||
}
|
||||
} else {
|
||||
return None
|
||||
}
|
||||
_ => len += 1
|
||||
}
|
||||
}
|
||||
None
|
||||
});
|
||||
|
||||
tkrule_def!(DQuote, |input: &str| {
|
||||
|
||||
@@ -158,18 +158,18 @@ impl<'a> Parser<'a> {
|
||||
|
||||
pub fn parse(mut self) -> ShResult<SynTree> {
|
||||
log!(TRACE, "Starting parse");
|
||||
let mut lists = VecDeque::new();
|
||||
let mut lists = vec![];
|
||||
let token_slice = &*self.token_stream;
|
||||
// Get the Main rule
|
||||
if let Some(mut node) = Main::try_match(token_slice,self.shenv)? {
|
||||
// Extract the inner lists
|
||||
if let NdRule::Main { ref mut cmd_lists } = node.rule_mut() {
|
||||
while let Some(node) = cmd_lists.pop() {
|
||||
lists.bpush(node)
|
||||
lists.push(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
while let Some(node) = lists.bpop() {
|
||||
while let Some(node) = lists.pop() {
|
||||
// Push inner command lists to self.ast
|
||||
self.ast.push_node(node);
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@ pub use crate::{
|
||||
},
|
||||
sys::{
|
||||
self,
|
||||
get_path_cmds,
|
||||
get_bin_path,
|
||||
sh_quit,
|
||||
read_to_string,
|
||||
|
||||
86
src/prompt/comp.rs
Normal file
86
src/prompt/comp.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use rustyline::completion::{Candidate, Completer};
|
||||
|
||||
use crate::{expand::cmdsub::expand_cmdsub_string, parse::lex::KEYWORDS, prelude::*};
|
||||
|
||||
use super::readline::SynHelper;
|
||||
|
||||
impl<'a> Completer for SynHelper<'a> {
|
||||
type Candidate = String;
|
||||
fn complete( &self, line: &str, pos: usize, ctx: &rustyline::Context<'_>,) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
|
||||
let mut shenv = self.shenv.clone();
|
||||
let mut comps = vec![];
|
||||
shenv.new_input(line);
|
||||
let mut token_stream = Lexer::new(line.to_string(), &mut shenv).lex();
|
||||
if let Some(comp_token) = token_stream.pop() {
|
||||
let raw = comp_token.as_raw(&mut shenv);
|
||||
let is_cmd = if let Some(token) = token_stream.pop() {
|
||||
match token.rule() {
|
||||
TkRule::Sep => true,
|
||||
_ if KEYWORDS.contains(&token.rule()) => true,
|
||||
_ => false
|
||||
}
|
||||
} else {
|
||||
true
|
||||
};
|
||||
if let TkRule::Ident | TkRule::Whitespace = comp_token.rule() {
|
||||
if is_cmd {
|
||||
let cmds = shenv.meta().path_cmds();
|
||||
comps.extend(cmds.iter().map(|cmd| cmd.to_string()));
|
||||
comps.retain(|cmd| cmd.starts_with(&raw));
|
||||
if !comps.is_empty() && comps.len() > 1 {
|
||||
if get_bin_path("fzf", &self.shenv).is_some() {
|
||||
if let Some(mut selection) = fzf_comp(&comps, &mut shenv) {
|
||||
while selection.starts_with(&raw) {
|
||||
selection = selection.strip_prefix(&raw).unwrap().to_string();
|
||||
}
|
||||
comps = vec![selection];
|
||||
}
|
||||
}
|
||||
} else if let Some(mut comp) = comps.pop() {
|
||||
while comp.starts_with(&raw) {
|
||||
comp = comp.strip_prefix(&raw).unwrap().to_string();
|
||||
}
|
||||
comps = vec![comp];
|
||||
}
|
||||
return Ok((pos,comps))
|
||||
} else {
|
||||
let (start, matches) = self.file_comp.complete(line, pos, ctx)?;
|
||||
comps.extend(matches.iter().map(|c| c.display().to_string()));
|
||||
|
||||
if !comps.is_empty() && comps.len() > 1 {
|
||||
if get_bin_path("fzf", &self.shenv).is_some() {
|
||||
if let Some(selection) = fzf_comp(&comps, &mut shenv) {
|
||||
return Ok((start, vec![selection]))
|
||||
} else {
|
||||
return Ok((start, comps))
|
||||
}
|
||||
} else {
|
||||
return Ok((start, comps))
|
||||
}
|
||||
} else if let Some(comp) = comps.pop() {
|
||||
// Slice off the already typed bit
|
||||
return Ok((start, vec![comp]))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((pos,comps))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fzf_comp(comps: &[String], shenv: &mut ShEnv) -> Option<String> {
|
||||
// All of the fzf wrapper libraries suck
|
||||
// So we gotta do this now
|
||||
let echo_args = comps.join("\n");
|
||||
let echo = format!("echo \"{echo_args}\"");
|
||||
let fzf = "fzf --height=~30% --layout=reverse --border --border-label=completion";
|
||||
let command = format!("{echo} | {fzf}");
|
||||
|
||||
shenv.ctx_mut().set_flag(ExecFlags::NO_EXPAND); // Prevent any pesky shell injections with filenames like '$(rm -rf /)'
|
||||
let selection = expand_cmdsub_string(&command, shenv).ok()?;
|
||||
if selection.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(selection.trim().to_string())
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ use rustyline::{config::Configurer, history::{DefaultHistory, History}, ColorMod
|
||||
pub mod readline;
|
||||
pub mod highlight;
|
||||
pub mod validate;
|
||||
pub mod comp;
|
||||
|
||||
fn init_rl<'a>(shenv: &'a mut ShEnv) -> Editor<SynHelper<'a>, DefaultHistory> {
|
||||
let hist_path = std::env::var("FERN_HIST").unwrap_or_default();
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use rustyline::{completion::{Completer, FilenameCompleter}, hint::{Hint, Hinter}, history::{History, SearchDirection}, Helper};
|
||||
use rustyline::{completion::{Candidate, Completer, FilenameCompleter}, hint::{Hint, Hinter}, history::{History, SearchDirection}, Helper};
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
pub struct SynHelper<'a> {
|
||||
file_comp: FilenameCompleter,
|
||||
pub file_comp: FilenameCompleter,
|
||||
pub shenv: &'a mut ShEnv,
|
||||
pub commands: Vec<String>
|
||||
}
|
||||
|
||||
impl<'a> Helper for SynHelper<'a> {}
|
||||
@@ -15,7 +14,6 @@ impl<'a> SynHelper<'a> {
|
||||
Self {
|
||||
file_comp: FilenameCompleter::new(),
|
||||
shenv,
|
||||
commands: vec![]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,12 +33,6 @@ impl<'a> SynHelper<'a> {
|
||||
|
||||
|
||||
|
||||
impl<'a> Completer for SynHelper<'a> {
|
||||
type Candidate = String;
|
||||
fn complete( &self, line: &str, pos: usize, ctx: &rustyline::Context<'_>,) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
|
||||
Ok((0,vec![]))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SynHint {
|
||||
text: String,
|
||||
|
||||
@@ -3,8 +3,9 @@ use crate::prelude::*;
|
||||
bitflags! {
|
||||
#[derive(Copy,Clone,Debug,PartialEq,PartialOrd)]
|
||||
pub struct ExecFlags: u32 {
|
||||
const NO_FORK = 0x00000001;
|
||||
const IN_FUNC = 0x00000010;
|
||||
const NO_FORK = 0b00000001;
|
||||
const IN_FUNC = 0b00000010;
|
||||
const NO_EXPAND = 0b00000100;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,13 +5,16 @@ use crate::prelude::*;
|
||||
pub struct MetaTab {
|
||||
timer_start: Instant,
|
||||
last_runtime: Option<Duration>,
|
||||
path_cmds: Vec<String> // Used for command completion
|
||||
}
|
||||
|
||||
impl MetaTab {
|
||||
pub fn new() -> Self {
|
||||
let path_cmds = get_path_cmds().unwrap_or_default();
|
||||
Self {
|
||||
timer_start: Instant::now(),
|
||||
last_runtime: None,
|
||||
path_cmds
|
||||
}
|
||||
}
|
||||
pub fn start_timer(&mut self) {
|
||||
@@ -23,4 +26,7 @@ impl MetaTab {
|
||||
pub fn get_runtime(&self) -> Option<Duration> {
|
||||
self.last_runtime
|
||||
}
|
||||
pub fn path_cmds(&self) -> &[String] {
|
||||
&self.path_cmds
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user