use ls_types::Position;
use crate::semantic::text::position_to_offset;
use crate::semantic::types::{DocumentAnalysis, QueryFact};
pub fn is_table_name_context(source: &str, position: Position) -> bool {
let offset = position_to_offset(source, position);
let Some(before) = source.get(..offset) else {
return false;
};
let chars: Vec<char> = before.chars().collect();
let mut i = chars.len();
while i > 0 && is_table_ident_char(chars[i - 1]) {
i -= 1;
}
loop {
while i > 0 && chars[i - 1].is_whitespace() {
i -= 1;
}
if i == 0 || chars[i - 1] != ',' {
break;
}
i -= 1;
while i > 0 && chars[i - 1].is_whitespace() {
i -= 1;
}
while i > 0 && is_table_ident_char(chars[i - 1]) {
i -= 1;
}
}
let keyword_end = i;
while i > 0 && is_table_ident_char(chars[i - 1]) {
i -= 1;
}
if i == keyword_end {
return false;
}
let keyword: String = chars[i..keyword_end].iter().collect();
matches!(
keyword.to_ascii_uppercase().as_str(),
"FROM" | "INTO" | "UPDATE"
)
}
fn is_table_ident_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_' || c == '`'
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColumnSlot {
Strict { allow_star: bool },
Loose,
}
pub fn column_completion_context(source: &str, position: Position) -> Option<ColumnSlot> {
let offset = position_to_offset(source, position);
let before = source.get(..offset)?;
let chars: Vec<char> = before.chars().collect();
let mut i = chars.len();
while i > 0 && is_table_ident_char(chars[i - 1]) {
i -= 1;
}
loop {
while i > 0 && chars[i - 1].is_whitespace() {
i -= 1;
}
if i == 0 || chars[i - 1] != ',' {
break;
}
i -= 1;
while i > 0 {
let c = chars[i - 1];
if c == ',' {
break;
}
if matches!(c, '\'' | '"' | '(' | ')' | '{' | '}' | '[' | ']' | ';') {
return None;
}
i -= 1;
}
}
while i > 0 && chars[i - 1].is_whitespace() {
i -= 1;
}
let keyword_end = i;
while i > 0 && is_table_ident_char(chars[i - 1]) {
i -= 1;
}
if i == keyword_end {
return None;
}
let keyword: String = chars[i..keyword_end]
.iter()
.collect::<String>()
.to_ascii_uppercase();
match keyword.as_str() {
"SELECT" => Some(ColumnSlot::Strict { allow_star: true }),
"SET" => Some(ColumnSlot::Strict { allow_star: false }),
"WHERE" | "AND" | "OR" | "BY" => Some(ColumnSlot::Loose),
_ => None,
}
}
pub fn completion_prefix(source: &str, position: Position, record_type_context: bool) -> String {
let prefix = crate::semantic::text::token_prefix(source, position).unwrap_or_default();
if record_type_context {
prefix
.rsplit_once('<')
.map(|(_, suffix)| suffix.to_string())
.unwrap_or(prefix)
} else {
prefix
}
}
pub fn active_query_fact<'a>(
analysis: &'a DocumentAnalysis,
position: Position,
) -> Option<&'a QueryFact> {
analysis
.query_facts
.iter()
.find(|fact| range_contains_position(fact.location.range, position))
}
fn range_contains_position(range: ls_types::Range, position: Position) -> bool {
position_gte(position, range.start) && position_lte(position, range.end)
}
fn position_lte(left: Position, right: Position) -> bool {
left.line < right.line || (left.line == right.line && left.character <= right.character)
}
fn position_gte(left: Position, right: Position) -> bool {
left.line > right.line || (left.line == right.line && left.character >= right.character)
}
pub fn completion_table_qualifier(source: &str, position: Position) -> Option<String> {
let offset = position_to_offset(source, position);
let before_cursor = source.get(..offset)?;
let (left, right) = before_cursor.rsplit_once('.')?;
if !right.chars().all(is_field_prefix_char) {
return None;
}
let qualifier = left
.chars()
.rev()
.take_while(|ch| is_table_qualifier_char(*ch))
.collect::<String>()
.chars()
.rev()
.collect::<String>();
let qualifier = qualifier.trim_matches('`');
if qualifier.is_empty() {
return None;
}
if qualifier
.chars()
.next()
.map(|ch| ch.is_ascii_digit())
.unwrap_or(false)
{
return None;
}
let table = qualifier.split(':').next().unwrap_or(qualifier).trim();
if table.is_empty() {
None
} else {
Some(table.to_string())
}
}
fn is_table_qualifier_char(ch: char) -> bool {
ch.is_alphanumeric() || matches!(ch, '_' | ':' | '-' | '`')
}
fn is_field_prefix_char(ch: char) -> bool {
ch.is_alphanumeric() || matches!(ch, '_' | ':' | '-')
}