use std::fs;
use std::path::{Path, PathBuf};
use crate::app::{COPILOT_SUBCOMMANDS, GRAMMAR_SUBCOMMANDS};
use crate::config::COMMAND_BINDS;
use super::line_input::LineInput;
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum CompletionKind {
CommandName,
Path,
}
pub struct CompletionState {
pub kind: CompletionKind,
pub prefix: String,
pub head_chars: usize,
pub matches: Vec<String>,
pub selected: usize,
}
pub struct CommandPrompt {
pub input: LineInput,
pub completion: Option<CompletionState>,
}
impl CommandPrompt {
pub(super) fn new() -> Self {
Self {
input: LineInput::new(),
completion: None,
}
}
pub(super) fn tab(&mut self, step: i32, root: &Path) {
if self.completion.is_none() {
let Some(state) = build_completion(self.input.as_str(), root) else {
return;
};
self.completion = Some(state);
} else if let Some(c) = self.completion.as_mut() {
let len = c.matches.len() as i32;
let next = (c.selected as i32 + step).rem_euclid(len);
c.selected = next as usize;
}
if let Some(c) = &self.completion {
let head: String = self.input.as_str().chars().take(c.head_chars).collect();
let new = format!("{}{}", head, c.matches[c.selected]);
self.input = LineInput::new();
for ch in new.chars() {
self.input.insert(ch);
}
}
}
}
fn build_completion(input: &str, root: &Path) -> Option<CompletionState> {
match input.find(' ') {
None => {
let prefix = input.to_string();
let matches: Vec<String> = COMMAND_BINDS
.iter()
.flat_map(|b| b.all_names())
.chain(["copilot", "grammar"])
.filter(|n| n.starts_with(&prefix))
.map(|n| n.to_string())
.collect();
if matches.is_empty() {
return None;
}
Some(CompletionState {
kind: CompletionKind::CommandName,
prefix,
head_chars: 0,
matches,
selected: 0,
})
}
Some(sp_byte) => {
let cmd = &input[..sp_byte];
if cmd == "copilot" {
let partial = &input[sp_byte + 1..];
if partial.contains(' ') {
return None;
}
let matches: Vec<String> = COPILOT_SUBCOMMANDS
.iter()
.flat_map(|s| std::iter::once(s.name).chain(s.aliases.iter().copied()))
.filter(|n| n.starts_with(partial))
.map(|n| n.to_string())
.collect();
if matches.is_empty() {
return None;
}
return Some(CompletionState {
kind: CompletionKind::CommandName,
prefix: partial.to_string(),
head_chars: sp_byte + 1,
matches,
selected: 0,
});
}
if cmd == "grammar" {
let partial = &input[sp_byte + 1..];
if partial.contains(' ') {
return None;
}
let matches: Vec<String> = GRAMMAR_SUBCOMMANDS
.iter()
.flat_map(|s| std::iter::once(s.name).chain(s.aliases.iter().copied()))
.filter(|n| n.starts_with(partial))
.map(|n| n.to_string())
.collect();
if matches.is_empty() {
return None;
}
return Some(CompletionState {
kind: CompletionKind::CommandName,
prefix: partial.to_string(),
head_chars: sp_byte + 1,
matches,
selected: 0,
});
}
let bind = COMMAND_BINDS
.iter()
.find(|b| b.name == cmd || b.aliases.contains(&cmd))?;
if !bind.takes_path {
return None;
}
let partial = &input[sp_byte + 1..];
if partial.contains(' ') {
return None;
}
let matches = path_candidates(partial, root);
if matches.is_empty() {
return None;
}
let head_chars = cmd.chars().count() + 1;
Some(CompletionState {
kind: CompletionKind::Path,
prefix: partial.to_string(),
head_chars,
matches,
selected: 0,
})
}
}
}
fn path_candidates(partial: &str, root: &Path) -> Vec<String> {
let (dir_str, base_prefix) = match partial.rfind('/') {
Some(i) => (&partial[..=i], &partial[i + 1..]),
None => ("", partial),
};
let listing_dir: PathBuf = if dir_str.is_empty() {
root.to_path_buf()
} else {
let p = Path::new(dir_str);
if p.is_absolute() {
p.to_path_buf()
} else {
root.join(p)
}
};
let Ok(rd) = fs::read_dir(&listing_dir) else {
return Vec::new();
};
let mut out: Vec<String> = Vec::new();
for entry in rd.flatten() {
let name = entry.file_name();
let Some(name) = name.to_str() else { continue };
if name.starts_with('.') && !base_prefix.starts_with('.') {
continue;
}
if !name.starts_with(base_prefix) {
continue;
}
let is_dir = entry.file_type().map(|t| t.is_dir()).unwrap_or(false);
let mut s = String::with_capacity(dir_str.len() + name.len() + 1);
s.push_str(dir_str);
s.push_str(name);
if is_dir {
s.push('/');
}
out.push(s);
}
out.sort_by(|a, b| {
let a_dir = a.ends_with('/');
let b_dir = b.ends_with('/');
b_dir.cmp(&a_dir).then_with(|| a.cmp(b))
});
out.truncate(200);
out
}