blob: f319d655b75a1cbcb3e7984427c46d8d38c5e67b [file] [log] [blame]
//! 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 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, 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));
}
}