use rustyline::Result;
use rusty_v8 as v8;
use std::task::Context;
#[derive(Debug, Clone)]
pub struct CompletionCandidate {
pub text: String,
pub display: Option<String>,
pub kind: CompletionKind,
}
#[derive(Debug, Clone, PartialEq)]
pub enum CompletionKind {
Variable,
Property,
Keyword,
Builtin,
Command,
File,
Other,
}
#[derive(Debug, Clone)]
pub struct CompletionContext {
pub line: String,
pub cursor_pos: usize,
pub before_cursor: String,
pub after_cursor: String,
}
pub struct ReplCompleter {
keywords: Vec<&'static str>,
builtins: HashMap<&'static str, Vec<&'static str>>,
commands: Vec<&'static str>,
isolate: Option<Arc<v8::Isolate>>,
context: Option<v8::Global<v8::Context>>,
}
impl ReplCompleter {
pub fn new() -> Self {
let mut builtins = HashMap::new();
builtins.insert("console", vec!["log", "error", "warn", "info", "debug"]);
builtins.insert("Object", vec!["keys", "values", "entries", "assign", "create"]);
builtins.insert("Array", vec!["push", "pop", "shift", "unshift", "map", "filter", "reduce"]);
builtins.insert("String", vec!["length", "substring", "replace", "split", "trim"]);
builtins.insert("Number", vec!["isNaN", "isInteger", "parseFloat", "parseInt"]);
builtins.insert("JSON", vec!["stringify", "parse"]);
builtins.insert("Math", vec!["random", "floor", "ceil", "round", "abs", "max", "min"]);
builtins.insert("Date", vec!["now", "parse", "UTC"]);
builtins.insert("Promise", vec!["resolve", "reject", "all", "race"]);
builtins.insert("Set", vec!["add", "delete", "has", "size", "clear"]);
builtins.insert("Map", vec!["set", "get", "has", "delete", "size", "clear"]);
builtins.insert("RegExp", vec!["test", "exec"]);
builtins.insert("JSON", vec!["stringify", "parse"]);
builtins.insert("globalThis", vec!["console", "Object", "Array", "String", "Number", "Math", "Date", "Promise", "Set", "Map", "RegExp", "JSON"]);
Self {
keywords: vec![
"break", "case", "catch", "class", "const", "continue", "debugger", "default",
"delete", "do", "else", "export", "extends", "finally", "for", "function",
"if", "import", "in", "instanceof", "new", "return", "super", "switch",
"this", "throw", "try", "typeof", "var", "void", "while", "with", "yield",
"let", "static", "enum", "await", "async", "implements", "interface",
"package", "private", "protected", "public",
],
builtins,
commands: vec![
".help", ".exit", ".quit", ".clear", ".history", ".load", ".save",
".inspect", ".time", ".type", ".await",
],
isolate: None,
context: None,
}
}
pub fn set_runtime(&mut self, isolate: Arc<v8::Isolate>, context: v8::Global<v8::Context>) {
self.isolate = Some(isolate);
self.context = Some(context);
}
pub fn complete(&self, line: &str, cursor: usize) -> Result<(usize, Vec<CompletionCandidate>)> {
let context: _ = self.extract_context(line, cursor);
let candidates: _ = self.find_completions(&context);
Ok((context.before_cursor.len(), candidates))
}
fn extract_context(&self, line: &str, cursor: usize) -> CompletionContext {
let before_cursor: _ = &line[..cursor];
let after_cursor: _ = &line[cursor..];
CompletionContext {
line: line.to_string(),
cursor_pos: cursor,
before_cursor: before_cursor.to_string(),
after_cursor: after_cursor.to_string(),
}
}
fn find_completions(&self, context: &CompletionContext) -> Vec<CompletionCandidate> {
let input: _ = context.before_cursor.trim_end();
if input.starts_with('.') {
return self.complete_commands(input);
}
if let Some(dot_pos) = input.rfind('.') {
let obj_part: _ = &input[..dot_pos];
let prop_part: _ = &input[dot_pos + 1..];
if let Some(props) = self.builtins.get(obj_part) {
return self.complete_properties(props, prop_part);
}
if let Some(props) = self.get_runtime_properties(obj_part) {
return self.complete_properties(&props, prop_part);
}
}
if let Some(space_pos) = input.rfind(' ') {
return vec![];
}
let mut candidates = Vec::new();
for keyword in &self.keywords {
if keyword.starts_with(input) {
candidates.push(CompletionCandidate {
text: keyword.to_string(),
display: None,
kind: CompletionKind::Keyword,
});
}
}
for builtin in self.builtins.keys() {
if builtin.starts_with(input) {
candidates.push(CompletionCandidate {
text: builtin.to_string(),
display: Some(format!("Built-in object: {}", builtin)),
kind: CompletionKind::Builtin,
});
}
}
if let Some(vars) = self.get_runtime_variables() {
for var in vars {
if var.starts_with(input) {
candidates.push(CompletionCandidate {
text: var.clone(),
display: Some("Variable from runtime".to_string()),
kind: CompletionKind::Variable,
});
}
}
}
candidates
}
fn complete_commands(&self, input: &str) -> Vec<CompletionCandidate> {
let mut candidates = Vec::new();
for cmd in &self.commands {
if cmd.starts_with(input) {
candidates.push(CompletionCandidate {
text: cmd.to_string(),
display: Some(self.get_command_description(cmd)),
kind: CompletionKind::Command,
});
}
}
candidates
}
fn get_command_description(&self, command: &str) -> String {
match command {
".help" => "Show help message".to_string(),
".exit" | ".quit" => "Exit the REPL".to_string(),
".clear" => "Clear the screen".to_string(),
".history" => "Show command history".to_string(),
".load" => "Load and execute a file".to_string(),
".save" => "Save session to a file".to_string(),
".inspect" => "Inspect object deeply".to_string(),
".time" => "Measure execution time".to_string(),
".type" => "Show type information".to_string(),
".await" => "Await a promise".to_string(),
_ => "REPL command".to_string(),
}
}
fn complete_properties(&self, properties: &[&str], input: &str) -> Vec<CompletionCandidate> {
let mut candidates = Vec::new();
for prop in properties {
if prop.starts_with(input) {
candidates.push(CompletionCandidate {
text: prop.to_string(),
display: Some(format!("Property of object",)),
kind: CompletionKind::Property,
});
}
}
candidates
}
fn get_runtime_properties(&self, _obj_name: &str) -> Option<Vec<&str>> {
None
}
fn get_runtime_variables(&self) -> Option<Vec<String>> {
None
}
pub fn get_common_prefix(candidates: &[CompletionCandidate]) -> String {
if candidates.is_empty() {
return String::new();
}
let first: _ = &candidates[0].text;
let mut common = first.to_string();
for candidate in &candidates[1..] {
let mut i = 0;
while i < common.len() && i < candidate.text.len() && common.chars().nth(i) == candidate.text.chars().nth(i) {
i += 1;
}
common.truncate(i);
if common.is_empty() {
break;
}
}
common
}
}
impl Default for ReplCompleter {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_completer_creation() {
let completer: _ = ReplCompleter::new();
assert!(!completer.keywords.is_empty());
assert!(!completer.builtins.is_empty());
assert!(!completer.commands.is_empty());
}
#[test]
fn test_complete_keywords() {
let completer: _ = ReplCompleter::new();
let (_, candidates) = completer.complete("fun", 3).unwrap();
assert!(!candidates.is_empty());
assert!(candidates.iter().any(|c| c.text == "function"));
}
#[test]
fn test_complete_builtins() {
let completer: _ = ReplCompleter::new();
let (_, candidates) = completer.complete("console", 7).unwrap();
assert!(!candidates.is_empty());
assert!(candidates.iter().any(|c| c.text == "console"));
}
#[test]
fn test_complete_commands() {
let completer: _ = ReplCompleter::new();
let (_, candidates) = completer.complete(".h", 2).unwrap();
assert!(!candidates.is_empty());
assert!(candidates.iter().any(|c| c.text == ".help"));
}
#[test]
fn test_complete_properties() {
let completer: _ = ReplCompleter::new();
let (_, candidates) = completer.complete("console.l", 8).unwrap();
assert!(!candidates.is_empty());
assert!(candidates.iter().any(|c| c.text == "log"));
}
#[test]
fn test_common_prefix() {
let candidates: _ = vec![
CompletionCandidate { text: "function".to_string(), display: None, kind: CompletionKind::Keyword },
CompletionCandidate { text: "for".to_string(), display: None, kind: CompletionKind::Keyword },
];
assert_eq!(ReplCompleter::get_common_prefix(&candidates), "");
let candidates: _ = vec![
CompletionCandidate { text: "console.log".to_string(), display: None, kind: CompletionKind::Property },
CompletionCandidate { text: "console.length".to_string(), display: None, kind: CompletionKind::Property },
];
assert_eq!(ReplCompleter::get_common_prefix(&candidates), "console.");
}
}
use std::collections::{BTreeMap, HashMap};
use std::sync::{Arc};
use std::fs::File;