pub mod completers;
pub mod file_finder;
pub mod formatters;
pub mod strategies;
use crate::controllers::SlashCommand;
use completers::{CommandCompleter, Completer, FileCompleter, SymbolCompleter};
use formatters::CompletionFormatter;
use strategies::CompletionStrategy;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CompletionKind {
Command,
File,
Symbol,
}
#[derive(Debug, Clone)]
pub struct CompletionItem {
pub insert_text: String,
pub label: String,
pub description: String,
pub kind: CompletionKind,
pub score: f64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Trigger {
Slash,
SlashArg { command: String },
At,
Tab,
}
pub fn detect_trigger(text_before_cursor: &str) -> Option<(Trigger, String)> {
if let Some(pos) = text_before_cursor.rfind('@') {
let after_at = &text_before_cursor[pos + 1..];
if !after_at.contains(' ') {
return Some((Trigger::At, after_at.to_string()));
}
}
if let Some(pos) = text_before_cursor.rfind('/') {
let valid_start = pos == 0
|| text_before_cursor
.as_bytes()
.get(pos - 1)
.map(|&b| b == b' ' || b == b'\t' || b == b'\n')
.unwrap_or(false);
if valid_start {
let after_slash = &text_before_cursor[pos + 1..];
if after_slash.contains(' ') {
let parts: Vec<&str> = after_slash.splitn(2, ' ').collect();
let command = parts[0].to_string();
let arg_query = parts.get(1).copied().unwrap_or("").to_string();
return Some((Trigger::SlashArg { command }, arg_query));
}
return Some((Trigger::Slash, after_slash.to_string()));
}
}
None
}
impl std::fmt::Debug for AutocompleteEngine {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AutocompleteEngine")
.field("visible", &self.visible)
.field("selected", &self.selected)
.field("items_count", &self.items.len())
.finish()
}
}
pub struct AutocompleteEngine {
command_completer: CommandCompleter,
file_completer: FileCompleter,
symbol_completer: SymbolCompleter,
strategy: CompletionStrategy,
items: Vec<CompletionItem>,
selected: usize,
visible: bool,
trigger_len: usize,
}
impl AutocompleteEngine {
pub fn new(working_dir: std::path::PathBuf) -> Self {
Self {
command_completer: CommandCompleter::new(None),
file_completer: FileCompleter::new(working_dir),
symbol_completer: SymbolCompleter::new(),
strategy: CompletionStrategy::default(),
items: Vec::new(),
selected: 0,
visible: false,
trigger_len: 0,
}
}
pub fn update(&mut self, text_before_cursor: &str) {
match detect_trigger(text_before_cursor) {
Some((Trigger::Slash, ref query)) => {
self.items = self.command_completer.complete(query);
self.strategy.sort(&mut self.items);
self.selected = 0;
self.visible = !self.items.is_empty();
self.trigger_len = 1 + query.len(); }
Some((Trigger::SlashArg { ref command }, ref query)) => {
self.items = self.command_completer.complete_args(command, query);
self.strategy.sort(&mut self.items);
self.selected = 0;
self.visible = !self.items.is_empty();
self.trigger_len = query.len();
}
Some((Trigger::At, ref query)) => {
self.items = self.file_completer.complete(query);
self.strategy.sort(&mut self.items);
self.selected = 0;
self.visible = !self.items.is_empty();
self.trigger_len = 1 + query.len(); }
Some((Trigger::Tab, ref query)) => {
let mut results = self.file_completer.complete(query);
results.extend(self.symbol_completer.complete(query));
self.strategy.sort(&mut results);
self.items = results;
self.selected = 0;
self.visible = !self.items.is_empty();
self.trigger_len = query.len();
}
None => {
self.dismiss();
}
}
}
pub fn accept(&mut self) -> Option<(String, usize)> {
if !self.visible || self.items.is_empty() {
return None;
}
let item = &self.items[self.selected];
let insert = item.insert_text.clone();
let delete_count = self.trigger_len;
self.dismiss();
Some((insert, delete_count))
}
pub fn select_prev(&mut self) {
if !self.items.is_empty() {
self.selected = if self.selected == 0 {
self.items.len() - 1
} else {
self.selected - 1
};
}
}
pub fn select_next(&mut self) {
if !self.items.is_empty() {
self.selected = (self.selected + 1) % self.items.len();
}
}
pub fn dismiss(&mut self) {
self.visible = false;
self.items.clear();
self.selected = 0;
self.trigger_len = 0;
}
pub fn is_visible(&self) -> bool {
self.visible
}
pub fn items(&self) -> &[CompletionItem] {
&self.items
}
pub fn selected_index(&self) -> usize {
self.selected
}
pub fn render_popup(&self) -> Vec<(String, String, bool)> {
self.items
.iter()
.enumerate()
.map(|(i, item)| {
let display = CompletionFormatter::format(item);
(display.0, display.1, i == self.selected)
})
.collect()
}
pub fn record_frecency(&mut self, text: &str) {
self.strategy.record_access(text);
}
pub fn add_commands(&mut self, commands: &[SlashCommand]) {
self.command_completer.add_commands(commands);
}
pub fn set_working_dir(&mut self, dir: std::path::PathBuf) {
self.file_completer = FileCompleter::new(dir);
}
}
#[cfg(test)]
mod tests;