use super::main::Zle;
#[derive(Debug, Default, Clone)]
pub struct CompletionState {
pub in_menu: bool,
pub menu_index: usize,
pub completions: Vec<String>,
pub prefix: String,
pub suffix: String,
pub word_start: usize,
pub word_end: usize,
pub last_menu: bool,
}
#[derive(Debug, Clone)]
pub struct BraceInfo {
pub str_val: String,
pub pos: usize,
pub cur_pos: usize,
pub qpos: usize,
pub curlen: usize,
}
impl Zle {
pub fn complete_word(&mut self, state: &mut CompletionState) {
self.do_complete(state, false, false);
}
pub fn menu_complete(&mut self, state: &mut CompletionState) {
if state.in_menu && !state.completions.is_empty() {
state.menu_index = (state.menu_index + 1) % state.completions.len();
self.apply_completion(state);
} else {
self.do_complete(state, true, false);
}
}
pub fn reverse_menu_complete(&mut self, state: &mut CompletionState) {
if state.in_menu && !state.completions.is_empty() {
if state.menu_index == 0 {
state.menu_index = state.completions.len() - 1;
} else {
state.menu_index -= 1;
}
self.apply_completion(state);
}
}
pub fn expand_or_complete(&mut self, state: &mut CompletionState) {
if !self.try_expand() {
self.do_complete(state, false, false);
}
}
pub fn expand_or_complete_prefix(&mut self, state: &mut CompletionState) {
state.suffix = self.zleline[self.zlecs..].iter().collect();
self.expand_or_complete(state);
}
pub fn list_choices(&mut self, state: &mut CompletionState) {
self.do_complete(state, false, true);
if !state.completions.is_empty() {
println!();
for (i, c) in state.completions.iter().enumerate() {
if i > 0 && i % 5 == 0 {
println!();
}
print!("{:<16}", c);
}
println!();
self.resetneeded = true;
}
}
pub fn list_expand(&mut self) {
let word = self.get_word_at_cursor();
let expansions = self.do_expansion(&word);
if !expansions.is_empty() {
println!();
for exp in &expansions {
println!("{}", exp);
}
self.resetneeded = true;
}
}
pub fn expand_word(&mut self) {
let _ = self.try_expand();
}
pub fn expand_history(&mut self) {
let line: String = self.zleline.iter().collect();
let expanded = self.do_expand_hist(&line);
if expanded != line {
self.zleline = expanded.chars().collect();
self.zlell = self.zleline.len();
if self.zlecs > self.zlell {
self.zlecs = self.zlell;
}
self.resetneeded = true;
}
}
pub fn magic_space(&mut self) {
self.expand_history();
self.self_insert(' ');
}
pub fn delete_char_or_list(&mut self, state: &mut CompletionState) {
if self.zlecs < self.zlell {
self.delete_char();
} else {
self.list_choices(state);
}
}
pub fn accept_and_menu_complete(&mut self, state: &mut CompletionState) -> Option<String> {
let line = self.accept_line();
state.in_menu = false;
Some(line)
}
pub fn spell_word(&mut self) {
let word = self.get_word_at_cursor();
let _ = word;
}
fn do_complete(&mut self, state: &mut CompletionState, menu_mode: bool, list_only: bool) {
let (word_start, word_end) = self.get_word_bounds();
let word: String = self.zleline[word_start..word_end].iter().collect();
state.word_start = word_start;
state.word_end = word_end;
state.prefix = word.clone();
state.completions = self.get_completions(&word);
if state.completions.is_empty() {
return;
}
if list_only {
return;
}
if menu_mode || state.completions.len() > 1 {
state.in_menu = true;
state.menu_index = 0;
self.apply_completion(state);
} else if state.completions.len() == 1 {
state.menu_index = 0;
self.apply_completion(state);
state.in_menu = false;
}
}
fn apply_completion(&mut self, state: &CompletionState) {
if state.completions.is_empty() {
return;
}
let completion = &state.completions[state.menu_index];
self.zleline.drain(state.word_start..state.word_end);
self.zlell = self.zleline.len();
self.zlecs = state.word_start;
for c in completion.chars() {
self.zleline.insert(self.zlecs, c);
self.zlecs += 1;
}
self.zlell = self.zleline.len();
self.resetneeded = true;
}
fn get_word_at_cursor(&self) -> String {
let (start, end) = self.get_word_bounds();
self.zleline[start..end].iter().collect()
}
fn get_word_bounds(&self) -> (usize, usize) {
let mut start = self.zlecs;
let mut end = self.zlecs;
while start > 0 && !self.zleline[start - 1].is_whitespace() {
start -= 1;
}
while end < self.zlell && !self.zleline[end].is_whitespace() {
end += 1;
}
(start, end)
}
fn try_expand(&mut self) -> bool {
let word = self.get_word_at_cursor();
if word.is_empty() {
return false;
}
let expansions = self.do_expansion(&word);
if expansions.is_empty() || (expansions.len() == 1 && expansions[0] == word) {
return false;
}
let (start, end) = self.get_word_bounds();
self.zleline.drain(start..end);
self.zlecs = start;
let expanded = expansions.join(" ");
for c in expanded.chars() {
self.zleline.insert(self.zlecs, c);
self.zlecs += 1;
}
self.zlell = self.zleline.len();
self.resetneeded = true;
true
}
fn do_expansion(&self, word: &str) -> Vec<String> {
let mut results = Vec::new();
if word.contains('*') || word.contains('?') || word.contains('[') {
if let Ok(paths) = glob::glob(word) {
for path in paths.flatten() {
results.push(path.display().to_string());
}
}
}
if word.starts_with('~') {
if let Some(home) = std::env::var_os("HOME") {
let expanded = word.replacen('~', home.to_str().unwrap_or("~"), 1);
results.push(expanded);
}
}
if word.starts_with('$') {
let var_name = &word[1..];
if let Ok(val) = std::env::var(var_name) {
results.push(val);
}
}
if results.is_empty() {
results.push(word.to_string());
}
results
}
fn do_expand_hist(&self, line: &str) -> String {
let mut result = line.to_string();
if result.contains("!!") {
result = result.replace("!!", "[last-command]");
}
if result.contains("!$") {
result = result.replace("!$", "[last-arg]");
}
result
}
fn get_completions(&self, prefix: &str) -> Vec<String> {
let mut completions = Vec::new();
if prefix.contains('/') || prefix.starts_with('.') {
let dir = if let Some(pos) = prefix.rfind('/') {
&prefix[..=pos]
} else {
"./"
};
let file_prefix = if let Some(pos) = prefix.rfind('/') {
&prefix[pos + 1..]
} else {
prefix
};
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with(file_prefix) {
let full_path = if dir == "./" {
name
} else {
format!("{}{}", dir, name)
};
completions.push(full_path);
}
}
}
} else {
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() {
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with(prefix) {
if !completions.contains(&name) {
completions.push(name);
}
}
}
}
}
}
}
completions.sort();
completions
}
}
pub const META: char = '\u{83}';
pub fn metafy_line(s: &str) -> String {
let mut result = String::with_capacity(s.len() * 2);
for c in s.chars() {
if c == META || (c as u32) >= 0x83 {
result.push(META);
result.push(char::from_u32((c as u32) ^ 32).unwrap_or(c));
} else {
result.push(c);
}
}
result
}
pub fn unmetafy_line(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == META {
if let Some(&next) = chars.peek() {
chars.next();
result.push(char::from_u32((next as u32) ^ 32).unwrap_or(next));
}
} else {
result.push(c);
}
}
result
}
pub fn get_cur_cmd(line: &[char], cursor: usize) -> Option<String> {
let mut cmd_start = 0;
for i in 0..cursor {
let c = line[i];
if c == ';' || c == '|' || c == '&' || c == '(' || c == ')' || c == '`' {
cmd_start = i + 1;
}
}
while cmd_start < cursor && line[cmd_start].is_whitespace() {
cmd_start += 1;
}
let mut cmd_end = cmd_start;
while cmd_end < cursor && !line[cmd_end].is_whitespace() {
cmd_end += 1;
}
if cmd_start < cmd_end {
Some(line[cmd_start..cmd_end].iter().collect())
} else {
None
}
}
pub fn has_real_token(s: &str) -> bool {
let special = ['$', '`', '"', '\'', '\\', '{', '}', '[', ']', '*', '?', '~'];
let mut escaped = false;
for c in s.chars() {
if escaped {
escaped = false;
continue;
}
if c == '\\' {
escaped = true;
continue;
}
if special.contains(&c) {
return true;
}
}
false
}
pub fn pfx_len(s1: &str, s2: &str) -> usize {
s1.chars()
.zip(s2.chars())
.take_while(|(a, b)| a == b)
.count()
}
pub fn sfx_len(s1: &str, s2: &str) -> usize {
s1.chars()
.rev()
.zip(s2.chars().rev())
.take_while(|(a, b)| a == b)
.count()
}
pub fn quote_string(s: &str, style: QuoteStyle) -> String {
match style {
QuoteStyle::Single => format!("'{}'", s.replace('\'', "'\\''")),
QuoteStyle::Double => format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")),
QuoteStyle::Dollar => format!("$'{}'", s.replace('\\', "\\\\").replace('\'', "\\'")),
QuoteStyle::Backslash => {
let mut result = String::with_capacity(s.len() * 2);
for c in s.chars() {
if " \t\n\\'\"`$&|;()<>*?[]{}#~".contains(c) {
result.push('\\');
}
result.push(c);
}
result
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum QuoteStyle {
Single,
Double,
Dollar,
Backslash,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pfx_len() {
assert_eq!(pfx_len("hello", "help"), 3);
assert_eq!(pfx_len("abc", "xyz"), 0);
assert_eq!(pfx_len("test", "test"), 4);
}
#[test]
fn test_sfx_len() {
assert_eq!(sfx_len("testing", "running"), 3);
assert_eq!(sfx_len("abc", "xyz"), 0);
}
#[test]
fn test_quote_string() {
assert_eq!(quote_string("hello", QuoteStyle::Single), "'hello'");
assert_eq!(quote_string("it's", QuoteStyle::Single), "'it'\\''s'");
assert_eq!(quote_string("hello", QuoteStyle::Double), "\"hello\"");
}
#[test]
fn test_has_real_token() {
assert!(has_real_token("$HOME"));
assert!(has_real_token("*.txt"));
assert!(!has_real_token("hello"));
assert!(!has_real_token("test\\$var")); }
}