#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CompResult {
NoMatch,
Single(String),
Ambiguous {
prefix: String,
matches: Vec<String>,
},
Menu {
current: usize,
matches: Vec<String>,
},
}
pub fn instmatch(
buffer: &str,
cursor: usize,
word_start: usize,
word_end: usize,
replacement: &str,
) -> (String, usize) {
let mut result = String::with_capacity(buffer.len() + replacement.len());
result.push_str(&buffer[..word_start]);
result.push_str(replacement);
result.push_str(&buffer[word_end..]);
let new_cursor = word_start + replacement.len();
(result, new_cursor)
}
pub fn unambig_data(matches: &[String]) -> String {
if matches.is_empty() {
return String::new();
}
if matches.len() == 1 {
return matches[0].clone();
}
let first = &matches[0];
let mut prefix_len = first.len();
for m in &matches[1..] {
let common = first
.chars()
.zip(m.chars())
.take_while(|(a, b)| a == b)
.count();
prefix_len = prefix_len.min(common);
}
first[..first
.char_indices()
.nth(prefix_len)
.map(|(i, _)| i)
.unwrap_or(first.len())]
.to_string()
}
pub fn unambig_data_icase(matches: &[String]) -> String {
if matches.is_empty() {
return String::new();
}
if matches.len() == 1 {
return matches[0].clone();
}
let first = matches[0].to_lowercase();
let mut prefix_len = first.len();
for m in &matches[1..] {
let lower = m.to_lowercase();
let common = first
.chars()
.zip(lower.chars())
.take_while(|(a, b)| a == b)
.count();
prefix_len = prefix_len.min(common);
}
let first = &matches[0];
first[..first
.char_indices()
.nth(prefix_len)
.map(|(i, _)| i)
.unwrap_or(first.len())]
.to_string()
}
pub fn do_single(
buffer: &str,
cursor: usize,
word_start: usize,
word_end: usize,
the_match: &str,
add_space: bool,
) -> (String, usize) {
let suffix = if add_space { " " } else { "" };
let replacement = format!("{}{}", the_match, suffix);
instmatch(buffer, cursor, word_start, word_end, &replacement)
}
pub fn do_ambiguous(matches: &[String]) -> CompResult {
let prefix = unambig_data(matches);
if prefix.is_empty() && matches.is_empty() {
CompResult::NoMatch
} else {
CompResult::Ambiguous {
prefix,
matches: matches.to_vec(),
}
}
}
pub fn do_allmatches(
buffer: &str,
cursor: usize,
word_start: usize,
word_end: usize,
matches: &[String],
separator: &str,
) -> (String, usize) {
let all = matches.join(separator);
instmatch(buffer, cursor, word_start, word_end, &all)
}
pub fn do_menucmp(matches: &[String], current: usize, forward: bool) -> (usize, &str) {
if matches.is_empty() {
return (0, "");
}
let next = if forward {
(current + 1) % matches.len()
} else {
if current == 0 {
matches.len() - 1
} else {
current - 1
}
};
(next, &matches[next])
}
pub fn accept_last(
buffer: &str,
cursor: usize,
word_start: usize,
word_end: usize,
selected: &str,
) -> (String, usize) {
do_single(buffer, cursor, word_start, word_end, selected, true)
}
pub fn valid_match(word: &str, prefix: &str, suffix: &str) -> bool {
word.starts_with(prefix) && (suffix.is_empty() || word.ends_with(suffix))
}
pub fn hasbrpsfx(s: &str) -> bool {
s.contains('{') || s.contains('}')
}
pub fn build_pos_string(current: usize, total: usize) -> String {
format!("{}/{}", current + 1, total)
}
pub fn cut_cline(s: &str, max_len: usize) -> String {
if s.len() <= max_len {
s.to_string()
} else {
format!("{}...", &s[..max_len.saturating_sub(3)])
}
}
pub fn cline_str(prefix: &str, line: &str, suffix: &str) -> String {
format!("{}{}{}", prefix, line, suffix)
}
pub fn list_lines(matches: &[String], columns: usize) -> usize {
if columns == 0 {
return matches.len();
}
(matches.len() + columns - 1) / columns
}
pub fn skipnolist(matches: &[String], list_max: usize) -> bool {
matches.len() > list_max && list_max > 0
}
pub fn comp_list(nmatches: usize, term_lines: usize) -> bool {
nmatches < term_lines
}
pub fn asklist(nmatches: usize) -> String {
format!("zsh: do you wish to see all {} possibilities? ", nmatches)
}
pub fn ztat(path: &str) -> Option<std::fs::Metadata> {
std::fs::metadata(path).ok()
}
pub fn comp_mod(result: &str, to_end: bool) -> String {
if to_end {
format!("{} ", result) } else {
result.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_unambig_data() {
assert_eq!(unambig_data(&["foobar".into(), "foobaz".into()]), "fooba");
assert_eq!(unambig_data(&["abc".into()]), "abc");
assert_eq!(unambig_data(&[]), "");
}
#[test]
fn test_instmatch() {
let (result, cursor) = instmatch("git co", 6, 4, 6, "commit");
assert_eq!(result, "git commit");
assert_eq!(cursor, 10);
}
#[test]
fn test_do_single() {
let (result, cursor) = do_single("git co", 6, 4, 6, "commit", true);
assert_eq!(result, "git commit ");
assert_eq!(cursor, 11);
}
#[test]
fn test_do_menucmp() {
let matches = vec!["commit".into(), "checkout".into(), "cherry-pick".into()];
let (next, word) = do_menucmp(&matches, 0, true);
assert_eq!(next, 1);
assert_eq!(word, "checkout");
let (next, word) = do_menucmp(&matches, 2, true);
assert_eq!(next, 0);
assert_eq!(word, "commit");
}
#[test]
fn test_valid_match() {
assert!(valid_match("foobar", "foo", ""));
assert!(valid_match("foobar", "foo", "bar"));
assert!(!valid_match("foobar", "baz", ""));
}
#[test]
fn test_build_pos_string() {
assert_eq!(build_pos_string(0, 10), "1/10");
assert_eq!(build_pos_string(9, 10), "10/10");
}
#[test]
fn test_list_lines() {
assert_eq!(list_lines(&vec!["a".into(); 10], 3), 4);
assert_eq!(list_lines(&vec!["a".into(); 6], 3), 2);
}
}