#[derive(Debug, Clone)]
pub struct LineContext {
line: String,
cursor: usize,
}
impl LineContext {
pub(crate) fn new(line: String, cursor: usize) -> Self {
let cursor = cursor.min(line.len());
let cursor = (0..=cursor)
.rev()
.find(|&i| line.is_char_boundary(i))
.unwrap_or(0);
LineContext { line, cursor }
}
pub fn line(&self) -> &str {
&self.line
}
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn word(&self) -> &str {
let before = &self.line[..self.cursor];
let start = before
.rfind(|c: char| c.is_ascii_whitespace())
.map(|i| i + 1)
.unwrap_or(0);
&before[start..]
}
pub fn word_start(&self) -> usize {
self.cursor - self.word().len()
}
}
#[derive(Debug, Clone, Default)]
pub struct Completion {
pub(crate) candidates: Vec<String>,
pub(crate) insertion: Option<String>,
}
impl Completion {
pub fn new(candidates: Vec<String>) -> Self {
Completion {
candidates,
insertion: None,
}
}
pub fn with_insertion(insertion: impl Into<String>, candidates: Vec<String>) -> Self {
Completion {
candidates,
insertion: Some(insertion.into()),
}
}
pub fn none() -> Self {
Completion {
candidates: Vec::new(),
insertion: None,
}
}
pub fn candidates(&self) -> &[String] {
&self.candidates
}
pub fn is_empty(&self) -> bool {
self.candidates.is_empty() && self.insertion.is_none()
}
}
pub trait Completer {
fn complete(&mut self, ctx: &LineContext) -> Completion;
}
impl<F> Completer for F
where
F: FnMut(&LineContext) -> Completion,
{
fn complete(&mut self, ctx: &LineContext) -> Completion {
(self)(ctx)
}
}
pub trait CandidateStyler {
fn style(&mut self, candidate: &str, out: &mut String);
}
impl<F> CandidateStyler for F
where
F: FnMut(&str, &mut String),
{
fn style(&mut self, candidate: &str, out: &mut String) {
(self)(candidate, out)
}
}
pub fn longest_common_prefix<S: AsRef<str>>(candidates: &[S]) -> String {
let mut iter = candidates.iter();
let Some(first) = iter.next() else {
return String::new();
};
let first = first.as_ref();
let mut prefix_len = first.chars().count();
for c in iter {
let common = first
.chars()
.zip(c.as_ref().chars())
.take_while(|(a, b)| a == b)
.count();
prefix_len = prefix_len.min(common);
if prefix_len == 0 {
break;
}
}
first.chars().take(prefix_len).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lcp_basic() {
let v = vec!["show".to_string(), "shutdown".to_string()];
assert_eq!(longest_common_prefix(&v), "sh");
}
#[test]
fn lcp_single() {
let v = vec!["show".to_string()];
assert_eq!(longest_common_prefix(&v), "show");
}
#[test]
fn lcp_none() {
let v = vec!["show".to_string(), "abort".to_string()];
assert_eq!(longest_common_prefix(&v), "");
}
#[test]
fn lcp_empty() {
assert_eq!(longest_common_prefix::<String>(&[]), "");
}
#[test]
fn lcp_unicode() {
let v = vec!["café".to_string(), "cafétière".to_string()];
assert_eq!(longest_common_prefix(&v), "café");
}
#[test]
fn word_extraction() {
let ctx = LineContext::new("show ip ro".to_string(), 10);
assert_eq!(ctx.word(), "ro");
assert_eq!(ctx.line(), "show ip ro");
}
#[test]
fn word_at_start() {
let ctx = LineContext::new("sh".to_string(), 2);
assert_eq!(ctx.word(), "sh");
assert_eq!(ctx.word_start(), 0);
}
#[test]
fn word_empty_after_space() {
let ctx = LineContext::new("show ".to_string(), 5);
assert_eq!(ctx.word(), "");
assert_eq!(ctx.word_start(), 5);
}
}