use crate::server::{
analysis::namespace_is_declared,
lsp::{
CodeAction, WorkspaceEdit,
textdocument::{Position, Range, TextDocumentItem, TextEdit},
},
state::ServerState,
};
use ll_sparql_parser::{
ast::{AstNode, Var},
syntax_kind::SyntaxKind,
};
use std::collections::HashMap;
const RDFS_PREFIX: &str = "rdfs";
const RDFS_NAMESPACE: &str = "http://www.w3.org/2000/01/rdf-schema#";
pub(super) fn code_action(
var: &Var,
server_state: &ServerState,
document: &TextDocumentItem,
) -> Option<CodeAction> {
let triple = var.triple()?;
let subject = triple.subject()?;
let subject_start: usize = subject.syntax().text_range().start().into();
let line_start = document.text[..subject_start]
.rfind('\n')
.map(|pos| pos + 1)
.unwrap_or(0);
let indentation = &document.text[line_start..subject_start];
let var_name = var.var_name();
let label_var = format!("?{var_name}_label");
let lang_filter = format!(r#"FILTER (LANG({label_var}) = "en")"#);
let label_triple = format!(
" .\n{indentation}{} rdfs:label {label_var} . {lang_filter}",
var.syntax()
);
let start = Position::from_byte_index(
triple.properties_list_path().and_then(|path| {
path.properties()
.last()
.and_then(|last| Some(last.object.as_ref()?.syntax().text_range().end()))
})?,
&document.text,
)?;
let end = triple
.syntax()
.next_sibling_or_token()
.and_then(|next| {
if next.kind().is_trivia() {
next.next_sibling_or_token()
} else {
Some(next)
}
})
.filter(|next| matches!(next.kind(), SyntaxKind::Dot))
.and_then(|next| Position::from_byte_index(next.text_range().end(), &document.text))
.unwrap_or(start);
let mut edits = vec![TextEdit::new(Range { start, end }, &label_triple)];
if !namespace_is_declared(server_state, &document.uri, RDFS_PREFIX).unwrap_or(true) {
edits.push(TextEdit::new(
Range::new(0, 0, 0, 0),
&format!("PREFIX {RDFS_PREFIX}: <{RDFS_NAMESPACE}>\n"),
));
}
Some(CodeAction {
title: "Add rdfs:label".to_string(),
kind: None,
diagnostics: vec![],
edit: WorkspaceEdit {
changes: Some(HashMap::from_iter([(document.uri.to_string(), edits)])),
},
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::server::lsp::textdocument::TextDocumentItem;
use indoc::indoc;
use ll_sparql_parser::{ast::AstNode, parse};
fn setup_state(text: &str) -> ServerState {
let mut state = ServerState::new();
let document = TextDocumentItem::new("uri", text);
state.add_document(document);
state
}
fn find_var_by_name(text: &str, var_name: &str) -> Option<Var> {
let (root, _) = parse(text);
root.descendants()
.filter_map(Var::cast)
.find(|v| v.syntax().text() == var_name)
}
#[test]
fn test_add_label_without_rdfs_prefix() {
let text = indoc!(
"SELECT * WHERE {
?thing wdt:P17 ?country .
}"
);
let state = setup_state(text);
let document = state.get_document("uri").unwrap().clone();
let var = find_var_by_name(text, "?country").unwrap();
let action = code_action(&var, &state, &document).unwrap();
assert_eq!(action.title, "Add rdfs:label");
let edits = action.edit.changes.unwrap();
let uri_edits = edits.get("uri").unwrap();
assert_eq!(uri_edits.len(), 2);
let insert_edit = &uri_edits[0];
assert!(
insert_edit
.new_text
.contains("?country rdfs:label ?country_label . FILTER")
);
assert!(insert_edit.new_text.starts_with(" .\n ?country"));
let prefix_edit = &uri_edits[1];
assert_eq!(
prefix_edit.new_text,
"PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>\n"
);
assert_eq!(prefix_edit.range, Range::new(0, 0, 0, 0));
}
#[test]
fn test_add_label_with_rdfs_prefix() {
let text = indoc!(
"PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT * WHERE {
?thing wdt:P17 ?country .
}"
);
let state = setup_state(text);
let document = state.get_document("uri").unwrap().clone();
let var = find_var_by_name(text, "?country").unwrap();
let action = code_action(&var, &state, &document).unwrap();
let edits = action.edit.changes.unwrap();
let uri_edits = edits.get("uri").unwrap();
assert_eq!(uri_edits.len(), 1);
}
#[test]
fn test_label_variable_naming() {
let text = "SELECT * WHERE { ?person a ?type . }";
let state = setup_state(text);
let document = state.get_document("uri").unwrap().clone();
let var = find_var_by_name(text, "?person").unwrap();
let action = code_action(&var, &state, &document).unwrap();
let edits = action.edit.changes.unwrap();
let uri_edits = edits.get("uri").unwrap();
let insert_edit = &uri_edits[0];
assert!(
insert_edit.new_text.contains(
r#"?person rdfs:label ?person_label . FILTER (LANG(?person_label) = "en")"#
)
);
}
}