use std::borrow::Cow;
use tishlang_core::VmRef;
use rustyline::completion::{Completer, Pair};
use rustyline::highlight::Highlighter;
use rustyline::hint::Hinter;
use rustyline::validate::Validator;
use rustyline::Context;
use rustyline::Helper;
use tishlang_bytecode::{compile_for_repl, compile_for_repl_unoptimized};
use tishlang_parser;
use tishlang_vm::Vm;
const KEYWORDS: &[&str] = &[
"async", "await", "break", "case", "catch", "const", "continue", "default", "do", "else",
"export", "false", "finally", "for", "fn", "function", "if", "import", "in", "let", "null",
"of", "return", "switch", "throw", "true", "try", "typeof", "void", "while",
];
const ANSI_DIM: &str = "\x1b[2m";
const ANSI_RESET: &str = "\x1b[0m";
pub struct ReplCompleter {
pub vm: VmRef<Vm>,
pub no_optimize: bool,
}
impl ReplCompleter {
fn ident_prefix_at_cursor<'a>(&self, line_before_cursor: &'a str) -> (usize, &'a str) {
let s = line_before_cursor;
if s.is_empty() {
return (0, "");
}
let mut start = s.len();
for (i, c) in s.char_indices().rev() {
if c.is_alphanumeric() || c == '_' {
start = i;
} else {
break;
}
}
(start, &s[start..])
}
fn get_bare_completions(&self, line_before_cursor: &str) -> (usize, Vec<String>) {
let (start, prefix) = self.ident_prefix_at_cursor(line_before_cursor);
if prefix.is_empty() {
return (start, vec![]);
}
let mut names: Vec<String> = self.vm.borrow().global_names();
names.extend(KEYWORDS.iter().map(|s| (*s).to_string()));
names.sort();
names.dedup();
let filtered: Vec<String> = names
.into_iter()
.filter(|k| k.starts_with(prefix))
.collect();
(start, filtered)
}
fn get_dotted_completions(&self, line_before_cursor: &str) -> Option<(usize, Vec<String>)> {
let last_dot = line_before_cursor.rfind('.')?;
let prefix_expr = line_before_cursor[..last_dot].trim();
if prefix_expr.is_empty() {
return None;
}
let member_prefix = line_before_cursor[last_dot + 1..].trim();
let program = tishlang_parser::parse(prefix_expr).ok()?;
let compile_fn = if self.no_optimize {
compile_for_repl_unoptimized
} else {
compile_for_repl
};
let chunk = compile_fn(&program).ok()?;
let value = self.vm.borrow_mut().run_with_options(&chunk, true).ok()?;
let keys = value.completion_keys();
let filtered: Vec<String> = keys
.into_iter()
.filter(|k| k.starts_with(member_prefix))
.collect();
Some((last_dot + 1, filtered))
}
fn get_completions(&self, line_before_cursor: &str) -> (usize, Vec<String>) {
if line_before_cursor.contains('.') {
if let Some((start, filtered)) = self.get_dotted_completions(line_before_cursor) {
if !filtered.is_empty() || line_before_cursor.trim_end().ends_with('.') {
return (start, filtered);
}
}
}
self.get_bare_completions(line_before_cursor)
}
fn longest_common_prefix(items: &[String]) -> Option<String> {
let first = items.first()?;
let mut len = first.len();
for item in items.iter().skip(1) {
len = first
.bytes()
.zip(item.bytes())
.take_while(|(a, b)| a == b)
.count()
.min(len);
}
if len == 0 {
None
} else {
Some(first[..len].to_string())
}
}
}
impl Completer for ReplCompleter {
type Candidate = Pair;
fn complete(
&self,
line: &str,
pos: usize,
_ctx: &Context<'_>,
) -> rustyline::Result<(usize, Vec<Pair>)> {
let line_before_cursor = &line[..pos];
let (start, filtered) = self.get_completions(line_before_cursor);
let pairs: Vec<Pair> = filtered
.into_iter()
.map(|k| Pair {
display: k.clone(),
replacement: k,
})
.collect();
Ok((start, pairs))
}
}
impl Hinter for ReplCompleter {
type Hint = String;
fn hint(&self, line: &str, pos: usize, _ctx: &Context<'_>) -> Option<Self::Hint> {
let line_before_cursor = &line[..pos];
let (start, filtered) = self.get_completions(line_before_cursor);
if filtered.is_empty() {
return None;
}
let member_prefix = line_before_cursor.get(start..).unwrap_or("").trim();
let hint = if filtered.len() == 1 {
filtered[0].clone()
} else if let Some(lcp) = Self::longest_common_prefix(&filtered) {
if lcp.len() > member_prefix.len() {
lcp
} else {
filtered[0].clone()
}
} else {
filtered[0].clone()
};
if hint.starts_with(member_prefix) && hint.len() > member_prefix.len() {
Some(hint[member_prefix.len()..].to_string())
} else if hint == member_prefix {
None
} else {
Some(hint)
}
}
}
impl Highlighter for ReplCompleter {
fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
if hint.is_empty() {
return Cow::Borrowed(hint);
}
Cow::Owned(format!("{ANSI_DIM}{hint}{ANSI_RESET}"))
}
}
impl Validator for ReplCompleter {}
impl Helper for ReplCompleter {}