Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filename completion #12

Merged
merged 8 commits into from
Sep 17, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/example.rs
Original file line number Diff line number Diff line change
@@ -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.");
}
Expand Down
118 changes: 113 additions & 5 deletions src/completion.rs
Original file line number Diff line number Diff line change
@@ -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<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::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<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));
}
}
9 changes: 5 additions & 4 deletions src/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
47 changes: 25 additions & 22 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
//!```
#![feature(drain)]
#![feature(io)]
#![feature(path_relative_from)]
#![feature(str_split_at)]
#![feature(str_char)]
#![feature(unicode)]
extern crate libc;
Expand All @@ -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;
Expand Down Expand Up @@ -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<S: Into<String>>(&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);
Expand Down Expand Up @@ -135,12 +136,12 @@ fn enable_raw_mode() -> Result<termios::Termios> {
} 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)
}
Expand Down Expand Up @@ -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)
Expand All @@ -434,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)
Expand All @@ -446,11 +447,11 @@ 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));
s.update_buf(&buf);
s.update_buf(buf);
s.pos = pos;
} else {
try!(refresh_line(s));
Expand All @@ -469,12 +470,12 @@ fn complete_line<R: io::Read>(chars: &mut io::Chars<R>, 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
Expand All @@ -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;
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -809,8 +812,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")))
}
}

Expand Down