use crate::base::{CompleterResult, MainCompleteState};
use crate::compcore::CompletionState;
use crate::completion::{Completion, CompletionFlags};
use std::collections::{HashMap, HashSet};
use std::path::Path;
use std::process::Command;
pub fn setup(state: &mut MainCompleteState, tag: &str) {
let context = format!(":completion:{}:{}", state.ctx.context, tag);
if let Some(colors) = state.styles.lookup_values(&context, "list-colors") {
let _ = colors;
}
if let Some(val) = state.styles.lookup_values(&context, "show-ambiguity") {
if let Some(v) = val.first() {
if v == "yes" || v == "true" || v == "on" {
}
}
}
if state
.styles
.lookup_values(&context, "list-packed")
.is_some()
{
state.comp.params.compstate.list.push_str(" packed");
}
if state
.styles
.lookup_values(&context, "list-rows-first")
.is_some()
{
state.comp.params.compstate.list.push_str(" rows");
}
if state
.styles
.lookup_values(&context, "last-prompt")
.is_some()
{
state.comp.params.compstate.last_prompt = true.to_string();
}
if state
.styles
.lookup_values(&context, "accept-exact")
.is_some()
{
state.comp.params.compstate.exact = "accept".to_string();
}
if let Some(menu) = state.styles.lookup_values(&context, "menu") {
let _ = menu;
}
if let Some(val) = state.styles.lookup_values(&context, "force-list") {
if let Some(v) = val.first() {
if v == "always" {
state.comp.params.compstate.list.push_str(" force");
}
}
}
}
pub fn dispatch(
_state: &mut MainCompleteState,
comps: &HashMap<String, String>,
commands: &[&str],
) -> CompleterResult {
for cmd in commands {
if let Some(func) = comps.get(*cmd) {
let _ = func;
return CompleterResult::Matched;
}
}
CompleterResult::NoMatch
}
pub fn wanted(
state: &mut MainCompleteState,
tag: &str,
description: &str,
action: impl FnOnce(&mut CompletionState) -> bool,
) -> bool {
if !state.tags.requested(tag) {
return false;
}
state.comp.begin_group(tag, true);
if !description.is_empty() {
state
.comp
.add_explanation(description.to_string(), Some(tag));
}
let result = action(&mut state.comp);
state.comp.end_group();
result
}
pub fn call_program(state: &MainCompleteState, tag: &str, command: &[&str]) -> Option<String> {
if command.is_empty() {
return None;
}
let context = format!(":completion:{}:{}", state.ctx.context, tag);
let cmd = if let Some(override_cmd) = state.styles.lookup_values(&context, "command") {
override_cmd.to_vec()
} else {
command.iter().map(|s| s.to_string()).collect()
};
if cmd.is_empty() {
return None;
}
let output = Command::new(&cmd[0]).args(&cmd[1..]).output().ok()?;
if output.status.success() {
Some(String::from_utf8_lossy(&output.stdout).to_string())
} else {
None
}
}
pub fn call_function(_state: &mut MainCompleteState, _func: &str) -> bool {
false
}
pub fn cache_invalid(state: &MainCompleteState, cache_name: &str) -> bool {
let context = format!(":completion:{}:", state.ctx.context);
if let Some(policy) = state.styles.lookup_values(&context, "cache-policy") {
let _ = policy;
}
if let Some(use_cache) = state.styles.lookup_values(&context, "use-cache") {
if let Some(v) = use_cache.first() {
if v == "no" || v == "false" || v == "off" || v == "0" {
return true;
}
}
}
if let Some(cache_path) = state.styles.lookup_values(&context, "cache-path") {
if let Some(path) = cache_path.first() {
let cache_file = format!("{}/{}", path, cache_name);
return !Path::new(&cache_file).exists();
}
}
true
}
pub fn retrieve_cache(state: &MainCompleteState, cache_name: &str) -> Option<Vec<String>> {
let context = format!(":completion:{}:", state.ctx.context);
if let Some(cache_path) = state.styles.lookup_values(&context, "cache-path") {
if let Some(path) = cache_path.first() {
let cache_file = format!("{}/{}", path, cache_name);
if let Ok(contents) = std::fs::read_to_string(&cache_file) {
return Some(contents.lines().map(String::from).collect());
}
}
}
None
}
pub fn store_cache(state: &MainCompleteState, cache_name: &str, data: &[String]) -> bool {
let context = format!(":completion:{}:", state.ctx.context);
if let Some(cache_path) = state.styles.lookup_values(&context, "cache-path") {
if let Some(path) = cache_path.first() {
let cache_file = format!("{}/{}", path, cache_name);
if let Some(parent) = Path::new(&cache_file).parent() {
let _ = std::fs::create_dir_all(parent);
}
let contents = data.join("\n");
return std::fs::write(&cache_file, contents).is_ok();
}
}
false
}
pub fn guard(state: &MainCompleteState, pattern: &str) -> bool {
let prefix = state.comp.params.prefix.clone();
if pattern.contains('*') || pattern.contains('?') {
glob_match(pattern, &prefix)
} else {
prefix.starts_with(pattern)
}
}
pub fn glob_match(pattern: &str, text: &str) -> bool {
let pattern_chars: Vec<char> = pattern.chars().collect();
let text_chars: Vec<char> = text.chars().collect();
glob_match_helper(&pattern_chars, &text_chars)
}
fn glob_match_helper(pattern: &[char], text: &[char]) -> bool {
match (pattern.first(), text.first()) {
(None, None) => true,
(Some('*'), _) => {
glob_match_helper(&pattern[1..], text)
|| (!text.is_empty() && glob_match_helper(pattern, &text[1..]))
}
(Some('?'), Some(_)) => glob_match_helper(&pattern[1..], &text[1..]),
(Some(p), Some(t)) if p == t => glob_match_helper(&pattern[1..], &text[1..]),
_ => false,
}
}
pub fn nothing(_state: &mut CompletionState) -> bool {
true
}
pub fn numbers(
state: &mut CompletionState,
min: i64,
max: i64,
step: i64,
description: Option<&str>,
) -> bool {
let prefix = state.params.prefix.clone();
state.begin_group("numbers", true);
if let Some(desc) = description {
state.add_explanation(desc.to_string(), Some("numbers"));
}
let mut matched = false;
let mut n = min;
while n <= max {
let s = n.to_string();
if s.starts_with(&prefix) || prefix.is_empty() {
state.add_match(Completion::new(&s), Some("numbers"));
matched = true;
}
n += step;
}
state.end_group();
matched
}
pub fn pick_variant(
command: &str,
tests: &[(&str, &str)], ) -> Option<String> {
for (test_arg, expected) in tests {
let output = Command::new(command).arg(test_arg).output().ok()?;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let combined = format!("{}{}", stdout, stderr);
if combined.contains(expected) {
return Some(expected.to_string());
}
}
None
}
pub fn sub_commands(
state: &mut CompletionState,
commands: &[(String, String)], ) -> bool {
let prefix = state.params.prefix.clone();
state.begin_group("subcommands", true);
let mut matched = false;
for (name, desc) in commands {
if name.starts_with(&prefix) {
let mut comp = Completion::new(name);
if !desc.is_empty() {
comp.disp = Some(format!("{} -- {}", name, desc));
}
state.add_match(comp, Some("subcommands"));
matched = true;
}
}
state.end_group();
matched
}
pub fn sequence(
state: &mut CompletionState,
separator: &str,
completer: impl Fn(&mut CompletionState) -> bool,
) -> bool {
let prefix = state.params.prefix.clone();
if let Some(last_sep) = prefix.rfind(separator) {
let new_prefix = prefix[last_sep + separator.len()..].to_string();
state.params.prefix = new_prefix;
state.params.iprefix = prefix[..last_sep + separator.len()].to_string();
}
completer(state)
}
pub fn combination(
state: &mut CompletionState,
tag: &str,
specs: &[(&str, Vec<String>)], ) -> bool {
let prefix = state.params.prefix.clone();
state.begin_group(tag, true);
let mut matched = false;
for (name, values) in specs {
for value in values {
let full = format!("{}={}", name, value);
if full.starts_with(&prefix) {
let mut comp = Completion::new(&full);
comp.disp = Some(format!("{}={}", name, value));
state.add_match(comp, Some(tag));
matched = true;
}
}
}
state.end_group();
matched
}
pub fn regex_arguments(
state: &mut CompletionState,
_name: &str,
patterns: &[(String, String, String)], ) -> bool {
let current = state.params.current_word();
for (pattern, desc, action) in patterns {
if let Ok(re) = regex_lite::Regex::new(pattern) {
if re.is_match(¤t) {
state.begin_group("regex", true);
state.add_explanation(desc.clone(), Some("regex"));
state.end_group();
let _ = action;
return true;
}
}
}
false
}
pub fn regex_words(
state: &mut CompletionState,
tag: &str,
description: &str,
specs: &[(String, String)], ) -> bool {
let prefix = state.params.prefix.clone();
state.begin_group(tag, true);
if !description.is_empty() {
state.add_explanation(description.to_string(), Some(tag));
}
let mut matched = false;
for (word, desc) in specs {
if word.starts_with(&prefix) {
let mut comp = Completion::new(word);
if !desc.is_empty() {
comp.disp = Some(format!("{} -- {}", word, desc));
}
state.add_match(comp, Some(tag));
matched = true;
}
}
state.end_group();
matched
}
pub fn set_command(state: &mut MainCompleteState) {
if !state.comp.params.words.is_empty() {
let cmd = &state.comp.params.words[0];
state.lastcomp.insert("command".to_string(), cmd.clone());
}
}
pub fn shadow(
state: &mut CompletionState,
_shadow_name: &str,
action: impl FnOnce(&mut CompletionState) -> bool,
) -> bool {
action(state)
}
pub fn as_if(
state: &mut MainCompleteState,
new_context: &str,
action: impl FnOnce(&mut MainCompleteState) -> bool,
) -> bool {
let old_context = state.ctx.context.clone();
state.ctx.context = new_context.to_string();
let result = action(state);
state.ctx.context = old_context;
result
}
pub fn comp_locale() {
}
pub fn complete_help_generic(state: &mut CompletionState, help_text: &str) -> bool {
let prefix = state.params.prefix.clone();
let mut options = Vec::new();
for line in help_text.lines() {
let line = line.trim();
if line.starts_with('-') {
let parts: Vec<&str> = line.splitn(2, |c: char| c.is_whitespace()).collect();
if let Some(opt) = parts.first() {
let desc = parts.get(1).unwrap_or(&"").trim();
if opt.starts_with(&prefix) || prefix.is_empty() {
options.push((opt.to_string(), desc.to_string()));
}
}
}
}
if options.is_empty() {
return false;
}
state.begin_group("options", true);
for (opt, desc) in options {
let mut comp = Completion::new(&opt);
if !desc.is_empty() {
comp.disp = Some(format!("{} -- {}", opt, desc));
}
state.add_match(comp, Some("options"));
}
state.end_group();
true
}
pub fn arg_compile(specs: &[String]) -> Vec<CompiledArgSpec> {
specs
.iter()
.filter_map(|s| CompiledArgSpec::parse(s))
.collect()
}
#[derive(Clone, Debug)]
pub struct CompiledArgSpec {
pub pattern: String,
pub action: String,
pub description: String,
}
impl CompiledArgSpec {
pub fn parse(spec: &str) -> Option<Self> {
let parts: Vec<&str> = spec.splitn(3, ':').collect();
if parts.is_empty() {
return None;
}
Some(Self {
pattern: parts[0].to_string(),
description: parts.get(1).unwrap_or(&"").to_string(),
action: parts.get(2).unwrap_or(&"").to_string(),
})
}
}
pub fn all_matches(state: &mut CompletionState) -> bool {
state.params.compstate.insert = "all".to_string();
true
}
pub fn approximate(state: &mut MainCompleteState, max_errors: usize) -> CompleterResult {
let original = state.comp.params.prefix.clone();
let matches: Vec<String> = state
.comp
.all_completions()
.iter()
.filter(|c| edit_distance(&original, &c.str_) <= max_errors)
.map(|c| c.str_.clone())
.collect();
if matches.is_empty() {
CompleterResult::NoMatch
} else {
for m in matches {
state.comp.add_match(Completion::new(&m), None);
}
CompleterResult::Matched
}
}
pub fn edit_distance(a: &str, b: &str) -> usize {
let a_chars: Vec<char> = a.chars().collect();
let b_chars: Vec<char> = b.chars().collect();
let m = a_chars.len();
let n = b_chars.len();
let mut dp = vec![vec![0; n + 1]; m + 1];
for i in 0..=m {
dp[i][0] = i;
}
for j in 0..=n {
dp[0][j] = j;
}
for i in 1..=m {
for j in 1..=n {
let cost = if a_chars[i - 1] == b_chars[j - 1] {
0
} else {
1
};
dp[i][j] = (dp[i - 1][j] + 1)
.min(dp[i][j - 1] + 1)
.min(dp[i - 1][j - 1] + cost);
}
}
dp[m][n]
}
pub fn correct(state: &mut MainCompleteState) -> CompleterResult {
approximate(state, 1)
}
pub fn expand(state: &mut CompletionState) -> bool {
let prefix = &state.params.prefix;
let mut expanded = prefix.clone();
let mut did_expand = false;
if expanded.starts_with('~') {
if let Some(home) = std::env::var("HOME").ok() {
if expanded == "~" || expanded.starts_with("~/") {
expanded = expanded.replacen("~", &home, 1);
did_expand = true;
}
}
}
while let Some(dollar_pos) = expanded.find('$') {
let rest = &expanded[dollar_pos + 1..];
let var_end = rest
.find(|c: char| !c.is_alphanumeric() && c != '_')
.unwrap_or(rest.len());
let var_name = &rest[..var_end];
if let Ok(value) = std::env::var(var_name) {
let before = &expanded[..dollar_pos];
let after = &rest[var_end..];
expanded = format!("{}{}{}", before, value, after);
did_expand = true;
} else {
break;
}
}
if did_expand && expanded != *prefix {
state.add_match(Completion::new(&expanded), None);
true
} else {
false
}
}
pub fn expand_alias(state: &mut CompletionState, aliases: &HashMap<String, String>) -> bool {
let word = state.params.current_word();
if let Some(expansion) = aliases.get(&word) {
let mut comp = Completion::new(expansion);
comp.flags |= CompletionFlags::NOSPACE;
state.add_match(comp, None);
true
} else {
false
}
}
pub fn extensions(state: &mut CompletionState, extensions: &[&str]) -> bool {
use std::fs;
let prefix = state.params.prefix.clone();
let (dir, file_prefix) = if let Some(sep) = prefix.rfind('/') {
(&prefix[..sep + 1], &prefix[sep + 1..])
} else {
(".", prefix.as_str())
};
let entries = match fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return false,
};
state.begin_group("files", true);
let mut matched = false;
for entry in entries.flatten() {
let name = entry.file_name();
let name_str = name.to_string_lossy();
if !name_str.starts_with(file_prefix) {
continue;
}
let has_ext = extensions
.iter()
.any(|ext| name_str.ends_with(ext) || name_str.ends_with(&format!(".{}", ext)));
if has_ext || entry.path().is_dir() {
let full = if dir == "." {
name_str.to_string()
} else {
format!("{}{}", dir, name_str)
};
let mut comp = Completion::new(&full);
let is_dir = entry.path().is_dir();
if is_dir {
comp.modec = '/';
comp.suf = Some("/".to_string());
comp.flags |= CompletionFlags::NOSPACE;
} else if entry.path().is_symlink() {
comp.modec = '@';
} else {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(meta) = entry.metadata() {
if meta.permissions().mode() & 0o111 != 0 {
comp.modec = '*';
}
}
}
}
state.add_match(comp, Some("files"));
matched = true;
}
}
state.end_group();
matched
}
pub fn external_pwds(state: &mut CompletionState) -> bool {
if let Ok(pwd) = std::env::current_dir() {
state.add_match(Completion::new(pwd.to_string_lossy().to_string()), None);
true
} else {
false
}
}
pub fn history(state: &mut CompletionState, history_entries: &[String]) -> bool {
let prefix = state.params.prefix.clone();
state.begin_group("history", true);
let mut matched = false;
let mut seen = HashSet::new();
for entry in history_entries.iter().rev() {
if entry.starts_with(&prefix) && !seen.contains(entry) {
state.add_match(Completion::new(entry), Some("history"));
seen.insert(entry.clone());
matched = true;
}
}
state.end_group();
matched
}
pub fn ignored(state: &mut CompletionState, ignored_patterns: &[String]) -> bool {
let _ = ignored_patterns;
state.ignored > 0
}
pub fn list(state: &mut CompletionState) -> bool {
state.params.compstate.list.push_str(" list");
state.params.compstate.insert.clear();
true
}
pub fn match_pattern(state: &mut CompletionState, pattern: &str, candidates: &[String]) -> bool {
let mut matched = false;
for candidate in candidates {
if glob_match(pattern, candidate) {
state.add_match(Completion::new(candidate), None);
matched = true;
}
}
matched
}
pub fn menu(state: &mut CompletionState) -> bool {
state.params.compstate.insert = "menu".to_string();
true
}
pub fn oldlist(state: &mut CompletionState) -> bool {
state.params.compstate.old_list = "keep".to_string();
true
}
pub fn prefix_complete(
state: &mut CompletionState,
action: impl FnOnce(&mut CompletionState) -> bool,
) -> bool {
let saved_suffix = state.params.suffix.clone();
state.params.suffix.clear();
let result = action(state);
state.params.suffix = saved_suffix;
result
}
pub fn user_expand(state: &mut CompletionState, expansions: &HashMap<String, String>) -> bool {
let prefix = state.params.prefix.clone();
let mut matched = false;
for (pattern, expansion) in expansions {
if prefix.starts_with(pattern) {
let expanded = prefix.replacen(pattern, expansion, 1);
state.add_match(Completion::new(&expanded), None);
matched = true;
}
}
matched
}
pub fn bash_completions(state: &mut MainCompleteState, compspec: &str) -> CompleterResult {
let prefix = &state.comp.params.prefix.clone();
let mut matches = Vec::new();
let args: Vec<&str> = compspec.split_whitespace().collect();
let mut i = 0;
while i < args.len() {
match args[i] {
"-W" if i + 1 < args.len() => {
i += 1;
let wordlist = args[i].trim_matches('"').trim_matches('\'');
for word in wordlist.split_whitespace() {
if word.to_lowercase().starts_with(&prefix.to_lowercase()) {
matches.push(crate::Completion::new(word));
}
}
}
"-G" if i + 1 < args.len() => {
i += 1;
let pattern = args[i].trim_matches('"').trim_matches('\'');
if let Ok(entries) = std::fs::read_dir(".") {
for entry in entries.flatten() {
if let Some(name) = entry.file_name().to_str() {
if glob_match_simple(pattern, name)
&& name.to_lowercase().starts_with(&prefix.to_lowercase())
{
matches.push(crate::Completion::new(name));
}
}
}
}
}
"-d" => {
if let Ok(entries) = std::fs::read_dir(".") {
for entry in entries.flatten() {
if let Ok(ft) = entry.file_type() {
if ft.is_dir() {
if let Some(name) = entry.file_name().to_str() {
if name.to_lowercase().starts_with(&prefix.to_lowercase()) {
let mut c = crate::Completion::new(format!("{}/", name));
c.flags |= crate::CompletionFlags::FILE;
matches.push(c);
}
}
}
}
}
}
}
"-f" => {
if let Ok(entries) = std::fs::read_dir(".") {
for entry in entries.flatten() {
if let Some(name) = entry.file_name().to_str() {
if name.to_lowercase().starts_with(&prefix.to_lowercase()) {
let mut c = crate::Completion::new(name);
c.flags |= crate::CompletionFlags::FILE;
matches.push(c);
}
}
}
}
}
"-c" => {
if let Ok(path) = std::env::var("PATH") {
for dir in path.split(':') {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
if let Some(name) = entry.file_name().to_str() {
if name.to_lowercase().starts_with(&prefix.to_lowercase()) {
matches.push(crate::Completion::new(name));
}
}
}
}
}
}
}
"-u" => {
if let Ok(content) = std::fs::read_to_string("/etc/passwd") {
for line in content.lines() {
if let Some(user) = line.split(':').next() {
if user.to_lowercase().starts_with(&prefix.to_lowercase()) {
matches.push(crate::Completion::new(user));
}
}
}
}
}
"-F" | "-C" => {
i += 1;
}
"-X" | "-P" | "-S" if i + 1 < args.len() => {
i += 1;
}
_ => {}
}
i += 1;
}
if !matches.is_empty() {
let mut group = crate::CompletionGroup::new("bash-completion");
group.matches = matches;
state.comp.groups.push(group);
state.comp.nmatches += state
.comp
.groups
.last()
.map(|g| g.matches.len())
.unwrap_or(0);
CompleterResult::Matched
} else {
CompleterResult::NoMatch
}
}
fn glob_match_simple(pattern: &str, text: &str) -> bool {
let pattern = pattern.as_bytes();
let text = text.as_bytes();
fn match_impl(p: &[u8], t: &[u8]) -> bool {
match (p.first(), t.first()) {
(None, None) => true,
(Some(b'*'), _) => match_impl(&p[1..], t) || (!t.is_empty() && match_impl(p, &t[1..])),
(Some(b'?'), Some(_)) => match_impl(&p[1..], &t[1..]),
(Some(a), Some(b)) if a == b => match_impl(&p[1..], &t[1..]),
_ => false,
}
}
match_impl(pattern, text)
}
pub fn complete_debug(state: &mut MainCompleteState) -> CompleterResult {
eprintln!("Context: {}", state.ctx.context);
eprintln!("Completer: {}", state.ctx.completer);
eprintln!("Prefix: {}", state.comp.params.prefix);
eprintln!("Suffix: {}", state.comp.params.suffix);
eprintln!("Words: {:?}", state.comp.params.words);
eprintln!("Current: {}", state.comp.params.current);
CompleterResult::NoMatch
}
pub fn complete_help(state: &mut CompletionState, help_entries: &[(String, String)]) -> bool {
state.begin_group("help", true);
for (topic, desc) in help_entries {
let mut comp = Completion::new(topic);
comp.disp = Some(format!("{} -- {}", topic, desc));
state.add_match(comp, Some("help"));
}
state.end_group();
!help_entries.is_empty()
}
pub fn complete_tag(
state: &mut MainCompleteState,
tag: &str,
action: impl FnOnce(&mut CompletionState) -> bool,
) -> bool {
if state.tags.requested(tag) {
state.comp.begin_group(tag, true);
let result = action(&mut state.comp);
state.comp.end_group();
result
} else {
false
}
}
pub fn correct_filename(state: &mut CompletionState) -> bool {
use std::fs;
let prefix = state.params.prefix.clone();
let (dir, file_prefix) = if let Some(sep) = prefix.rfind('/') {
(&prefix[..sep + 1], &prefix[sep + 1..])
} else {
(".", prefix.as_str())
};
let entries = match fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return false,
};
let mut matched = false;
for entry in entries.flatten() {
let name = entry.file_name();
let name_str = name.to_string_lossy();
if edit_distance(file_prefix, &name_str) <= 2 {
let full = if dir == "." {
name_str.to_string()
} else {
format!("{}{}", dir, name_str)
};
state.add_match(Completion::new(&full), None);
matched = true;
}
}
matched
}
pub fn correct_word(state: &mut CompletionState, words: &[String]) -> bool {
let prefix = state.params.prefix.clone();
let mut matched = false;
for word in words {
if edit_distance(&prefix, word) <= 2 {
state.add_match(Completion::new(word), None);
matched = true;
}
}
matched
}
pub fn expand_word(state: &mut CompletionState) -> bool {
expand(state)
}
pub fn generic(
state: &mut MainCompleteState,
action: impl FnOnce(&mut MainCompleteState) -> CompleterResult,
) -> CompleterResult {
action(state)
}
pub fn history_complete_word(
state: &mut CompletionState,
history_entries: &[String],
direction: i32, ) -> bool {
let prefix = state.params.prefix.clone();
let iter: Box<dyn Iterator<Item = &String>> = if direction < 0 {
Box::new(history_entries.iter().rev())
} else {
Box::new(history_entries.iter())
};
for entry in iter {
for word in entry.split_whitespace() {
if word.starts_with(&prefix) && word != prefix {
state.add_match(Completion::new(word), None);
return true;
}
}
}
false
}
pub fn most_recent_file(state: &mut CompletionState, dir: &str, pattern: Option<&str>) -> bool {
use std::fs;
let entries = match fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return false,
};
let mut files: Vec<_> = entries
.filter_map(|e| e.ok())
.filter(|e| {
if let Some(pat) = pattern {
glob_match(pat, &e.file_name().to_string_lossy())
} else {
true
}
})
.filter_map(|e| {
let meta = e.metadata().ok()?;
let modified = meta.modified().ok()?;
Some((e, modified))
})
.collect();
files.sort_by(|a, b| b.1.cmp(&a.1));
if let Some((entry, _)) = files.first() {
let name = entry.file_name();
let full = format!("{}/{}", dir, name.to_string_lossy());
state.add_match(Completion::new(&full), None);
true
} else {
false
}
}
pub fn next_tags(state: &mut MainCompleteState) -> bool {
state.tags.next()
}
pub fn read_comp(state: &mut CompletionState, file: &str) -> bool {
let contents = match std::fs::read_to_string(file) {
Ok(c) => c,
Err(_) => return false,
};
let prefix = state.params.prefix.clone();
let mut matched = false;
for line in contents.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if line.starts_with(&prefix) {
state.add_match(Completion::new(line), None);
matched = true;
}
}
matched
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_glob_match() {
assert!(glob_match("*.rs", "foo.rs"));
assert!(glob_match("foo*", "foobar"));
assert!(glob_match("f?o", "foo"));
assert!(!glob_match("*.rs", "foo.txt"));
}
#[test]
fn test_edit_distance() {
assert_eq!(edit_distance("kitten", "sitting"), 3);
assert_eq!(edit_distance("foo", "foo"), 0);
assert_eq!(edit_distance("foo", "bar"), 3);
assert_eq!(edit_distance("", "abc"), 3);
}
#[test]
fn test_compiled_arg_spec() {
let spec = CompiledArgSpec::parse("*:file:_files").unwrap();
assert_eq!(spec.pattern, "*");
assert_eq!(spec.description, "file");
assert_eq!(spec.action, "_files");
}
}