use crate::dialect::Dialect;
use crate::schema::Schema;
use async_trait::async_trait;
use tower_lsp::lsp_types::{
CompletionItem, CompletionItemKind, Diagnostic, DiagnosticSeverity, Hover, Location,
MarkedString, NumberOrString, Position, Range,
};
pub struct RedisDialect;
impl Default for RedisDialect {
fn default() -> Self {
Self::new()
}
}
impl RedisDialect {
pub fn new() -> Self {
Self
}
}
#[async_trait]
impl Dialect for RedisDialect {
fn name(&self) -> &str {
"redis"
}
async fn parse(&self, sql: &str, _schema: Option<&Schema>) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
if sql.trim().is_empty() {
return diagnostics;
}
if sql.to_uppercase().contains("FT.SEARCH") && !sql.contains("@") {
diagnostics.push(Diagnostic {
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: sql.len() as u32,
},
},
severity: Some(DiagnosticSeverity::WARNING),
code: Some(NumberOrString::String("REDIS001".to_string())),
code_description: None,
source: Some("redis".to_string()),
message: "FT.SEARCH query might benefit from field filters using @field"
.to_string(),
related_information: None,
tags: None,
data: None,
});
}
diagnostics
}
async fn completion(
&self,
_sql: &str,
_position: Position,
schema: Option<&Schema>,
) -> Vec<CompletionItem> {
let mut items = Vec::new();
let basic_commands = vec![
"SET",
"GET",
"DEL",
"EXISTS",
"EXPIRE",
"TTL",
"PERSIST",
"HSET",
"HGET",
"HGETALL",
"HDEL",
"HKEYS",
"HVALS",
"HINCRBY",
"LPUSH",
"RPUSH",
"LPOP",
"RPOP",
"LLEN",
"LRANGE",
"LINDEX",
"SADD",
"SMEMBERS",
"SREM",
"SCARD",
"SISMEMBER",
"SINTER",
"ZADD",
"ZRANGE",
"ZREM",
"ZCARD",
"ZSCORE",
"ZRANK",
];
let search_commands = [
"FT.SEARCH",
"FT.AGGREGATE",
"FT.CREATE",
"FT.DROPINDEX",
"FT.INFO",
"FT.ALIASADD",
"FT.ALIASDEL",
"FT.ALIASUPDATE",
"FT.SUGADD",
"FT.SUGGET",
"FT.SUGDEL",
"FT.SUGLEN",
];
let graph_commands = [
"GRAPH.QUERY",
"GRAPH.DELETE",
"GRAPH.EXPLAIN",
"GRAPH.PROFILE",
];
let json_commands = [
"JSON.GET",
"JSON.SET",
"JSON.DEL",
"JSON.MGET",
"JSON.KEYS",
"JSON.ARRAPPEND",
"JSON.ARRINDEX",
"JSON.ARRINSERT",
"JSON.ARRLEN",
"JSON.ARRPOP",
"JSON.OBJKEYS",
"JSON.OBJLEN",
];
let commands: Vec<&str> = basic_commands
.iter()
.chain(search_commands.iter())
.chain(graph_commands.iter())
.chain(json_commands.iter())
.copied()
.collect();
for cmd in commands {
let (kind, detail_prefix) = if cmd.starts_with("FT.") {
(CompletionItemKind::FUNCTION, "RediSearch command")
} else if cmd.starts_with("GRAPH.") {
(CompletionItemKind::FUNCTION, "RedisGraph command")
} else if cmd.starts_with("JSON.") {
(CompletionItemKind::FUNCTION, "RedisJSON command")
} else {
(CompletionItemKind::FUNCTION, "Redis command")
};
items.push(CompletionItem {
label: cmd.to_string(),
kind: Some(kind),
detail: Some(format!("{}: {}", detail_prefix, cmd)),
documentation: None,
deprecated: None,
preselect: None,
sort_text: Some(format!("0{}", cmd)),
filter_text: None,
insert_text: Some(cmd.to_string()),
insert_text_format: None,
text_edit: None,
additional_text_edits: None,
commit_characters: None,
command: None,
data: None,
tags: None,
insert_text_mode: None,
label_details: None,
});
}
let keywords = vec![
"@", "AND", "OR", "NOT", "-", "+", "~", "|", "(", ")", "{", "}", "[", "]", ];
for keyword in keywords {
let detail = match keyword {
"@" => "Field prefix for RediSearch queries (e.g., @field:value)",
"AND" => "Logical AND operator",
"OR" => "Logical OR operator",
"NOT" => "Logical NOT operator",
"-" => "Exclude operator (must not contain)",
"+" => "Must contain operator",
"~" => "Fuzzy match operator",
"|" => "OR operator (used in aggregations)",
"(" | ")" => "Grouping parentheses",
"{" | "}" => "Range query braces (exclusive)",
"[" | "]" => "Range query brackets (inclusive)",
_ => "RediSearch query operator",
};
items.push(CompletionItem {
label: keyword.to_string(),
kind: Some(CompletionItemKind::OPERATOR),
detail: Some(detail.to_string()),
documentation: None,
deprecated: None,
preselect: None,
sort_text: Some(format!("1{}", keyword)),
filter_text: None,
insert_text: Some(keyword.to_string()),
insert_text_format: None,
insert_text_mode: None,
text_edit: None,
additional_text_edits: None,
commit_characters: None,
command: None,
data: None,
tags: None,
label_details: None,
});
}
if let Some(schema) = schema {
for table in &schema.tables {
items.push(CompletionItem {
label: table.name.clone(),
kind: Some(CompletionItemKind::CLASS),
detail: Some(format!("Redis Index/Key: {}", table.name)),
documentation: table
.comment
.clone()
.map(tower_lsp::lsp_types::Documentation::String),
deprecated: None,
preselect: None,
sort_text: Some(format!("2{}", table.name)),
filter_text: None,
insert_text: Some(table.name.clone()),
insert_text_format: None,
insert_text_mode: None,
text_edit: None,
additional_text_edits: None,
commit_characters: None,
command: None,
data: None,
tags: None,
label_details: None,
});
}
}
items
}
async fn hover(
&self,
sql: &str,
_position: Position,
schema: Option<&Schema>,
) -> Option<Hover> {
if let Some(schema) = schema {
for table in &schema.tables {
if sql.contains(&table.name) {
return Some(Hover {
contents: tower_lsp::lsp_types::HoverContents::Scalar(
MarkedString::String(format!(
"Redis Index/Key: {}\n{}",
table.name,
table.comment.as_deref().unwrap_or("No description")
)),
),
range: None,
});
}
}
}
None
}
async fn goto_definition(
&self,
_sql: &str,
_position: Position,
_schema: Option<&Schema>,
) -> Option<Location> {
None
}
async fn references(
&self,
_sql: &str,
_position: Position,
_schema: Option<&Schema>,
) -> Vec<Location> {
Vec::new()
}
async fn format(&self, sql: &str) -> String {
sql.split_whitespace().collect::<Vec<_>>().join(" ")
}
async fn validate(&self, sql: &str, schema: Option<&Schema>) -> Vec<Diagnostic> {
self.parse(sql, schema).await
}
}