use std::path::PathBuf;
use super::file_finder::FileFinder;
use super::{CompletionItem, CompletionKind};
use crate::controllers::{BUILTIN_COMMANDS, SlashCommand};
pub trait Completer {
fn complete(&self, query: &str) -> Vec<CompletionItem>;
}
pub struct CommandCompleter {
extra_commands: Vec<SlashCommand>,
}
impl CommandCompleter {
pub fn new(extra: Option<&[SlashCommand]>) -> Self {
Self {
extra_commands: extra.map(|e| e.to_vec()).unwrap_or_default(),
}
}
pub fn add_commands(&mut self, commands: &[SlashCommand]) {
self.extra_commands.extend_from_slice(commands);
}
fn all_commands(&self) -> impl Iterator<Item = &SlashCommand> {
BUILTIN_COMMANDS.iter().chain(self.extra_commands.iter())
}
}
impl CommandCompleter {
pub fn complete_args(&self, command: &str, query: &str) -> Vec<CompletionItem> {
let candidates = match command {
"mode" => vec![
("plan", "Read-only tools, planning mode"),
("normal", "Full tool access, normal mode"),
],
"autonomy" => vec![
("manual", "All commands require approval"),
("semi-auto", "Safe commands auto-approved"),
("auto", "All commands auto-approved"),
],
"models" => vec![],
"model" | "session-models" => vec![
("gpt-4o", "OpenAI GPT-4o"),
("gpt-4o-mini", "OpenAI GPT-4o Mini"),
("claude-sonnet-4", "Anthropic Claude Sonnet 4"),
("claude-3-opus", "Anthropic Claude 3 Opus"),
("claude-3-haiku", "Anthropic Claude 3 Haiku"),
("gemini-1.5-pro", "Google Gemini 1.5 Pro"),
("deepseek-chat", "DeepSeek Chat"),
],
"mcp" => vec![
("list", "List MCP servers"),
("add", "Add an MCP server"),
("remove", "Remove an MCP server"),
("enable", "Enable an MCP server"),
("disable", "Disable an MCP server"),
],
"plugins" => vec![
("list", "List installed plugins"),
("install", "Install a plugin"),
("remove", "Remove a plugin"),
],
"agents" => vec![("list", "List available agents")],
"skills" => vec![("list", "List available skills")],
_ => vec![],
};
let query_lower = query.to_lowercase();
candidates
.into_iter()
.filter(|(name, _)| name.starts_with(&query_lower))
.map(|(name, desc)| CompletionItem {
insert_text: name.to_string(),
label: name.to_string(),
description: desc.to_string(),
kind: CompletionKind::Command,
score: 0.0,
})
.collect()
}
}
impl Completer for CommandCompleter {
fn complete(&self, query: &str) -> Vec<CompletionItem> {
let query_lower = query.to_lowercase();
self.all_commands()
.filter(|cmd| cmd.name.starts_with(&query_lower))
.map(|cmd| CompletionItem {
insert_text: format!("/{}", cmd.name),
label: format!("/{}", cmd.name),
description: cmd.description.to_string(),
kind: CompletionKind::Command,
score: 0.0, })
.collect()
}
}
pub struct FileCompleter {
finder: FileFinder,
}
impl FileCompleter {
pub fn new(working_dir: PathBuf) -> Self {
Self {
finder: FileFinder::new(working_dir),
}
}
}
impl Completer for FileCompleter {
fn complete(&self, query: &str) -> Vec<CompletionItem> {
let paths = self.finder.find_files(query, 50);
paths
.into_iter()
.map(|rel| {
let is_dir = self.finder.working_dir().join(&rel).is_dir();
let display = if is_dir {
format!("{}/", rel.display())
} else {
rel.display().to_string()
};
CompletionItem {
insert_text: format!("@{}", display),
label: display,
description: if is_dir {
"dir".to_string()
} else {
super::formatters::CompletionFormatter::file_size_string(
&self.finder.working_dir().join(&rel),
)
},
kind: CompletionKind::File,
score: 0.0,
}
})
.collect()
}
}
pub struct SymbolCompleter {
symbols: Vec<(String, String)>, }
impl SymbolCompleter {
pub fn new() -> Self {
Self {
symbols: Vec::new(),
}
}
pub fn register_symbols(&mut self, symbols: Vec<(String, String)>) {
self.symbols = symbols;
}
}
impl Default for SymbolCompleter {
fn default() -> Self {
Self::new()
}
}
impl Completer for SymbolCompleter {
fn complete(&self, query: &str) -> Vec<CompletionItem> {
let query_lower = query.to_lowercase();
self.symbols
.iter()
.filter(|(name, _)| name.to_lowercase().contains(&query_lower))
.map(|(name, kind)| CompletionItem {
insert_text: name.clone(),
label: name.clone(),
description: kind.clone(),
kind: CompletionKind::Symbol,
score: 0.0,
})
.collect()
}
}
#[cfg(test)]
#[path = "completers_tests.rs"]
mod tests;