From c5076f2fa9666b2e09c10191a808123bd487f191 Mon Sep 17 00:00:00 2001
From: Gwenael Treguier <gwen@cas75-5-78-192-41-37.fbxo.proxad.net>
Date: Sun, 13 Sep 2015 15:53:48 +0200
Subject: [PATCH] Add FilenameCompleter.

---
 examples/example.rs |   3 ++
 src/completion.rs   | 117 ++++++++++++++++++++++++++++++++++++++++++--
 src/lib.rs          |  17 ++++---
 3 files changed, 125 insertions(+), 12 deletions(-)

diff --git a/examples/example.rs b/examples/example.rs
index acec55e3..d3f9771c 100644
--- a/examples/example.rs
+++ b/examples/example.rs
@@ -1,10 +1,13 @@
 extern crate rustyline;
 
+use rustyline::completion::FilenameCompleter;
 use rustyline::error::ReadlineError;
 use rustyline::Editor;
 
 fn main() {
+    let c = FilenameCompleter::new();
     let mut rl = Editor::new();
+    rl.set_completer(Some(&c));
     if let Err(_) = rl.load_history("history.txt") {
         println!("No previous history.");
     }
diff --git a/src/completion.rs b/src/completion.rs
index 2769cf07..62d1ef5f 100644
--- a/src/completion.rs
+++ b/src/completion.rs
@@ -1,15 +1,122 @@
 //! Completion API
+use std::collections::BTreeSet;
+use std::fs;
+use std::path::{self,Path};
+
+use super::Result;
+
+// TODO: let the implementers choose/find word boudaries ???
+// (line, pos) is like (rl_line_buffer, rl_point) to make contextual completion ("select t.na| from tbl as t")
+// TOOD: make &self &mut self ???
+// TODO: change update signature: _line: Into<String>
 
 /// To be called for tab-completion.
 pub trait Completer {
     /// Takes the currently edited `line` with the cursor `pos`ition and
-    /// returns the completion candidates for the partial word to be completed.
-    fn complete(&self, line: &str, pos: usize) -> Vec<String>;
+    /// returns the start position and the completion candidates for the partial word to be completed.
+    /// "ls /usr/loc" => Ok((3, vec!["/usr/local/"]))
+    fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<String>)>;
     /// Takes the currently edited `line` with the cursor `pos`ition and
     /// the `elected` candidate.
     /// Returns the new line content and cursor position.
-    fn update(&self, _line: &str, _pos: usize, elected: &str) -> (String, usize) {
-        // line completion (vs word completion)
-        (String::from(elected), elected.len())
+    fn update(&self, line: &str, pos: usize, start: usize, elected: &str) -> (String, usize) {
+        let mut buf = String::from(&line[..start]);
+        buf.push_str(elected);
+        buf.push(' ');
+        let new_pos = buf.len();
+        buf.push_str(&line[pos..]);
+        (buf, new_pos)
+    }
+}
+
+pub struct FilenameCompleter {
+    break_chars: BTreeSet<char>
+}
+
+static DEFAULT_BREAK_CHARS : [char; 18] = [ ' ', '\t', '\n', '"', '\\', '\'', '`', '@', '$',
+            '>', '<', '=', ';', '|', '&', '{', '(', '\0' ];
+
+impl FilenameCompleter {
+    pub fn new() -> FilenameCompleter {
+        FilenameCompleter { break_chars: DEFAULT_BREAK_CHARS.iter().cloned().collect() }
+    }
+}
+
+impl Completer for FilenameCompleter {
+    fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<String>)> {
+        let (start, path) = extract_word(line, pos, &self.break_chars);
+        let matches = try!(filename_complete(path));
+        Ok((start, matches))
+    }
+}
+
+fn filename_complete(path: &str) -> Result<Vec<String>> {
+    use std::env::{current_dir,home_dir};
+
+    let sep = path::MAIN_SEPARATOR;
+    let (dir_name, file_name) = match path.rfind(sep) {
+        Some(idx) => path.split_at(idx+sep.len_utf8()),
+        None => ("", path)
+    };
+
+    let dir_path = Path::new(dir_name);
+    let dir = if dir_path.starts_with("~") { // ~[/...]
+        if let Some(home) = home_dir() {
+            match dir_path.relative_from("~") {
+                Some(rel_path) => home.join(rel_path),
+                None => home
+            }
+        } else {
+            dir_path.to_path_buf()
+        }
+    } else if dir_path.is_relative() { // TODO ~user[/...] (https://crates.io/crates/users)
+        if let Ok(cwd) = current_dir() {
+            cwd.join(dir_path)
+        } else {
+            dir_path.to_path_buf()
+        }
+    } else {
+        dir_path.to_path_buf()
+    };
+
+    let mut entries: Vec<String> = Vec::new();
+    for entry in try!(fs::read_dir(dir)) {
+        let entry = try!(entry);
+        if let Some(s) = entry.file_name().to_str() {
+            if s.starts_with(file_name) {
+                let mut path = String::from(dir_name) + s;
+                if try!(fs::metadata(entry.path())).is_dir() {
+                    path.push(sep);
+                }
+                entries.push(path);
+            }
+        }
+    }
+    Ok(entries)
+}
+
+pub fn extract_word<'l>(line: &'l str, pos: usize, break_chars: &BTreeSet<char>) -> (usize, &'l str) {
+    let line = &line[..pos];
+    if line.is_empty() {
+        return (0, line);
+    }
+    match line.char_indices().rev().find(|&(_, c)| break_chars.contains(&c)) {
+        Some((i, c)) => {
+            let start = i+c.len_utf8();
+            (start, &line[start..])
+        },
+        None => (0, line)
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use std::collections::BTreeSet;
+
+    #[test]
+    pub fn extract_word() {
+        let break_chars: BTreeSet<char> = super::DEFAULT_BREAK_CHARS.iter().cloned().collect();
+        let line = "ls '/usr/local/b";
+        assert_eq!((4, "/usr/local/b"), super::extract_word(line, line.len(), &break_chars));
+    }
+}
\ No newline at end of file
diff --git a/src/lib.rs b/src/lib.rs
index 5cce82ff..0463092f 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -16,6 +16,8 @@
 //!```
 #![feature(drain)]
 #![feature(io)]
+#![feature(path_relative_from)]
+#![feature(str_split_at)]
 #![feature(str_char)]
 #![feature(unicode)]
 extern crate libc;
@@ -433,7 +435,7 @@ fn edit_history_next(s: &mut State, history: &mut History, prev: bool) -> Result
 
 /// Completes the line/word
 fn complete_line<R: io::Read>(chars: &mut io::Chars<R>, s: &mut State, completer: &Completer) -> Result<Option<char>> {
-    let candidates = completer.complete(&s.buf, s.pos);
+    let (start, candidates) = try!(completer.complete(&s.buf, s.pos));
     if candidates.is_empty() {
         try!(beep());
         Ok(None)
@@ -445,7 +447,7 @@ fn complete_line<R: io::Read>(chars: &mut io::Chars<R>, s: &mut State, completer
             if i < candidates.len() {
                 let buf = s.buf.clone(); // TODO how to avoid cloning?
                 let pos = s.pos;
-                let (tmp_buf, tmp_pos) = completer.update(&s.buf, s.pos, &candidates[i]);
+                let (tmp_buf, tmp_pos) = completer.update(&s.buf, s.pos, start, &candidates[i]);
                 s.buf = tmp_buf;
                 s.pos = tmp_pos;
                 try!(refresh_line(s));
@@ -472,7 +474,7 @@ fn complete_line<R: io::Read>(chars: &mut io::Chars<R>, s: &mut State, completer
                 },
                 _ => { // Update buffer and return
                     if i < candidates.len() {
-                        let (buf, pos) = completer.update(&s.buf, s.pos, &candidates[i]);
+                        let (buf, pos) = completer.update(&s.buf, s.pos, start, &candidates[i]);
                         s.update_buf(buf);
                         s.pos = pos;
                     }
@@ -662,6 +664,7 @@ mod test {
     use history::History;
     use completion::Completer;
     use State;
+    use super::Result;
 
     fn init_state<'out>(out: &'out mut Write, line: &str, pos: usize, cols: usize) -> State<'out, 'static> {
         State {
@@ -808,8 +811,8 @@ mod test {
 
     struct SimpleCompleter;
     impl Completer for SimpleCompleter {
-        fn complete(&self, line: &str, _pos: usize) -> Vec<String> {
-            vec!(line.to_string() + "t")
+        fn complete(&self, line: &str, _pos: usize) -> Result<(usize, Vec<String>)> {
+            Ok((0, vec!(line.to_string() + "t")))
         }
     }
 
@@ -824,7 +827,7 @@ mod test {
         let completer = SimpleCompleter;
         let ch = super::complete_line(&mut chars, &mut s, &completer).unwrap();
         assert_eq!(Some('\n'), ch);
-        assert_eq!("rust", s.buf);
-        assert_eq!(4, s.pos);
+        assert_eq!("rust ", s.buf);
+        assert_eq!(5, s.pos);
     }
 }
-- 
GitLab