diff options
Diffstat (limited to 'src/completer.rs')
-rw-r--r-- | src/completer.rs | 160 |
1 files changed, 160 insertions, 0 deletions
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<String, CompleterHint>, + path_hints: Trie<String, CompleterHint>, +} + +#[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<CommandHinter, std::io::Error> { + 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<String, CompleterHint>, + line: &str, + pos: usize, +) -> Option<std::collections::VecDeque<CompleterHint>> { + 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<CompleterHint> { + 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()) + } +} |