aboutsummaryrefslogtreecommitdiff
path: root/src/completer.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/completer.rs')
-rw-r--r--src/completer.rs160
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())
+ }
+}