use super::tokenizer::{Token, tokenize};
pub(super) const TABLE_EXPECTED_KEYWORDS: &[&str] = &[
"FROM", "JOIN", "INNER", "LEFT", "RIGHT", "OUTER", "FULL", "CROSS", "INTO", "UPDATE", "TABLE",
"DESCRIBE", "DESC",
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CompletionContext {
Generic,
TableExpected,
ColumnExpected { table: String },
SchemaTableExpected { schema: String },
}
pub fn detect_context(buffer: &str, cursor_byte_offset: usize) -> CompletionContext {
detect_context_with_schemas(buffer, cursor_byte_offset, &[])
}
pub fn detect_context_with_schemas(
buffer: &str,
cursor_byte_offset: usize,
known_schemas: &[String],
) -> CompletionContext {
let slice = trim_to_current_statement(buffer, cursor_byte_offset);
let tokens = tokenize(&slice);
let alias_map = extract_aliases(&tokens);
let mut saw_dot = false;
let mut ident_before_dot: Option<String> = None;
let mut last_keyword: Option<&str> = None;
for tok in tokens.iter().rev() {
match tok {
Token::Dot => {
if ident_before_dot.is_none() {
saw_dot = true;
}
}
Token::Ident(name) => {
if saw_dot && ident_before_dot.is_none() {
ident_before_dot = Some(name.to_ascii_lowercase());
saw_dot = false;
}
}
Token::Keyword(kw) => {
last_keyword = Some(kw);
break;
}
Token::StringLiteral | Token::Other => {}
}
}
if let Some(ident) = ident_before_dot {
if let Some(resolved) = alias_map.get(&ident) {
return CompletionContext::ColumnExpected {
table: resolved.clone(),
};
}
if known_schemas.iter().any(|s| s.eq_ignore_ascii_case(&ident)) {
return CompletionContext::SchemaTableExpected { schema: ident };
}
return CompletionContext::ColumnExpected { table: ident };
}
if let Some(kw) = last_keyword {
if TABLE_EXPECTED_KEYWORDS
.iter()
.any(|k| k.eq_ignore_ascii_case(kw))
{
return CompletionContext::TableExpected;
}
}
CompletionContext::Generic
}
fn extract_aliases(tokens: &[Token]) -> std::collections::HashMap<String, String> {
let mut map = std::collections::HashMap::new();
let mut i = 0;
while i < tokens.len() {
let claims_table = matches!(&tokens[i], Token::Keyword(kw)
if matches!(kw.as_str(), "FROM" | "JOIN"));
if !claims_table {
i += 1;
continue;
}
i += 1;
while let Some(Token::Keyword(kw)) = tokens.get(i) {
if matches!(
kw.as_str(),
"INNER" | "OUTER" | "LEFT" | "RIGHT" | "FULL" | "CROSS" | "JOIN"
) {
i += 1;
} else {
break;
}
}
let Some(Token::Ident(table)) = tokens.get(i) else {
continue;
};
let table_name = table.to_ascii_lowercase();
i += 1;
if matches!(tokens.get(i), Some(Token::Dot)) {
if let Some(Token::Ident(real)) = tokens.get(i + 1) {
let _ = table_name;
let table_name = real.to_ascii_lowercase();
i += 2;
consume_alias(tokens, &mut i, &mut map, &table_name);
continue;
}
}
consume_alias(tokens, &mut i, &mut map, &table_name);
}
map
}
fn consume_alias(
tokens: &[Token],
i: &mut usize,
map: &mut std::collections::HashMap<String, String>,
table_name: &str,
) {
if matches!(tokens.get(*i), Some(Token::Keyword(kw)) if kw == "AS") {
*i += 1;
}
if let Some(Token::Ident(alias)) = tokens.get(*i) {
let alias_lc = alias.to_ascii_lowercase();
map.insert(alias_lc, table_name.to_owned());
*i += 1;
}
}
fn trim_to_current_statement(buffer: &str, cursor_byte_offset: usize) -> String {
let end = cursor_byte_offset.min(buffer.len());
let prefix = &buffer[..end];
if let Some(pos) = prefix.rfind(';') {
prefix[pos + ';'.len_utf8()..].to_owned()
} else {
prefix.to_owned()
}
}