#[derive(Debug, Clone, Default)]
pub struct CompState {
pub current_word: String,
pub words: Vec<String>,
pub current: usize,
pub cursor_pos: usize,
pub prefix: String,
pub suffix: String,
pub buffer: String,
pub context: CompContext,
pub matches: Vec<CompMatch>,
pub active: bool,
pub list: bool,
pub insert: bool,
pub nmatches: usize,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum CompContext {
#[default]
Command,
Argument,
Redirect,
Assignment,
Subscript,
Math,
Condition,
Array,
Brace,
}
#[derive(Debug, Clone)]
pub struct CompMatch {
pub word: String,
pub description: Option<String>,
pub group: Option<String>,
pub prefix: String,
pub suffix: String,
pub display: Option<String>,
}
impl CompMatch {
pub fn new(word: &str) -> Self {
CompMatch {
word: word.to_string(),
description: None,
group: None,
prefix: String::new(),
suffix: String::new(),
display: None,
}
}
pub fn with_description(mut self, desc: &str) -> Self {
self.description = Some(desc.to_string());
self
}
}
pub fn init_completion(buffer: &str, cursor: usize) -> CompState {
let mut state = CompState::default();
state.buffer = buffer.to_string();
state.active = true;
let mut words = Vec::new();
let mut current = 0;
let mut word_start = 0;
let mut in_word = false;
let mut in_quote = false;
let mut quote_char = '\0';
for (i, c) in buffer.char_indices() {
if in_quote {
if c == quote_char {
in_quote = false;
}
continue;
}
if c == '\'' || c == '"' {
in_quote = true;
quote_char = c;
if !in_word {
word_start = i;
in_word = true;
}
continue;
}
if c.is_whitespace() {
if in_word {
words.push(buffer[word_start..i].to_string());
if cursor >= word_start && cursor <= i {
current = words.len();
}
in_word = false;
}
} else if !in_word {
word_start = i;
in_word = true;
}
}
if in_word {
words.push(buffer[word_start..].to_string());
if cursor >= word_start {
current = words.len();
}
}
if words.is_empty() || cursor >= buffer.len() {
words.push(String::new());
current = words.len();
}
state.words = words;
state.current = current;
if current > 0 && current <= state.words.len() {
state.current_word = state.words[current - 1].clone();
}
state
}
pub fn addmatch(state: &mut CompState, m: CompMatch) {
state.matches.push(m);
state.nmatches = state.matches.len();
}
pub fn get_user_var(
name: &str,
vars: &std::collections::HashMap<String, String>,
) -> Option<String> {
vars.get(name).cloned()
}
pub fn multiquote(s: &str, in_quotes: bool) -> String {
if in_quotes {
s.replace('\\', "\\\\").replace('\'', "\\'")
} else {
crate::utils::quote_string(s)
}
}
pub fn tildequote(s: &str) -> String {
if s.starts_with('~') {
format!("\\{}", s)
} else {
s.to_string()
}
}
pub fn rembslash(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut escape = false;
for c in s.chars() {
if escape {
result.push(c);
escape = false;
} else if c == '\\' {
escape = true;
} else {
result.push(c);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_init_completion() {
let state = init_completion("git commit -m ", 14);
assert_eq!(state.words, vec!["git", "commit", "-m", ""]);
assert!(state.active);
}
#[test]
fn test_addmatch() {
let mut state = CompState::default();
addmatch(&mut state, CompMatch::new("hello"));
addmatch(&mut state, CompMatch::new("world"));
assert_eq!(state.nmatches, 2);
}
#[test]
fn test_multiquote() {
assert_eq!(multiquote("it's", false), "'it'\\''s'");
}
#[test]
fn test_tildequote() {
assert_eq!(tildequote("~user"), "\\~user");
assert_eq!(tildequote("/home"), "/home");
}
#[test]
fn test_rembslash() {
assert_eq!(rembslash("hello\\ world"), "hello world");
assert_eq!(rembslash("no\\\\slash"), "no\\slash");
}
}