use {
crate::data::{pools::PoolDb, tags::TagDb},
bearask::{Autocomplete, Replacement},
owo_colors::OwoColorize,
std::sync::Arc,
};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PrefixChar {
None,
Exclude,
Wildcard,
Include,
}
impl PrefixChar {
#[inline]
#[bearive::argdoc]
pub fn find_from_str(
s: &str,
) -> (Self, &str) {
match s.as_bytes().first() {
Some(b'-') => (Self::Exclude, &s[1..]),
Some(b'~') => (Self::Wildcard, &s[1..]),
Some(b'+') => (Self::Include, &s[1..]),
_ => (Self::None, s),
}
}
#[inline]
pub fn as_str(self) -> &'static str {
match self {
Self::None => "",
Self::Exclude => "-",
Self::Wildcard => "~",
Self::Include => "+",
}
}
pub fn apply_color(self, formatted: String) -> String {
match self {
Self::Exclude => format!("{}{}", "-".red().bold(), formatted),
Self::Wildcard => format!("{}{}", "~".yellow().bold(), formatted),
Self::Include => format!("{}{}", "+".green().bold(), formatted),
Self::None => formatted,
}
}
}
#[inline]
pub fn get_current_token(input: &str) -> &str {
input.rsplit(char::is_whitespace).next().unwrap_or("")
}
#[inline]
pub fn get_prefix(input: &str) -> &str {
match input.rfind(char::is_whitespace) {
Some(idx) => &input[..=idx],
None => "",
}
}
#[inline]
pub fn strip_ansi(s: &str) -> String {
strip_ansi_escapes::strip_str(s)
}
pub fn extract_name_from_suggestion(suggestion: &str) -> String {
let stripped = strip_ansi(suggestion);
let cleaned = stripped.trim_start_matches(&['-', '~', '+'][..]);
if let Some(arrow_pos) = cleaned.find(" → ") {
cleaned[..arrow_pos].to_string()
} else {
cleaned.to_string()
}
}
pub trait AutocompleteDatabase {
fn autocomplete(&self, query: &str, limit: usize) -> Vec<String>;
fn resolve_name(&self, name: &str) -> String;
fn format_entry(&self, name: &str) -> String;
}
pub struct GenericAutocompleter<T: AutocompleteDatabase> {
pub db: Arc<T>,
pub limit: usize,
}
impl<T: AutocompleteDatabase> GenericAutocompleter<T> {
pub fn new(db: Arc<T>, limit: usize) -> Self {
Self { db, limit }
}
pub fn get_suggestions_impl(&self, input: &str) -> Vec<String> {
let current_token = get_current_token(input);
let (prefix_char, search_query) = PrefixChar::find_from_str(current_token);
if search_query.is_empty() {
return Vec::new();
}
let matches = self.db.autocomplete(search_query, self.limit);
let formatted: Vec<String> = matches
.into_iter()
.map(|name| {
let formatted = self.db.format_entry(&name);
prefix_char.apply_color(formatted)
})
.collect();
formatted
}
pub fn get_completion_impl(
&self,
input: &str,
highlighted_suggestion: Option<String>,
) -> Option<String> {
let suggestion = highlighted_suggestion?;
let prefix = get_prefix(input);
let current_token = get_current_token(input);
let (prefix_char, _) = PrefixChar::find_from_str(current_token);
let name = extract_name_from_suggestion(&suggestion);
let canonical_name = self.db.resolve_name(name.as_str());
let mut completion = String::with_capacity(
prefix.len() + prefix_char.as_str().len() + canonical_name.len() + 1,
);
completion.push_str(prefix);
completion.push_str(prefix_char.as_str());
completion.push_str(&canonical_name);
completion.push(' ');
Some(completion)
}
}
impl AutocompleteDatabase for TagDb {
fn autocomplete(&self, query: &str, limit: usize) -> Vec<String> {
self.autocomplete(query, limit)
}
fn resolve_name(&self, name: &str) -> String {
self.resolve_alias(name)
}
fn format_entry(&self, name: &str) -> String {
let canonical = self.resolve_alias(name);
if canonical != name {
format!(
"{} {} {}",
name.cyan(),
"→".bright_black(),
canonical.bright_green()
)
} else {
name.bright_white().to_string()
}
}
}
#[derive(Clone)]
pub struct TagAutocompleter {
pub inner: Arc<GenericAutocompleter<TagDb>>,
}
impl TagAutocompleter {
pub fn new(tag_db: Arc<TagDb>) -> Self {
Self::with_limit(tag_db, 10)
}
pub fn with_limit(tag_db: Arc<TagDb>, limit: usize) -> Self {
Self {
inner: Arc::new(GenericAutocompleter::new(tag_db, limit)),
}
}
}
impl Autocomplete for TagAutocompleter {
fn get_suggestions(&mut self, input: &str) -> Result<Vec<String>, String> {
Ok(self.inner.get_suggestions_impl(input))
}
fn get_completion(
&mut self,
input: &str,
highlighted_suggestion: Option<String>,
) -> Result<Replacement, String> {
Ok(self
.inner
.get_completion_impl(input, highlighted_suggestion))
}
}
impl AutocompleteDatabase for PoolDb {
fn autocomplete(&self, query: &str, limit: usize) -> Vec<String> {
self.autocomplete(query, limit)
}
fn resolve_name(&self, name: &str) -> String {
name.to_string()
}
fn format_entry(&self, name: &str) -> String {
name.bright_cyan().to_string()
}
}
#[derive(Clone)]
pub struct PoolAutocompleter {
pub inner: Arc<GenericAutocompleter<PoolDb>>,
}
impl PoolAutocompleter {
pub fn new(pool_db: Arc<PoolDb>) -> Self {
Self::with_limit(pool_db, 10)
}
pub fn with_limit(pool_db: Arc<PoolDb>, limit: usize) -> Self {
Self {
inner: Arc::new(GenericAutocompleter::new(pool_db, limit)),
}
}
}
impl Autocomplete for PoolAutocompleter {
fn get_suggestions(&mut self, input: &str) -> Result<Vec<String>, String> {
Ok(self.inner.get_suggestions_impl(input))
}
fn get_completion(
&mut self,
input: &str,
highlighted_suggestion: Option<String>,
) -> Result<Replacement, String> {
Ok(self
.inner
.get_completion_impl(input, highlighted_suggestion))
}
}