#[derive(Debug, PartialEq, Eq)]
pub enum Context<'a> {
Command,
Subcommand(&'a str),
Variable,
Argument,
}
pub fn word_at(line: &str, pos: usize) -> (usize, &str) {
let pos = pos.min(line.len());
let before = line.get(..pos).unwrap_or("");
let start = before
.char_indices()
.rev()
.find(|(_, c)| c.is_whitespace() || matches!(*c, ';' | '[' | ']' | '{' | '}' | '"'))
.map(|(i, c)| i + c.len_utf8())
.unwrap_or(0);
(start, line.get(start..pos).unwrap_or(""))
}
pub fn context_at<'a>(
line: &'a str,
start: usize,
word: &str,
ensemble: impl Fn(&str) -> bool,
) -> Context<'a> {
if word.starts_with('$') {
return Context::Variable;
}
let before = line.get(..start).unwrap_or("");
let command = match before.rfind(['\n', ';', '[']) {
Some(at) => &before[at + 1..],
None => before,
};
let mut words = command.split_whitespace();
match (words.next(), words.next()) {
(None, _) => Context::Command,
(Some(head), None) if ensemble(head) => Context::Subcommand(head),
_ => Context::Argument,
}
}
pub fn context_in_tcl<'a>(line: &'a str, start: usize, word: &str) -> Context<'a> {
context_at(line, start, word, |head| {
!crate::names::subcommands(head).is_empty()
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_word_keeps_its_sigil_and_stops_at_a_bracket() {
assert_eq!(word_at("puts $ab", 8), (5, "$ab"));
assert_eq!(word_at("puts [ll", 8), (6, "ll"));
assert_eq!(word_at("set x {a", 8), (7, "a"));
assert_eq!(word_at("", 0), (0, ""));
}
#[test]
fn a_word_is_read_up_to_the_cursor_only() {
assert_eq!(word_at("puts hello", 7), (5, "he"));
}
#[test]
fn the_head_of_a_command_names_a_command() {
for line in ["ls", "puts hi; ls", "puts [ls"] {
let (start, word) = word_at(line, line.len());
assert_eq!(
context_in_tcl(line, start, word),
Context::Command,
"{line}"
);
}
}
#[test]
fn the_word_after_an_ensemble_names_a_subcommand() {
let line = "string tou";
let (start, word) = word_at(line, line.len());
assert_eq!(
context_in_tcl(line, start, word),
Context::Subcommand("string")
);
for line in ["string toupper ab", "puts to"] {
let (start, word) = word_at(line, line.len());
assert_eq!(
context_in_tcl(line, start, word),
Context::Argument,
"{line}"
);
}
}
#[test]
fn a_dollar_names_a_variable_wherever_it_is() {
for line in ["puts $x", "$x"] {
let (start, word) = word_at(line, line.len());
assert_eq!(
context_in_tcl(line, start, word),
Context::Variable,
"{line}"
);
}
}
}