use std::sync::Arc;
use super::context::CompletionContext;
use super::fsm::CompletionState;
use super::provider::CandidateProvider;
use super::token_stream::TokenStream;
use crate::parser::{MongoLexer, SqlLexer};
#[derive(Debug, Clone, PartialEq)]
pub struct CompletionPair {
pub display: String,
pub replacement: String,
pub description: Option<String>,
}
pub struct CompletionEngine {
provider: Arc<dyn CandidateProvider>,
}
impl CompletionEngine {
pub fn new(provider: Arc<dyn CandidateProvider>) -> Self {
Self { provider }
}
pub fn complete(&self, line: &str, pos: usize) -> (usize, Vec<CompletionPair>) {
let stream = self.tokenize(line, pos);
let state = CompletionState::run(stream.tokens_before_cursor());
let context = state.to_context(&stream);
let mut candidates = self.fetch_candidates(&context);
let prefix = stream.current_prefix();
if !prefix.is_empty() {
candidates.retain(|c| c != &prefix);
}
let pairs: Vec<CompletionPair> = candidates
.into_iter()
.map(|c| CompletionPair {
display: c.clone(),
replacement: c,
description: None,
})
.collect();
(stream.completion_start(), pairs)
}
fn tokenize(&self, line: &str, cursor: usize) -> TokenStream {
if Self::is_sql_command(line) {
let tokens = SqlLexer::tokenize(line);
TokenStream::from_sql(tokens, cursor)
} else {
let tokens = MongoLexer::tokenize(line);
TokenStream::from_mongo(tokens, cursor)
}
}
fn is_sql_command(line: &str) -> bool {
let trimmed = line.trim();
trimmed.to_uppercase().starts_with("SELECT")
}
fn fetch_candidates(&self, context: &CompletionContext) -> Vec<String> {
match context {
CompletionContext::Collection { prefix } => self.provider.collections(prefix),
CompletionContext::Operation { prefix } => self.provider.operations(prefix),
CompletionContext::ShowSubcommand { prefix } => self.provider.show_subcommands(prefix),
CompletionContext::Database { prefix } => self.provider.databases(prefix),
CompletionContext::Command { prefix } => self.provider.commands(prefix),
CompletionContext::None => Vec::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::repl::SharedState;
use crate::repl::completion::provider::MongoCandidateProvider;
fn create_test_engine() -> CompletionEngine {
let shared_state = SharedState::new("test".to_string());
let provider = Arc::new(MongoCandidateProvider::new(shared_state, None));
CompletionEngine::new(provider)
}
#[test]
fn test_complete_db_dot() {
let engine = create_test_engine();
let (start, _pairs) = engine.complete("db.", 3);
assert_eq!(start, 3);
}
#[test]
fn test_complete_collection_dot() {
let engine = create_test_engine();
let (start, pairs) = engine.complete("db.users.", 9);
assert_eq!(start, 9);
assert!(!pairs.is_empty());
assert!(pairs.iter().any(|p| p.replacement == "find"));
assert!(pairs.iter().any(|p| p.replacement == "insertOne"));
}
#[test]
fn test_complete_operation_prefix() {
let engine = create_test_engine();
let (start, pairs) = engine.complete("db.users.fi", 11);
assert_eq!(start, 9); assert!(pairs.iter().any(|p| p.replacement == "find"));
assert!(pairs.iter().any(|p| p.replacement == "findOne"));
assert!(!pairs.iter().any(|p| p.replacement == "insertOne"));
}
#[test]
fn test_complete_collection_prefix() {
let engine = create_test_engine();
let (start, _pairs) = engine.complete("db.tes", 6);
assert_eq!(start, 3);
}
#[test]
fn test_complete_collection_single_char() {
let engine = create_test_engine();
let (start, _pairs) = engine.complete("db.t", 4);
assert_eq!(start, 3);
}
#[test]
fn test_complete_exact_match_optimization() {
let engine = create_test_engine();
let (_start, pairs) = engine.complete("db.users.find", 13);
assert!(!pairs.iter().any(|p| p.replacement == "find"));
if !pairs.is_empty() {
assert!(pairs.iter().all(|p| p.replacement.starts_with("find")));
assert!(pairs.iter().any(|p| p.replacement == "findOne"));
}
}
#[test]
fn test_complete_show_command() {
let engine = create_test_engine();
let (start, pairs) = engine.complete("show ", 5);
assert_eq!(start, 5);
assert!(pairs.iter().any(|p| p.replacement == "dbs"));
assert!(pairs.iter().any(|p| p.replacement == "collections"));
}
#[test]
fn test_complete_show_command_prefix() {
let engine = create_test_engine();
let (_start, pairs) = engine.complete("show c", 6);
assert!(pairs.iter().any(|p| p.replacement == "collections"));
assert!(!pairs.iter().any(|p| p.replacement == "dbs"));
}
#[test]
fn test_complete_use_command() {
let engine = create_test_engine();
let (start, pairs) = engine.complete("use ", 4);
assert_eq!(start, 4);
assert!(pairs.iter().any(|p| p.replacement == "test"));
}
#[test]
fn test_complete_sql_from() {
let engine = create_test_engine();
let (start, _pairs) = engine.complete("SELECT * FROM ", 14);
assert_eq!(start, 14);
}
#[test]
fn test_complete_top_level_command() {
let engine = create_test_engine();
let (_start, pairs) = engine.complete("sh", 2);
assert!(pairs.iter().any(|p| p.replacement == "show"));
assert!(!pairs.iter().any(|p| p.replacement == "use"));
}
#[test]
fn test_complete_empty_input() {
let engine = create_test_engine();
let (_start, pairs) = engine.complete("", 0);
assert!(pairs.is_empty());
}
#[test]
fn test_is_sql_command() {
assert!(CompletionEngine::is_sql_command("SELECT * FROM users"));
assert!(CompletionEngine::is_sql_command("select * from users"));
assert!(CompletionEngine::is_sql_command(" SELECT"));
assert!(!CompletionEngine::is_sql_command("db.users.find()"));
assert!(!CompletionEngine::is_sql_command("show dbs"));
}
#[test]
fn test_tokenize_mongo() {
let engine = create_test_engine();
let stream = engine.tokenize("db.users", 8);
assert!(!stream.is_empty());
assert_eq!(stream.cursor, 8);
}
#[test]
fn test_tokenize_sql() {
let engine = create_test_engine();
let stream = engine.tokenize("SELECT * FROM users", 19);
assert!(!stream.is_empty());
assert_eq!(stream.cursor, 19);
}
#[test]
fn test_completion_partial_table_name() {
let engine = create_test_engine();
let (start, _pairs) = engine.complete("SELECT * FROM tem", 17);
assert_eq!(start, 14, "Should start completion at beginning of 'tem'");
let (start, _pairs) = engine.complete("SELECT * FROM t", 15);
assert_eq!(start, 14, "Should complete even with single character");
let (start, _pairs) = engine.complete("SELECT * FROM templates_", 24);
assert_eq!(start, 14, "Should complete with longer prefix");
}
#[test]
fn test_completion_partial_table_name_after_join() {
let engine = create_test_engine();
let (start, _pairs) = engine.complete("SELECT * FROM users JOIN tem", 28);
assert_eq!(
start, 25,
"Should start completion at beginning of 'tem' after JOIN"
);
}
#[test]
fn test_no_completion_after_sql_semicolon() {
let engine = create_test_engine();
let (_start, pairs) = engine.complete("SELECT * FROM users;", 20);
assert!(pairs.is_empty(), "Should not complete after semicolon");
let (_start, pairs) = engine.complete("SELECT * FROM users; ", 21);
assert!(
pairs.is_empty(),
"Should not complete after semicolon with space"
);
let (_start, pairs) = engine.complete("SELECT * FROM users; ", 23);
assert!(
pairs.is_empty(),
"Should not complete after semicolon with multiple spaces"
);
}
#[test]
fn test_no_completion_after_sql_limit() {
let engine = create_test_engine();
let (_start, pairs) = engine.complete("SELECT * FROM users LIMIT ", 26);
assert!(
pairs.is_empty(),
"Should not complete after LIMIT (expects number)"
);
let (_start, pairs) = engine.complete("SELECT * FROM users LIMIT 1", 27);
assert!(
pairs.is_empty(),
"Should not complete after LIMIT with number"
);
}
#[test]
fn test_no_completion_after_sql_offset() {
let engine = create_test_engine();
let (_start, pairs) = engine.complete("SELECT * FROM users OFFSET ", 27);
assert!(
pairs.is_empty(),
"Should not complete after OFFSET (expects number)"
);
}
#[test]
fn test_no_completion_after_table_name() {
let engine = create_test_engine();
let (_start, pairs) = engine.complete("SELECT * FROM users ", 20);
assert!(
pairs.is_empty(),
"Should not complete after table name (expects WHERE, JOIN, etc.)"
);
}
#[test]
fn test_completion_at_from() {
let engine = create_test_engine();
let (start, _pairs) = engine.complete("SELECT * FROM ", 14);
assert_eq!(start, 14, "Should start completion after FROM");
}
#[test]
fn test_no_completion_in_where_clause() {
let engine = create_test_engine();
let (_start, pairs) = engine.complete("SELECT * FROM users WHERE ", 26);
assert!(
pairs.is_empty(),
"Should not complete in WHERE clause (column completion not implemented)"
);
}
#[test]
fn test_partial_collection_name_mongo() {
let engine = create_test_engine();
let (start, _pairs) = engine.complete("db.us", 5);
assert_eq!(start, 3, "Should start completion at beginning of 'us'");
let (start, _pairs) = engine.complete("db.u", 4);
assert_eq!(start, 3, "Should complete even with single character");
let (start, _pairs) = engine.complete("db.users_col", 12);
assert_eq!(start, 3, "Should complete with longer prefix");
}
#[test]
fn test_partial_operation_name() {
let engine = create_test_engine();
let (start, pairs) = engine.complete("db.users.fin", 12);
assert_eq!(start, 9, "Should start completion at beginning of 'fin'");
assert!(pairs.iter().any(|p| p.replacement == "find"));
assert!(pairs.iter().any(|p| p.replacement == "findOne"));
let (start, pairs) = engine.complete("db.users.f", 10);
assert_eq!(start, 9, "Should complete even with single character");
assert!(!pairs.is_empty());
}
#[test]
fn test_partial_show_subcommand() {
let engine = create_test_engine();
let (_start, pairs) = engine.complete("show d", 6);
assert!(pairs.iter().any(|p| p.replacement == "dbs"));
assert!(!pairs.iter().any(|p| p.replacement == "collections"));
let (_start, pairs) = engine.complete("show col", 8);
assert!(pairs.iter().any(|p| p.replacement == "collections"));
assert!(!pairs.iter().any(|p| p.replacement == "dbs"));
}
#[test]
fn test_partial_use_database() {
let engine = create_test_engine();
let (start, pairs) = engine.complete("use tes", 7);
assert_eq!(start, 4, "Should start completion at beginning of 'tes'");
assert!(pairs.iter().any(|p| p.replacement == "test"));
}
#[test]
fn test_partial_top_level_command() {
let engine = create_test_engine();
let (_start, pairs) = engine.complete("sh", 2);
assert!(pairs.iter().any(|p| p.replacement == "show"));
assert!(!pairs.iter().any(|p| p.replacement == "use"));
let (_start, pairs) = engine.complete("us", 2);
assert!(pairs.iter().any(|p| p.replacement == "use"));
assert!(!pairs.iter().any(|p| p.replacement == "show"));
}
#[test]
fn test_complete_after_partial_keyword() {
let engine = create_test_engine();
let (_start, _pairs) = engine.complete("SEL", 3);
}
#[test]
fn test_no_completion_after_complete_table_name_with_space() {
let engine = create_test_engine();
let (_start, pairs) = engine.complete("SELECT * FROM users ", 20);
assert!(
pairs.is_empty(),
"Should not complete after complete table name (expects keyword)"
);
}
}