use super::super::{
CompletionEnvironment, CompletionLocation, error::CompletionError, utils::matches_search_term,
};
use crate::server::lsp::{
CompletionItem, CompletionItemBuilder, CompletionItemKind, CompletionList, InsertTextFormat,
ItemDefaults, textdocument::TextEdit,
};
use ll_sparql_parser::{
ast::{AstNode, QueryUnit},
syntax_kind::SyntaxKind,
};
use std::collections::HashSet;
pub fn completions(context: &CompletionEnvironment) -> Result<CompletionList, CompletionError> {
if let CompletionLocation::SelectBinding(_select_clause) = &context.location {
let select_clause = QueryUnit::cast(context.full_tree.clone())
.unwrap()
.select_query()
.unwrap()
.select_clause()
.unwrap();
let mut items = Vec::new();
let search_term = context.search_term.as_deref();
if context.continuations.contains(&SyntaxKind::DISTINCT) {
items.extend(
vec![
CompletionItem::new(
"DISTINCT",
Some("Ensure unique results".to_string()),
None,
"DISTINCT ",
CompletionItemKind::Keyword,
None,
),
CompletionItem::new(
"REDUCED",
Some("Permit elimination of some non-distinct solutions".to_string()),
None,
"REDUCED ",
CompletionItemKind::Keyword,
None,
),
]
.into_iter()
.filter(|item| matches_search_term(&item.label, search_term)),
);
}
let result_vars: HashSet<String> = HashSet::from_iter(
select_clause
.variables()
.iter()
.map(|var| var.syntax().text().to_string()),
);
let availible_vars: HashSet<String> =
select_clause
.select_query()
.map_or(HashSet::new(), |select_query| {
HashSet::from_iter(
select_query
.variables()
.iter()
.map(|var| var.syntax().text().to_string()),
)
});
let group_vars: HashSet<String> = HashSet::from_iter(
select_clause
.select_query()
.and_then(|sq| sq.soulution_modifier())
.and_then(|sm| sm.group_clause())
.map(move |gc| gc.visible_variables().into_iter().map(|var| var.text()))
.into_iter()
.flatten(),
);
let vars = if group_vars.is_empty() {
&availible_vars
} else {
&group_vars
} - &result_vars;
items.extend(vars.into_iter().map(|var| {
CompletionItemBuilder::new()
.label(&var)
.detail("variable")
.kind(CompletionItemKind::Variable)
.filter_text(&var)
.text_edit(TextEdit::new(
context.replace_range.clone(),
&format!("{} ", var),
))
.insert_text_format(InsertTextFormat::PlainText)
.build()
}));
let group_by = select_clause
.select_query()
.and_then(|sq| sq.soulution_modifier())
.and_then(|sm| sm.group_clause());
if group_by.is_some() || result_vars.is_empty() {
let grouped_vars: HashSet<String> =
HashSet::from_iter(group_by.into_iter().flat_map(|group_by| {
group_by
.visible_variables()
.into_iter()
.map(|var| var.syntax().text().to_string())
}));
let vars = &availible_vars - &grouped_vars;
let query = context.search_term.clone().map(|s| format!("({s}"));
items.extend(
vars.iter()
.flat_map(|var| {
["COUNT", "SUM", "MIN", "MAX", "AVG", "SAMPLE"]
.into_iter()
.map(move |aggregate| {
CompletionItemBuilder::new()
.label(&format!(
"({aggregate}({var}) AS ?{}_{})",
aggregate.to_lowercase(),
var.split_at(1).1
))
.kind(CompletionItemKind::Snippet)
.text_edit(TextEdit::new(
context.replace_range.clone(),
&format!(
"({aggregate}({var}) AS ?${{0:{}_{}}})",
aggregate.to_lowercase(),
var.split_at(1).1
),
))
.insert_text_format(InsertTextFormat::Snippet)
.build()
})
.chain(std::iter::once(
CompletionItemBuilder::new()
.label(&format!(
"(GROUP_CONCAT(DISTINCT {var}; SEPARATOR=\", \") AS ?concat_{})",
var.split_at(1).1
))
.kind(CompletionItemKind::Snippet)
.text_edit(TextEdit::new(
context.replace_range.clone(),
&format!(
"(GROUP_CONCAT(DISTINCT {var}; SEPARATOR=\", \") AS ?${{0:concat_{}}})",
var.split_at(1).1
),
))
.insert_text_format(InsertTextFormat::Snippet)
.build(),
))
})
.chain(std::iter::once(
CompletionItemBuilder::new()
.label("(COUNT(*) AS ?count)")
.kind(CompletionItemKind::Function)
.text_edit(TextEdit::new(
context.replace_range.clone(),
"(COUNT(*) AS ?count)",
))
.insert_text_format(InsertTextFormat::PlainText)
.build(),
))
.filter(|comp_item| {
matches_search_term(&comp_item.label, query.as_ref().map(|s| s.as_str()))
}),
);
}
Ok(CompletionList {
is_incomplete: true,
item_defaults: Some(ItemDefaults {
edit_range: None,
commit_characters: None,
data: None,
insert_text_format: None,
insert_text_mode: None,
}),
items,
})
} else {
Err(CompletionError::Resolve(format!(
"select binding completions was called with location: {:?}",
context.location
)))
}
}
#[cfg(test)]
mod tests {
use super::matches_search_term;
const SELECT_BINDING_KEYWORDS: [&str; 2] = ["DISTINCT", "REDUCED"];
fn filter_keywords(search_term: Option<&str>) -> Vec<&'static str> {
SELECT_BINDING_KEYWORDS
.into_iter()
.filter(|label| matches_search_term(label, search_term))
.collect()
}
#[test]
fn no_search_term_returns_all_keywords() {
let labels = filter_keywords(None);
assert_eq!(labels.len(), 2);
assert!(labels.contains(&"DISTINCT"));
assert!(labels.contains(&"REDUCED"));
}
#[test]
fn distinct_prefix_returns_distinct() {
let labels = filter_keywords(Some("DI"));
assert_eq!(labels, vec!["DISTINCT"]);
}
#[test]
fn reduced_prefix_returns_reduced() {
let labels = filter_keywords(Some("RE"));
assert_eq!(labels, vec!["REDUCED"]);
}
#[test]
fn d_prefix_returns_distinct() {
let labels = filter_keywords(Some("D"));
assert_eq!(labels, vec!["DISTINCT"]);
}
#[test]
fn r_prefix_returns_reduced() {
let labels = filter_keywords(Some("R"));
assert_eq!(labels, vec!["REDUCED"]);
}
#[test]
fn non_keyword_prefix_returns_empty() {
let labels = filter_keywords(Some("Germany"));
assert!(labels.is_empty());
}
#[test]
fn case_insensitive_matching() {
let labels = filter_keywords(Some("distinct"));
assert_eq!(labels, vec!["DISTINCT"]);
}
#[test]
fn variable_like_prefix_returns_empty() {
let labels = filter_keywords(Some("?foo"));
assert!(labels.is_empty());
}
}