From d49542575db8f931a4492d0b149bad028b4b68ca Mon Sep 17 00:00:00 2001 From: Aqua-sama Date: Mon, 30 Nov 2020 12:25:56 +0200 Subject: Add autocompleter --- Cargo.toml | 6 +- man/rs.1.scd | 3 + src/commandline.rs | 6 +- src/commandline/builtins.rs | 7 +- src/commandline/command.rs | 14 ++-- src/completer.rs | 160 ++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 21 +++--- 7 files changed, 196 insertions(+), 21 deletions(-) create mode 100644 src/completer.rs diff --git a/Cargo.toml b/Cargo.toml index e455a3c..f894eda 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,8 @@ edition = "2018" # https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +nix = "0.19.1" libc = "0.2" -rustyline = "6.3.0" - +rustyline = "7.0.0" +rustyline-derive = "0.4.0" +radix_trie = "0.2" diff --git a/man/rs.1.scd b/man/rs.1.scd index c9c423e..e5e24f7 100644 --- a/man/rs.1.scd +++ b/man/rs.1.scd @@ -51,6 +51,9 @@ is used by default. - {$} is replaced by '#' for root and '$' for other users - {PWD} is replaced by current directory path +## Autocompletion +Executables in $PATH are added to autocompletion. + # AUTHORS Maintained by Aqua . The source code can be found at https://neueland.iserlohn-fortress.net/cgit/rshell/. diff --git a/src/commandline.rs b/src/commandline.rs index 489eaa8..0737b75 100644 --- a/src/commandline.rs +++ b/src/commandline.rs @@ -17,7 +17,11 @@ impl CommandLine { CommandLine(split(line)) } - pub fn run(&self,home: &PathBuf, mut status: Option) -> Result, Error> { + pub fn run( + &self, + home: &PathBuf, + mut status: Option, + ) -> Result, Error> { for cmd in &self.0 { if cmd.when.can_run(&status) { match cmd.run(&home, &status)? { diff --git a/src/commandline/builtins.rs b/src/commandline/builtins.rs index c5999f2..882a690 100644 --- a/src/commandline/builtins.rs +++ b/src/commandline/builtins.rs @@ -1,13 +1,10 @@ +use super::command::RunResult; use std::io::{Error, ErrorKind::InvalidInput}; use std::path::{Path, PathBuf}; -use super::command::RunResult; pub(in crate::commandline) fn cd(args: &[String], home: &PathBuf) -> Result { if args.len() > 1 { - return Err(Error::new( - InvalidInput, - "Too many arguments passed to cd", - )); + return Err(Error::new(InvalidInput, "Too many arguments passed to cd")); } let root = if args.len() == 0 { diff --git a/src/commandline/command.rs b/src/commandline/command.rs index d00f420..c8b326d 100644 --- a/src/commandline/command.rs +++ b/src/commandline/command.rs @@ -1,8 +1,8 @@ -use std::io::{Error}; -use std::path::{PathBuf}; -use std::process::ExitStatus; -use std::process::Command as Process; use super::builtins::{cd, set, unset}; +use std::io::Error; +use std::path::PathBuf; +use std::process::Command as Process; +use std::process::ExitStatus; // > overwrite // >> append @@ -16,7 +16,11 @@ enum Redirect { } #[derive(Debug, PartialEq, Copy, Clone)] -pub(in crate::commandline) enum RunIf { Always, ExitSuccess, ExitFailure } +pub(in crate::commandline) enum RunIf { + Always, + ExitSuccess, + ExitFailure, +} impl RunIf { pub(in crate::commandline) fn can_run(&self, status: &Option) -> bool { diff --git a/src/completer.rs b/src/completer.rs new file mode 100644 index 0000000..6bf717b --- /dev/null +++ b/src/completer.rs @@ -0,0 +1,160 @@ +use nix::sys::stat::Mode; +use radix_trie::{Trie, TrieCommon}; +use rustyline::hint::{Hint, Hinter}; +use rustyline::Context; +use rustyline_derive::{Completer, Helper, Highlighter, Validator}; +use std::ffi::OsString; +use std::os::unix::fs::PermissionsExt; + +static EXE_MASK: Mode = Mode::from_bits_truncate(0o111); +#[test] +fn test_exe_mask() { + let a = Mode::from_bits_truncate(0o100755); + assert!(a.contains(EXE_MASK)); +} + +#[derive(Completer, Helper, Validator, Highlighter)] +pub struct CommandHinter { + cmd_hints: Trie, + path_hints: Trie, +} + +#[derive(Hash, Debug, PartialEq, Eq)] +pub struct CompleterHint { + display: String, + complete_up_to: usize, +} + +impl Hint for CompleterHint { + fn display(&self) -> &str { + &self.display + } + + fn completion(&self) -> Option<&str> { + if self.complete_up_to > 0 { + Some(&self.display[..self.complete_up_to]) + } else { + None + } + } +} + +impl CompleterHint { + fn new(text: &str, complete_up_to: &str) -> CompleterHint { + assert!(text.starts_with(complete_up_to)); + CompleterHint { + display: text.into(), + complete_up_to: complete_up_to.len(), + } + } + + fn from_path(text: OsString) -> CompleterHint { + CompleterHint { + complete_up_to: text.len(), + display: text.into_string().unwrap(), + } + } + + fn suffix(&self, strip_chars: usize) -> CompleterHint { + CompleterHint { + display: self.display[strip_chars..].to_owned(), + complete_up_to: self.complete_up_to.saturating_sub(strip_chars), + } + } +} + +impl CommandHinter { + pub fn new() -> Result { + let mut cmd_hints = Trie::new(); + let path_hints = Trie::new(); + + for path in std::env::var("PATH").unwrap_or_default().split(":") { + #[cfg(debug_assertions)] + println!("+index {}", path); + + if let Ok(entries) = std::fs::read_dir(path) { + for x in entries { + if let Ok(entry) = x { + let mode = Mode::from_bits_truncate(entry.metadata()?.permissions().mode()); + if mode.contains(EXE_MASK) { + cmd_hints.insert( + entry.file_name().into_string().unwrap(), + CompleterHint::from_path(entry.file_name()), + ); + } + } + } + } + } + + Ok(CommandHinter { + cmd_hints, + path_hints, + }) + } + + pub fn set_cwd(&mut self, path: &std::path::PathBuf) { + self.path_hints = Trie::new(); + if let Ok(entries) = std::fs::read_dir(path) { + for x in entries { + if let Ok(entry) = x { + self.path_hints.insert( + entry.file_name().into_string().unwrap(), + CompleterHint::from_path(entry.file_name()), + ); + } + } + } + } +} + +fn hints( + trie: &Trie, + line: &str, + pos: usize, +) -> Option> { + match trie.get_raw_descendant(line) { + Some(subtrie) => Some( + subtrie + .values() + .filter_map(|hint| { + if pos > 0 && hint.display.starts_with(&line[..pos]) { + Some(hint.suffix(pos)) + } else { + None + } + }) + .collect(), + ), + None => None, + } +} + +impl Hinter for CommandHinter { + type Hint = CompleterHint; + + /// Takes the currently edited `line` with the cursor `pos`ition and + /// returns the string that should be displayed or `None` + /// if no hint is available for the text the user currently typed. + fn hint(&self, line: &str, pos: usize, _ctx: &Context<'_>) -> Option { + if line.is_empty() { + return None; + } + + if pos < line.len() { + return None; + } + + if let Some(mut idx) = line.rfind(' ') { + idx += 1; + if pos <= idx { + return None; + } + + return hints(&self.path_hints, &line[idx..], pos - idx) + .and_then(|mut l| l.pop_front()); + } + + hints(&self.cmd_hints, line, pos).and_then(|mut l| l.pop_front()) + } +} diff --git a/src/main.rs b/src/main.rs index ca5dc00..118d2e2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,31 +2,36 @@ use rustyline::error::ReadlineError; use rustyline::Editor; mod commandline; -use commandline::CommandLine; +mod completer; mod prompt; -fn main() -> Result<(), std::io::Error>{ +fn main() -> Result<(), std::io::Error> { if let Ok(motd) = std::fs::read_to_string("/etc/motd") { print!("{}", motd) } - // TODO: [completer] `()` can be used when no completer is required - let mut rl = Editor::<()>::new(); - let prompt = prompt::Prompt::new()?; + let mut status = None; // exit status of last command + // map of variables + + let hinter = completer::CommandHinter::new()?; + let mut rl = Editor::::new(); + rl.set_helper(Some(hinter)); - let mut status = None; // exit status of last command - // map of variables + let prompt = prompt::Prompt::new()?; /*if rl.load_history("history.txt").is_err() { println!("No previous history."); }*/ 'repl: loop { + let cwd = std::env::current_dir()?; + rl.helper_mut().unwrap().set_cwd(&cwd); + match rl.readline(&prompt.print()) { Ok(line) => { rl.add_history_entry(line.as_str()); - let cmd = CommandLine::new(&line); + let cmd = commandline::CommandLine::new(&line); match cmd.run(&prompt.home, status) { Ok(s) => status = s, Err(e) => eprintln!("{}", e), -- cgit v1.2.1