#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompletionKind {
Command,
Path,
Other,
}
#[derive(Debug)]
pub struct CompletionContext {
pub prefix: String,
pub kind: CompletionKind,
pub start: usize,
}
pub(super) fn tokenize(line: &str, pos: usize) -> Vec<(String, usize)> {
let slice = line.get(..pos).unwrap_or("");
let mut tokens = Vec::new();
let mut i = 0;
let bytes = slice.as_bytes();
while i < bytes.len() {
while i < bytes.len() && bytes[i].is_ascii_whitespace() {
i += 1;
}
if i >= bytes.len() {
break;
}
let token_start = i;
if i + 1 < bytes.len() && bytes[i] == b'2' && bytes[i + 1] == b'>' {
tokens.push(("2>".to_string(), token_start));
i += 2;
continue;
}
if bytes[i] == b'|' || bytes[i] == b'<' || bytes[i] == b'>' {
let ch = char::from(bytes[i]);
tokens.push((ch.to_string(), token_start));
i += 1;
continue;
}
let start = i;
while i < bytes.len() {
if bytes[i].is_ascii_whitespace() {
break;
}
if bytes[i] == b'|' || bytes[i] == b'<' || bytes[i] == b'>' {
break;
}
if bytes[i] == b'2' && i + 1 < bytes.len() && bytes[i + 1] == b'>' {
break;
}
i += 1;
}
let token = slice[start..i].to_string();
if !token.is_empty() {
tokens.push((token, start));
}
}
tokens
}
const PATH_TRIGGER_TOKENS: &[&str] = &[
"cd",
"ls",
"cat",
"mkdir",
"touch",
"export-readonly",
"export_readonly",
"source",
".",
">",
"2>",
"<",
];
pub(super) fn token_at_cursor(
line: &str,
tokens: &[(String, usize)],
pos: usize,
) -> Option<(String, usize)> {
if pos > line.len() {
return None;
}
for (token, start) in tokens {
let end = start + token.len();
if *start <= pos && end >= pos {
let prefix = line.get(*start..pos).unwrap_or("").to_string();
return Some((prefix, *start));
}
}
if !tokens.is_empty() {
let (last_token, last_start) = tokens.last().unwrap();
let last_end = last_start + last_token.len();
if pos >= last_end {
return Some((String::new(), pos));
}
}
None
}
#[must_use]
pub fn completion_context(line: &str, pos: usize) -> Option<CompletionContext> {
if line.is_empty() {
return None;
}
let line_len = line.len();
if pos > line_len {
return None;
}
let tokens = tokenize(line, pos);
let (prefix, start) = token_at_cursor(line, &tokens, pos)?;
let prefix = if prefix.is_empty() && start == pos && !tokens.is_empty() {
String::new()
} else if prefix.is_empty() && start == pos {
return None;
} else {
prefix
};
let token_index = tokens
.iter()
.position(|(t, s)| *s == start && t.as_str() == prefix.as_str())
.or({
if prefix.is_empty() {
Some(tokens.len())
} else {
None
}
});
let idx = token_index.unwrap_or_else(|| tokens.iter().take_while(|(_, s)| *s < start).count());
let kind = if idx == 0 {
CompletionKind::Command
} else {
let prev = tokens.get(idx.wrapping_sub(1)).map(|(t, _)| t.as_str());
if prev == Some("|") {
CompletionKind::Command
} else if prev.is_some_and(|p| PATH_TRIGGER_TOKENS.contains(&p)) {
CompletionKind::Path
} else {
CompletionKind::Other
}
};
Some(CompletionContext {
prefix,
kind,
start,
})
}