diff --git a/examples/example.rs b/examples/example.rs index acec55e3fd..d3f9771cbe 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 2769cf07ca..f319d655b7 100644 --- a/src/completion.rs +++ b/src/completion.rs @@ -1,15 +1,123 @@ //! 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 /// 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; + /// 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)>; /// 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::with_capacity(start + elected.len() + line.len() - pos); + buf.push_str(&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 +} + +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)> { + 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> { + 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 = 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) -> (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 = 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/history.rs b/src/history.rs index 2ee9152c01..103c54c75a 100644 --- a/src/history.rs +++ b/src/history.rs @@ -31,14 +31,15 @@ impl History { if line.len() == 0 || line.chars().next().map_or(true, |c| c.is_whitespace()) { // ignorespace return false; } - let s = String::from(line); // TODO try to allocate only on push_back - if self.entries.back() == Some(&s) { // ignoredups - return false; + if let Some(s) = self.entries.back() { + if s == line { // ignoredups + return false; + } } if self.entries.len() == self.max_len { self.entries.pop_front(); } - self.entries.push_back(s); + self.entries.push_back(String::from(line)); return true; } diff --git a/src/lib.rs b/src/lib.rs index d00c6554c4..799f1498a0 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; @@ -29,8 +31,7 @@ pub mod error; pub mod history; use std::fmt; -use std::io; -use std::io::{Read, Write}; +use std::io::{self,Read, Write}; use std::path::Path; use std::result; use nix::errno::Errno; @@ -71,8 +72,8 @@ impl<'out, 'prompt> State<'out, 'prompt> { } } - fn update_buf(&mut self, buf: &str) { - self.buf = String::from(buf); + fn update_buf>(&mut self, buf: S) { + self.buf = buf.into(); if self.buf.capacity() < MAX_LINE { let cap = self.buf.capacity(); self.buf.reserve_exact(MAX_LINE - cap); @@ -135,12 +136,12 @@ fn enable_raw_mode() -> Result { } else { let original_term = try!(termios::tcgetattr(libc::STDIN_FILENO)); let mut raw = original_term; - raw.c_iflag = raw.c_iflag & !(BRKINT | ICRNL | INPCK | ISTRIP | IXON); - raw.c_oflag = raw.c_oflag & !(OPOST); - raw.c_cflag = raw.c_cflag | (CS8); - raw.c_lflag = raw.c_lflag & !(ECHO | ICANON | IEXTEN | ISIG); - raw.c_cc[VMIN] = 1; - raw.c_cc[VTIME] = 0; + raw.c_iflag = raw.c_iflag & !(BRKINT | ICRNL | INPCK | ISTRIP | IXON); // disable BREAK interrupt, CR to NL conversion on input, input parity check, strip high bit (bit 8), output flow control + raw.c_oflag = raw.c_oflag & !(OPOST); // disable all output processing + raw.c_cflag = raw.c_cflag | (CS8); // character-size mark (8 bits) + raw.c_lflag = raw.c_lflag & !(ECHO | ICANON | IEXTEN | ISIG); // disable echoing, canonical mode, extended input processing and signals + raw.c_cc[VMIN] = 1; // One character-at-a-time input + raw.c_cc[VTIME] = 0; // with blocking read try!(termios::tcsetattr(libc::STDIN_FILENO, termios::TCSAFLUSH, &raw)); Ok(original_term) } @@ -420,10 +421,10 @@ fn edit_history_next(s: &mut State, history: &mut History, prev: bool) -> Result } if s.history_index < history.len() { let buf = history.get(s.history_index).unwrap(); - s.update_buf(buf); + s.update_buf(buf.clone()); } else { let buf = s.history_end.clone(); // TODO how to avoid cloning? - s.update_buf(&buf); + s.update_buf(buf); }; s.pos = s.buf.len(); refresh_line(s) @@ -434,7 +435,7 @@ fn edit_history_next(s: &mut State, history: &mut History, prev: bool) -> Result /// Completes the line/word fn complete_line(chars: &mut io::Chars, s: &mut State, completer: &Completer) -> Result> { - 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) @@ -446,11 +447,11 @@ fn complete_line(chars: &mut io::Chars, 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)); - s.update_buf(&buf); + s.update_buf(buf); s.pos = pos; } else { try!(refresh_line(s)); @@ -469,12 +470,12 @@ fn complete_line(chars: &mut io::Chars, s: &mut State, completer if i < candidates.len() { try!(refresh_line(s)); } - break + return Ok(None) }, _ => { // Update buffer and return if i < candidates.len() { - let (buf, pos) = completer.update(&s.buf, s.pos, &candidates[i]); - s.update_buf(&buf); + let (buf, pos) = completer.update(&s.buf, s.pos, start, &candidates[i]); + s.update_buf(buf); s.pos = pos; } break @@ -496,13 +497,14 @@ fn readline_edit(prompt: &str, history: &mut History, completer: Option<&Complet let stdin = io::stdin(); let mut chars = stdin.lock().chars(); loop { - let ch = try!(chars.next().unwrap()); + let mut ch = try!(chars.next().unwrap()); // FIXME unwrap let mut key = char_to_key_press(ch); // autocomplete if key == KeyPress::TAB && completer.is_some() { let next = try!(complete_line(&mut chars, &mut s, completer.unwrap())); if next.is_some() { - key = char_to_key_press(next.unwrap()); + ch = next.unwrap(); + key = char_to_key_press(ch); } else { continue; } @@ -663,6 +665,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 { @@ -809,8 +812,8 @@ mod test { struct SimpleCompleter; impl Completer for SimpleCompleter { - fn complete(&self, line: &str, _pos: usize) -> Vec { - vec!(line.to_string() + "t") + fn complete(&self, line: &str, _pos: usize) -> Result<(usize, Vec)> { + Ok((0, vec!(line.to_string() + "t"))) } }