blob: 4eaeba90ea522e8dc516b52995dc084d7ce5c0d9 [file] [log] [blame]
//! Completion API
use std::borrow::Cow::{self, Borrowed, Owned};
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")
// TODO: 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", 11) => 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)
}
}
impl Completer for () {
fn complete(&self, _line: &str, _pos: usize) -> Result<(usize, Vec<String>)> {
Ok((0, Vec::with_capacity(0)))
}
fn update(&self, _line: &mut LineBuffer, _start: usize, _elected: &str) {
unreachable!()
}
}
impl<'c, C: ?Sized + Completer> Completer for &'c C {
fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<String>)> {
(**self).complete(line, pos)
}
fn update(&self, line: &mut LineBuffer, start: usize, elected: &str) {
(**self).update(line, start, elected)
}
}
macro_rules! box_completer {
($($id: ident)*) => {
$(
impl<C: ?Sized + Completer> Completer for $id<C> {
fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<String>)> {
(**self).complete(line, pos)
}
fn update(&self, line: &mut LineBuffer, start: usize, elected: &str) {
(**self).update(line, start, elected)
}
}
)*
}
}
use std::rc::Rc;
use std::sync::Arc;
box_completer! { Box Rc Arc }
/// A `Completer` for file and folder names.
pub struct FilenameCompleter {
break_chars: BTreeSet<char>,
double_quotes_special_chars: BTreeSet<char>,
}
// rl_basic_word_break_characters, rl_completer_word_break_characters
#[cfg(unix)]
static DEFAULT_BREAK_CHARS: [char; 18] = [
' ', '\t', '\n', '"', '\\', '\'', '`', '@', '$', '>', '<', '=', ';', '|', '&', '{', '(', '\0',
];
#[cfg(unix)]
static ESCAPE_CHAR: Option<char> = Some('\\');
// Remove \ to make file completion works on windows
#[cfg(windows)]
static DEFAULT_BREAK_CHARS: [char; 17] = [
' ', '\t', '\n', '"', '\'', '`', '@', '$', '>', '<', '=', ';', '|', '&', '{', '(', '\0',
];
#[cfg(windows)]
static ESCAPE_CHAR: Option<char> = None;
// In double quotes, not all break_chars need to be escaped
// https://www.gnu.org/software/bash/manual/html_node/Double-Quotes.html
static DOUBLE_QUOTES_SPECIAL_CHARS: [char; 4] = ['"', '$', '\\', '`'];
impl FilenameCompleter {
pub fn new() -> FilenameCompleter {
FilenameCompleter {
break_chars: DEFAULT_BREAK_CHARS.iter().cloned().collect(),
double_quotes_special_chars: DOUBLE_QUOTES_SPECIAL_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, esc_char, break_chars) =
if let Some((idx, double_quote)) = find_unclosed_quote(&line[..pos]) {
let start = idx + 1;
if double_quote {
(
start,
unescape(&line[start..pos], ESCAPE_CHAR),
ESCAPE_CHAR,
&self.double_quotes_special_chars,
)
} else {
(start, Borrowed(&line[start..pos]), None, &self.break_chars)
}
} else {
let (start, path) = extract_word(line, pos, ESCAPE_CHAR, &self.break_chars);
let path = unescape(path, ESCAPE_CHAR);
(start, path, ESCAPE_CHAR, &self.break_chars)
};
let matches = try!(filename_complete(&path, esc_char, break_chars));
Ok((start, matches))
}
}
/// Remove escape char
pub fn unescape(input: &str, esc_char: Option<char>) -> Cow<str> {
if esc_char.is_none() {
return Borrowed(input);
}
let esc_char = esc_char.unwrap();
let n = input.chars().filter(|&c| c == esc_char).count();
if n == 0 {
return Borrowed(input);
}
let mut result = String::with_capacity(input.len() - n);
let mut chars = input.chars();
while let Some(ch) = chars.next() {
if ch == esc_char {
if let Some(ch) = chars.next() {
result.push(ch);
}
} else {
result.push(ch);
}
}
Owned(result)
}
/// Escape any `break_chars` in `input` string with `esc_char`.
/// For example, '/User Information' becomes '/User\ Information'
/// when space is a breaking char and '\\' the escape char.
pub fn escape(input: String, esc_char: Option<char>, break_chars: &BTreeSet<char>) -> String {
if esc_char.is_none() {
return input;
}
let esc_char = esc_char.unwrap();
let n = input.chars().filter(|c| break_chars.contains(c)).count();
if n == 0 {
return input;
}
let mut result = String::with_capacity(input.len() + n);
for c in input.chars() {
if break_chars.contains(&c) {
result.push(esc_char);
}
result.push(c);
}
result
}
fn filename_complete(
path: &str,
esc_char: Option<char>,
break_chars: &BTreeSet<char>,
) -> Result<Vec<String>> {
use dirs::home_dir;
use std::env::current_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!(dir.read_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(escape(path, esc_char, break_chars));
}
}
}
Ok(entries)
}
/// Given a `line` and a cursor `pos`ition,
/// try to find backward the start of a word.
/// Return (0, `line[..pos]`) if no break char has been found.
/// Return the word and its start position (idx, `line[idx..pos]`) otherwise.
pub fn extract_word<'l>(
line: &'l str,
pos: usize,
esc_char: Option<char>,
break_chars: &BTreeSet<char>,
) -> (usize, &'l str) {
let line = &line[..pos];
if line.is_empty() {
return (0, line);
}
let mut start = None;
for (i, c) in line.char_indices().rev() {
if esc_char.is_some() && start.is_some() {
if esc_char.unwrap() == c {
// escaped break char
start = None;
continue;
} else {
break;
}
}
if break_chars.contains(&c) {
start = Some(i + c.len_utf8());
if esc_char.is_none() {
break;
} // else maybe escaped...
}
}
match start {
Some(start) => (start, &line[start..]),
None => (0, line),
}
}
pub fn longest_common_prefix(candidates: &[String]) -> Option<&str> {
if candidates.is_empty() {
return None;
} else if candidates.len() == 1 {
return Some(&candidates[0]);
}
let mut longest_common_prefix = 0;
'o: loop {
for (i, c1) in candidates.iter().enumerate().take(candidates.len() - 1) {
let b1 = c1.as_bytes();
let b2 = candidates[i + 1].as_bytes();
if b1.len() <= longest_common_prefix
|| b2.len() <= longest_common_prefix
|| b1[longest_common_prefix] != b2[longest_common_prefix]
{
break 'o;
}
}
longest_common_prefix += 1;
}
while !candidates[0].is_char_boundary(longest_common_prefix) {
longest_common_prefix -= 1;
}
if longest_common_prefix == 0 {
return None;
}
Some(&candidates[0][0..longest_common_prefix])
}
#[derive(PartialEq)]
enum ScanMode {
DoubleQuote,
Escape,
EscapeInDoubleQuote,
Normal,
SingleQuote,
}
/// try to find an unclosed single/double quote in `s`.
/// Return `None` if no unclosed quote is found.
/// Return the unclosed quote position and if it is a double quote.
fn find_unclosed_quote(s: &str) -> Option<(usize, bool)> {
let char_indices = s.char_indices();
let mut mode = ScanMode::Normal;
let mut quote_index = 0;
for (index, char) in char_indices {
match mode {
ScanMode::DoubleQuote => {
if char == '"' {
mode = ScanMode::Normal;
} else if char == '\\' {
mode = ScanMode::EscapeInDoubleQuote;
}
}
ScanMode::Escape => {
mode = ScanMode::Normal;
}
ScanMode::EscapeInDoubleQuote => {
mode = ScanMode::DoubleQuote;
}
ScanMode::Normal => {
if char == '"' {
mode = ScanMode::DoubleQuote;
quote_index = index;
} else if char == '\\' && cfg!(not(windows)) {
mode = ScanMode::Escape;
} else if char == '\'' && cfg!(not(windows)) {
mode = ScanMode::SingleQuote;
quote_index = index;
}
}
ScanMode::SingleQuote => {
if char == '\'' {
mode = ScanMode::Normal;
} // no escape in single quotes
}
};
}
if ScanMode::DoubleQuote == mode || ScanMode::SingleQuote == mode {
return Some((quote_index, ScanMode::DoubleQuote == mode));
}
None
}
#[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(), Some('\\'), &break_chars)
);
let line = "ls /User\\ Information";
assert_eq!(
(3, "/User\\ Information"),
super::extract_word(line, line.len(), Some('\\'), &break_chars)
);
}
#[test]
pub fn unescape() {
use std::borrow::Cow::{self, Borrowed, Owned};
let input = "/usr/local/b";
assert_eq!(Borrowed(input), super::unescape(input, Some('\\')));
let input = "/User\\ Information";
let result: Cow<str> = Owned(String::from("/User Information"));
assert_eq!(result, super::unescape(input, Some('\\')));
}
#[test]
pub fn escape() {
let break_chars: BTreeSet<char> = super::DEFAULT_BREAK_CHARS.iter().cloned().collect();
let input = String::from("/usr/local/b");
assert_eq!(
input.clone(),
super::escape(input, Some('\\'), &break_chars)
);
let input = String::from("/User Information");
let result = String::from("/User\\ Information");
assert_eq!(result, super::escape(input, Some('\\'), &break_chars));
}
#[test]
pub fn longest_common_prefix() {
let mut candidates = vec![];
{
let lcp = super::longest_common_prefix(&candidates);
assert!(lcp.is_none());
}
let s = "User";
let c1 = String::from(s);
candidates.push(c1.clone());
{
let lcp = super::longest_common_prefix(&candidates);
assert_eq!(Some(s), lcp);
}
let c2 = String::from("Users");
candidates.push(c2.clone());
{
let lcp = super::longest_common_prefix(&candidates);
assert_eq!(Some(s), lcp);
}
let c3 = String::from("");
candidates.push(c3.clone());
{
let lcp = super::longest_common_prefix(&candidates);
assert!(lcp.is_none());
}
let candidates = vec![String::from("fée"), String::from("fête")];
let lcp = super::longest_common_prefix(&candidates);
assert_eq!(Some("f"), lcp);
}
#[test]
pub fn find_unclosed_quote() {
assert_eq!(None, super::find_unclosed_quote("ls /etc"));
assert_eq!(
Some((3, true)),
super::find_unclosed_quote("ls \"User Information")
);
assert_eq!(
None,
super::find_unclosed_quote("ls \"/User Information\" /etc")
);
}
}