use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::Mutex;
use tower_lsp_server::jsonrpc::Result;
use tower_lsp_server::ls_types::*;
use crate::lsp::DocumentState;
use crate::utils::normalize_anchor_label;
use super::super::helpers;
use crate::metadata::inline_reference_map;
pub(crate) async fn completion(
client: &tower_lsp_server::Client,
document_map: Arc<Mutex<HashMap<String, DocumentState>>>,
salsa_db: Arc<Mutex<crate::salsa::SalsaDb>>,
workspace_root: Arc<Mutex<Option<PathBuf>>>,
params: CompletionParams,
) -> Result<Option<CompletionResponse>> {
let uri = ¶ms.text_document_position.text_document.uri;
let position = params.text_document_position.position;
let config = helpers::get_config(client, &workspace_root, uri).await;
let Some(text) = helpers::get_document_content(&document_map, &salsa_db, uri).await else {
return Ok(None);
};
let Some(offset) = super::super::conversions::position_to_offset(&text, position) else {
return Ok(None);
};
let Some(query) = citation_query_prefix(&text, offset) else {
return Ok(None);
};
let (salsa_file, salsa_config, doc_path, parsed_yaml_regions) = {
let map = document_map.lock().await;
match map.get(&uri.to_string()) {
Some(state) => (
state.salsa_file,
state.salsa_config,
state.path.clone(),
state.parsed_yaml_regions.clone(),
),
None => return Ok(None),
}
};
let offset_in_frontmatter =
helpers::is_offset_in_yaml_frontmatter(&parsed_yaml_regions, offset);
if offset_in_frontmatter {
return Ok(None);
}
let Some(doc_path) = doc_path else {
return Ok(None);
};
let yaml_ok = helpers::is_yaml_frontmatter_valid(&parsed_yaml_regions);
if !yaml_ok {
return Ok(None);
}
let metadata = {
let db = salsa_db.lock().await;
crate::salsa::metadata(&*db, salsa_file, salsa_config, doc_path.clone()).clone()
};
let parse = metadata.bibliography_parse.as_ref();
let symbol_index = {
let db = salsa_db.lock().await;
crate::salsa::symbol_usage_index(&*db, salsa_file, salsa_config, doc_path).clone()
};
let has_crossref_candidates = symbol_index
.crossref_declaration_entries()
.any(|(key, _)| is_supported_crossref_completion_key(key));
if parse.is_none() && metadata.inline_references.is_empty() && !has_crossref_candidates {
return Ok(None);
}
let mut seen = std::collections::HashSet::new();
let mut items = Vec::new();
if let Some(parse) = parse {
for entry in parse.index.entries() {
if !seen.insert(entry.key.to_lowercase()) {
continue;
}
if !matches_query(&entry.key, &query) {
continue;
}
items.push(CompletionItem {
label: entry.key.clone(),
kind: Some(CompletionItemKind::REFERENCE),
insert_text: Some(entry.key.clone()),
insert_text_format: Some(InsertTextFormat::PLAIN_TEXT),
..Default::default()
});
}
}
for (key, entries) in inline_reference_map(&metadata.inline_references) {
if entries.is_empty() || !seen.insert(key.clone()) {
continue;
}
let label = entries[0].id.clone();
if !matches_query(&label, &query) {
continue;
}
items.push(CompletionItem {
label: label.clone(),
kind: Some(CompletionItemKind::REFERENCE),
insert_text: Some(label),
insert_text_format: Some(InsertTextFormat::PLAIN_TEXT),
..Default::default()
});
}
if config.extensions.quarto_crossrefs || config.extensions.bookdown_references {
for (label, _) in symbol_index.crossref_declaration_entries() {
if !is_supported_crossref_completion_key(label) {
continue;
}
let display = normalize_anchor_label(label);
if display.is_empty() || !seen.insert(display.to_lowercase()) {
continue;
}
if !matches_query(&display, &query) {
continue;
}
items.push(CompletionItem {
label: display.clone(),
kind: Some(CompletionItemKind::REFERENCE),
insert_text: Some(display),
insert_text_format: Some(InsertTextFormat::PLAIN_TEXT),
..Default::default()
});
}
}
if items.is_empty() {
return Ok(None);
}
Ok(Some(CompletionResponse::Array(items)))
}
fn citation_query_prefix(text: &str, offset: usize) -> Option<String> {
let start = offset.saturating_sub(8);
let snippet = &text[start..offset];
let at = snippet.rfind('@')?;
let query = &snippet[at + 1..];
if query
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | ':' | '.'))
{
Some(query.to_string())
} else {
None
}
}
fn matches_query(candidate: &str, query: &str) -> bool {
if query.is_empty() {
return true;
}
candidate
.to_ascii_lowercase()
.starts_with(&query.to_ascii_lowercase())
}
fn is_supported_crossref_completion_key(key: &str) -> bool {
panache_parser::parser::inlines::citations::is_quarto_crossref_key(key)
|| panache_parser::parser::inlines::citations::has_bookdown_prefix(key)
}