use std::{collections::HashSet, rc::Rc};
use super::super::{CompletionEnvironment, CompletionLocation, error::CompletionError};
use crate::server::{
Server,
configuration::Replacement,
lsp::{
Command, CompletionItem, CompletionItemBuilder, CompletionItemKind, CompletionList,
InsertTextFormat, ItemDefaults, textdocument::TextEdit,
},
};
use futures::lock::Mutex;
use ll_sparql_parser::ast::{AstNode, PrefixedName, Var, VarOrTerm};
use regex::Regex;
pub async fn completions(
server_rc: Rc<Mutex<Server>>,
environment: &CompletionEnvironment,
) -> Result<CompletionList, CompletionError> {
let server = server_rc.lock().await;
let suffix = match environment.location {
CompletionLocation::Object(_)
| CompletionLocation::Subject
| CompletionLocation::Predicate(_)
| CompletionLocation::BlankNodeProperty(_)
| CompletionLocation::BlankNodeObject(_) => " ",
_ => "",
};
let mut suggestions: Vec<CompletionItem> = HashSet::<String>::from_iter(
environment
.full_tree
.descendants()
.filter_map(Var::cast)
.filter(|var| !var.has_error())
.map(|var| format!("?{}", var.var_name())),
)
.into_iter()
.map(|var| {
let mut item = CompletionItemBuilder::new()
.label(&var)
.kind(CompletionItemKind::Variable)
.detail("Variable")
.sort_text(&format!("{:0>4}0", 1))
.filter_text(&var)
.text_edit(TextEdit::new(
environment.replace_range.clone(),
&format!("{var}{suffix}"),
))
.insert_text_format(InsertTextFormat::PlainText)
.build();
match environment.location {
CompletionLocation::Subject
| CompletionLocation::Predicate(_)
| CompletionLocation::BlankNodeProperty(_) => {
item.command = Some(Command {
title: "triggerNewCompletion".to_string(),
command: "triggerNewCompletion".to_string(),
arguments: None,
});
}
_ => {}
}
item
})
.collect();
if matches!(
environment.location,
CompletionLocation::Object(_) | CompletionLocation::BlankNodeObject(_)
) && let Some(prefixed_name) = environment
.anchor_token
.clone()
.and_then(|token| token.parent())
.and_then(PrefixedName::cast)
{
let mut object_name = server
.state
.label_memory
.get(&prefixed_name.text())
.cloned()
.unwrap_or(prefixed_name.name());
if let Some(replacements) = server
.settings
.replacements
.as_ref()
.map(|replacements| &replacements.object_variable)
{
for Replacement {
pattern,
replacement,
} in replacements.iter()
{
object_name = Regex::new(pattern)
.unwrap()
.replace_all(&object_name, replacement)
.to_string();
}
}
let variable = to_sparql_variable(&object_name);
suggestions.insert(
0,
CompletionItemBuilder::new()
.label(&format!("?{variable}"))
.kind(CompletionItemKind::Variable)
.sort_text("00000")
.filter_text(&format!("?{variable}"))
.text_edit(TextEdit::new(
environment.replace_range.clone(),
&format!("?{variable}"),
))
.insert_text_format(InsertTextFormat::PlainText)
.build(),
);
if let CompletionLocation::Object(ref triple) = environment.location
&& let Some(var) = triple
.subject()
.map(|subject| subject.syntax().clone())
.and_then(VarOrTerm::cast)
.and_then(|var_or_term| var_or_term.var())
{
let subject_var_name = var.var_name();
suggestions.insert(
0,
CompletionItemBuilder::new()
.label(&format!("?{}_{}", subject_var_name, variable))
.kind(CompletionItemKind::Variable)
.sort_text("00001")
.filter_text(&format!("?{}_{}", subject_var_name, variable))
.text_edit(TextEdit::new(
environment.replace_range.clone(),
&format!("?{}_{}", subject_var_name, variable),
))
.insert_text_format(InsertTextFormat::PlainText)
.build(),
);
}
if let CompletionLocation::Object(ref triple) = environment.location
&& let Some(subject_label) = triple
.subject()
.map(|subject| subject.syntax().to_string())
.and_then(|subject| server.state.label_memory.get(&subject))
{
let subject_label = to_sparql_variable(subject_label);
suggestions.insert(
0,
CompletionItemBuilder::new()
.label(&format!("?{}_{}", subject_label, variable))
.kind(CompletionItemKind::Variable)
.sort_text("00001")
.filter_text(&format!("?{}_{}", subject_label, variable))
.text_edit(TextEdit::new(
environment.replace_range.clone(),
&format!("?{}_{}", subject_label, variable),
))
.insert_text_format(InsertTextFormat::PlainText)
.build(),
);
}
}
let limit = server.settings.completion.variable_completion_limit;
if let Some(limit) = limit {
suggestions.truncate(limit as usize);
}
Ok(CompletionList {
is_incomplete: limit.is_some_and(|l| suggestions.len() >= l as usize),
item_defaults: Some(ItemDefaults {
edit_range: None,
commit_characters: None,
data: None,
insert_text_format: Some(InsertTextFormat::PlainText),
insert_text_mode: None,
}),
items: suggestions,
})
}
fn to_sparql_variable(s: &str) -> String {
if s.is_empty() {
return "var".to_string();
}
let s = &s
.trim()
.to_lowercase()
.replace([' ', '-', ',', '.', '(', ')', '$', '?'], " ")
.split_whitespace()
.collect::<Vec<_>>()
.join("_");
let mut result = String::new();
let mut chars = s.chars();
if let Some(first) = chars.next() {
if first.is_ascii_alphabetic() || first == '_' {
result.push(first);
} else if first.is_ascii_digit() {
result.push('_');
result.push(first);
} else {
result.push('_');
}
}
for c in chars {
if c.is_ascii_alphanumeric() || c == '_' {
result.push(c);
} else {
result.push('_');
}
}
if result.is_empty() {
result.push_str("var");
}
result
}