blob: 9a5aa396721b977ebd31fe677445acf387d24397 [file] [log] [blame]
//! Completion API
use std::collections::BTreeSet;
use std::fs;
use std::path::{self, Path};
use super::Result;
use line_buffer::LineBuffer;
// 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 ???
/// 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>)>;
/// Updates the edited `line` with the `elected` candidate.
fn update(&self, line: &mut LineBuffer, start: usize, elected: &str) {
let end = line.pos();
line.replace(start, end, elected)
}
}
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 Default for FilenameCompleter {
fn default() -> FilenameCompleter {
FilenameCompleter::new()
}
}
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))
}
}
#[cfg_attr(feature="clippy", allow(single_char_pattern))]
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.strip_prefix("~") {
Ok(rel_path) => home.join(rel_path),
_ => 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));
}
}