use crate::{
state::CompletionContext as StateContext, CompParams, CompState, Completion, CompletionGroup,
CompletionReceiver, ZStyleStore,
};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct ShellCompletionContext {
pub words: Vec<String>,
pub current: i32,
pub prefix: String,
pub suffix: String,
pub curcontext: String,
pub service: String,
pub compstate: HashMap<String, String>,
}
impl Default for ShellCompletionContext {
fn default() -> Self {
Self {
words: Vec::new(),
current: 1,
prefix: String::new(),
suffix: String::new(),
curcontext: String::new(),
service: String::new(),
compstate: HashMap::new(),
}
}
}
impl ShellCompletionContext {
pub fn from_command_line(line: &str, cursor_pos: usize) -> Self {
let words: Vec<String> = line.split_whitespace().map(String::from).collect();
let mut current: i32 = (words.len() + 1) as i32; let mut prefix = String::new();
let mut suffix = String::new();
let mut found = false;
let mut pos = 0;
for (i, word) in words.iter().enumerate() {
let word_start = line[pos..].find(word).map(|p| pos + p).unwrap_or(pos);
let word_end = word_start + word.len();
if cursor_pos >= word_start && cursor_pos <= word_end {
current = (i + 1) as i32;
prefix = word[..cursor_pos.saturating_sub(word_start)].to_string();
suffix = word[cursor_pos.saturating_sub(word_start)..].to_string();
found = true;
break;
}
pos = word_end;
}
if !found && !words.is_empty() {
pos = 0;
for (i, word) in words.iter().enumerate() {
let word_start = line[pos..].find(word).map(|p| pos + p).unwrap_or(pos);
if cursor_pos < word_start {
current = (i + 1) as i32;
break;
}
pos = word_start + word.len();
}
}
let service = words.first().cloned().unwrap_or_default();
let curcontext = format!(":completion::complete:{}:", service);
Self {
words,
current,
prefix,
suffix,
curcontext,
service,
compstate: Self::default_compstate(),
}
}
fn default_compstate() -> HashMap<String, String> {
let mut state = HashMap::new();
state.insert("context".to_string(), "command".to_string());
state.insert("insert".to_string(), "unambiguous".to_string());
state.insert("list".to_string(), "list".to_string());
state
}
pub fn to_comp_params(&self) -> CompParams {
CompParams {
words: self.words.clone(),
current: self.current,
prefix: self.prefix.clone(),
suffix: self.suffix.clone(),
iprefix: String::new(),
isuffix: String::new(),
qiprefix: String::new(),
qisuffix: String::new(),
compstate: self.to_comp_state(),
}
}
pub fn to_comp_state(&self) -> CompState {
CompState {
context: StateContext::Command,
parameter: String::new(),
redirect: String::new(),
quote: String::new(),
quoting: String::new(),
restore: String::new(),
list: self.compstate.get("list").cloned().unwrap_or_default(),
insert: self.compstate.get("insert").cloned().unwrap_or_default(),
exact: String::new(),
exact_string: String::new(),
pattern_insert: String::new(),
pattern_match: String::new(),
unambiguous: String::new(),
unambiguous_cursor: 0,
unambiguous_positions: String::new(),
insert_positions: String::new(),
list_max: 0,
last_prompt: String::new(),
to_end: String::new(),
old_list: String::new(),
old_insert: String::new(),
vared: String::new(),
list_lines: 0,
nmatches: 0,
ignored: 0,
all_quotes: String::new(),
}
}
}
#[derive(Debug, Default)]
pub struct CompletionResult {
pub groups: Vec<CompletionGroup>,
pub messages: Vec<String>,
pub success: bool,
pub status: i32,
}
pub trait CompletionRunner {
fn run_completion_function(
&mut self,
func_name: &str,
context: &ShellCompletionContext,
zstyle: &ZStyleStore,
) -> CompletionResult;
fn has_completion_function(&self, name: &str) -> bool;
fn get_completer(&self, command: &str) -> Option<String>;
}
pub struct BuiltinDispatcher {
pub receiver: CompletionReceiver,
messages: Vec<String>,
zstyle: ZStyleStore,
}
impl Default for BuiltinDispatcher {
fn default() -> Self {
Self::new()
}
}
impl BuiltinDispatcher {
pub fn new() -> Self {
Self {
receiver: CompletionReceiver::unlimited(),
messages: Vec::new(),
zstyle: ZStyleStore::new(),
}
}
pub fn with_zstyle(zstyle: ZStyleStore) -> Self {
Self {
receiver: CompletionReceiver::unlimited(),
messages: Vec::new(),
zstyle,
}
}
pub fn message(&mut self, msg: &str) {
self.messages.push(msg.to_string());
}
pub fn zstyle_lookup(&self, context: &str, style: &str) -> Option<String> {
self.zstyle.lookup_str(context, style).map(String::from)
}
pub fn begin_group(&mut self, name: &str, sorted: bool) {
self.receiver.begin_group(name, sorted);
}
pub fn end_group(&mut self) {
self.receiver.begin_group("default", true);
}
pub fn add_completion(&mut self, comp: Completion) {
self.receiver.add(comp);
}
pub fn add_described(
&mut self,
tag: &str,
description: &str,
items: &[(String, Option<String>)],
) {
self.receiver.begin_group(tag, true);
self.receiver.add_explanation(description);
for (value, desc) in items {
let mut comp = Completion::new(value);
if let Some(d) = desc {
comp = comp.with_description(d);
}
self.receiver.add(comp);
}
self.receiver.begin_group("default", true);
}
pub fn add_files(&mut self, prefix: &str, dirs_only: bool) {
use std::path::Path;
let dir = if prefix.is_empty() {
Path::new(".")
} else if prefix.ends_with('/') {
Path::new(prefix)
} else {
Path::new(prefix).parent().unwrap_or(Path::new("."))
};
let file_prefix = if prefix.contains('/') {
prefix.rsplit('/').next().unwrap_or("")
} else {
prefix
};
if let Ok(entries) = std::fs::read_dir(dir) {
let tag = if dirs_only { "directories" } else { "files" };
self.receiver.begin_group(tag, true);
for entry in entries.flatten() {
if let Some(name) = entry.file_name().to_str() {
if !name.starts_with(file_prefix) && !file_prefix.is_empty() {
continue;
}
let is_dir = entry.file_type().map(|t| t.is_dir()).unwrap_or(false);
if dirs_only && !is_dir {
continue;
}
let comp = if is_dir {
Completion::new(name).with_suffix("/")
} else {
Completion::new(name)
};
self.receiver.add(comp);
}
}
self.receiver.begin_group("default", true);
}
}
pub fn finish(self) -> CompletionResult {
let groups = self.receiver.take();
let success = groups.iter().any(|g| !g.matches.is_empty()) || !self.messages.is_empty();
CompletionResult {
groups,
messages: self.messages,
success,
status: if success { 0 } else { 1 },
}
}
}
pub fn call_program(
_tag: &str,
command: &str,
args: &[&str],
) -> Result<Vec<String>, std::io::Error> {
use std::process::Command;
let output = Command::new(command).args(args).output()?;
if !output.status.success() {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("{} failed with status {}", command, output.status),
));
}
let stdout = String::from_utf8_lossy(&output.stdout);
Ok(stdout.lines().map(String::from).collect())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_context_from_command_line() {
let ctx = ShellCompletionContext::from_command_line("git add ", 8);
assert_eq!(ctx.words, vec!["git", "add"]);
assert_eq!(ctx.current, 3); assert_eq!(ctx.service, "git");
}
#[test]
fn test_context_mid_word() {
let ctx = ShellCompletionContext::from_command_line("git com", 6);
assert_eq!(ctx.words, vec!["git", "com"]);
assert_eq!(ctx.current, 2);
assert_eq!(ctx.prefix, "co");
assert_eq!(ctx.suffix, "m");
}
#[test]
fn test_builtin_dispatcher() {
let mut dispatcher = BuiltinDispatcher::new();
dispatcher.begin_group("commands", true);
dispatcher.add_completion(Completion::new("add"));
dispatcher.add_completion(Completion::new("commit"));
dispatcher.end_group();
let result = dispatcher.finish();
assert!(result.success);
assert!(!result.groups.is_empty());
}
#[test]
fn test_call_program() {
let result = call_program("test", "echo", &["hello"]);
assert!(result.is_ok());
let lines = result.unwrap();
assert_eq!(lines, vec!["hello"]);
}
}