use rustyline::completion::{Completer, FilenameCompleter, Pair};
use rustyline::highlight::Highlighter;
use rustyline::hint::Hinter;
use rustyline::validate::Validator;
use rustyline::{Context, Result};
use crate::slash::{slash_command_names, slash_command_takes_path};
pub struct ReplHelper {
filename_completer: FilenameCompleter,
}
impl ReplHelper {
pub fn new() -> Self {
Self {
filename_completer: FilenameCompleter::new(),
}
}
}
impl Default for ReplHelper {
fn default() -> Self {
Self::new()
}
}
impl Completer for ReplHelper {
type Candidate = Pair;
fn complete(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Result<(usize, Vec<Pair>)> {
complete_repl_line(&self.filename_completer, line, pos, ctx)
}
}
fn complete_repl_line(
filename_completer: &FilenameCompleter,
line: &str,
pos: usize,
ctx: &Context<'_>,
) -> Result<(usize, Vec<Pair>)> {
let prefix = &line[..pos];
if let Some(at) = mention_start(prefix) {
let (_, pairs) = filename_completer.complete(line, pos, ctx)?;
return Ok((at + 1, pairs));
}
let Some(rest) = prefix.strip_prefix('/') else {
return Ok((pos, Vec::new()));
};
if !rest.contains(char::is_whitespace) {
let mut candidates: Vec<Pair> = slash_command_names()
.filter(|name| name.starts_with(prefix))
.map(|name| Pair {
display: name.to_string(),
replacement: name.to_string(),
})
.collect();
candidates.sort_by(|a, b| a.display.cmp(&b.display));
return Ok((0, candidates));
}
let cmd_name = prefix.split_whitespace().next().unwrap_or("");
if slash_command_takes_path(cmd_name) {
return filename_completer.complete(line, pos, ctx);
}
Ok((pos, Vec::new()))
}
fn mention_start(prefix: &str) -> Option<usize> {
let at = prefix.rfind('@')?;
if prefix[at..].contains(char::is_whitespace) {
return None;
}
let starts_word = at == 0
|| prefix[..at]
.chars()
.next_back()
.is_some_and(char::is_whitespace);
starts_word.then_some(at)
}
impl Hinter for ReplHelper {
type Hint = String;
}
impl Highlighter for ReplHelper {}
impl Validator for ReplHelper {}
impl rustyline::Helper for ReplHelper {}