mod acronym_ref;
mod argument;
mod begin_command;
mod citation;
mod color;
mod color_model;
mod component_command;
mod component_environment;
mod entry_type;
mod field;
mod glossary_ref;
mod import;
mod include;
mod label;
mod theorem;
mod tikz_library;
mod types;
mod user_command;
mod user_environment;
mod util;
use std::borrow::Cow;
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
use itertools::Itertools;
use lsp_types::{
CompletionItem, CompletionList, CompletionParams, CompletionTextEdit, Documentation,
InsertTextFormat, MarkupContent, MarkupKind, TextEdit,
};
use rowan::{ast::AstNode, TextSize};
use rustc_hash::FxHashSet;
use crate::{
syntax::{
bibtex::{self},
latex,
},
LineIndexExt,
};
use self::{
acronym_ref::complete_acronyms,
argument::complete_arguments,
begin_command::complete_begin_command,
citation::complete_citations,
color::complete_colors,
color_model::complete_color_models,
component_command::complete_component_commands,
component_environment::complete_component_environments,
entry_type::complete_entry_types,
field::complete_fields,
glossary_ref::complete_glossary_entries,
import::complete_imports,
include::complete_includes,
label::complete_labels,
theorem::complete_theorem_environments,
tikz_library::complete_tikz_libraries,
types::{InternalCompletionItem, InternalCompletionItemData},
user_command::complete_user_commands,
user_environment::complete_user_environments,
util::{adjust_kind, component_detail, image_documentation},
};
pub use self::types::CompletionItemData;
use super::{
cursor::{Cursor, CursorContext},
lsp_kinds::Structure,
FeatureRequest,
};
pub const COMPLETION_LIMIT: usize = 50;
#[must_use]
pub fn complete(request: FeatureRequest<CompletionParams>) -> Option<CompletionList> {
let mut items = Vec::new();
let context = CursorContext::new(request);
log::debug!("[Completion] Cursor: {:?}", context.cursor);
complete_entry_types(&context, &mut items);
complete_fields(&context, &mut items);
complete_arguments(&context, &mut items);
complete_citations(&context, &mut items);
complete_imports(&context, &mut items);
complete_colors(&context, &mut items);
complete_color_models(&context, &mut items);
complete_acronyms(&context, &mut items);
complete_glossary_entries(&context, &mut items);
complete_includes(&context, &mut items);
complete_labels(&context, &mut items);
complete_tikz_libraries(&context, &mut items);
complete_component_environments(&context, &mut items);
complete_theorem_environments(&context, &mut items);
complete_user_environments(&context, &mut items);
complete_begin_command(&context, &mut items);
complete_component_commands(&context, &mut items);
complete_user_commands(&context, &mut items);
let mut items = dedup(items);
preselect(&context, &mut items);
score(&context, &mut items);
items.sort_by(|a, b| {
a.preselect
.cmp(&b.preselect)
.reverse()
.then_with(|| a.score.cmp(&b.score).reverse())
.then_with(|| a.data.label().cmp(b.data.label()))
});
let items: Vec<_> = items
.into_iter()
.take(COMPLETION_LIMIT)
.filter(|item| item.score.is_some())
.map(|item| convert_internal_items(&context, item))
.enumerate()
.map(|(i, item)| append_sort_text(item, i))
.collect();
let is_incomplete = if context
.request
.workspace
.environment
.client_info
.as_ref()
.as_ref()
.map_or(false, |info| info.name.as_str() == "Visual Studio Code")
{
true
} else {
items.len() >= COMPLETION_LIMIT
};
Some(CompletionList {
is_incomplete,
items,
})
}
fn dedup(items: Vec<InternalCompletionItem>) -> Vec<InternalCompletionItem> {
let mut labels = FxHashSet::default();
let mut insert = vec![false; items.len()];
for (i, item) in items.iter().enumerate() {
insert[i] = labels.insert(item.data.label());
}
items
.into_iter()
.enumerate()
.filter(|(i, _)| insert[*i])
.map(|(_, item)| item)
.collect()
}
fn score(context: &CursorContext<CompletionParams>, items: &mut Vec<InternalCompletionItem>) {
let pattern: Cow<str> = match &context.cursor {
Cursor::Latex(token) if token.kind().is_command_name() => {
if token.text_range().start() + TextSize::from(1) == context.offset {
"\\".into()
} else {
token.text().trim_end().into()
}
}
Cursor::Latex(token) if token.kind() == latex::WORD => {
if let Some(key) = token.parent().and_then(latex::Key::cast) {
key.words()
.take_while(|word| word.text_range() != token.text_range())
.chain(std::iter::once(token.clone()))
.filter(|word| word.text_range().start() < context.offset)
.join(" ")
.into()
} else {
token.text().into()
}
}
Cursor::Bibtex(token)
if matches!(
token.kind(),
bibtex::TYPE
| bibtex::NAME
| bibtex::WORD
| bibtex::COMMAND_NAME
| bibtex::ACCENT_NAME
) =>
{
token.text().into()
}
Cursor::Latex(_) | Cursor::Bibtex(_) | Cursor::Nothing => "".into(),
};
let file_pattern = pattern.split('/').last().unwrap();
let matcher = SkimMatcherV2::default().ignore_case();
for item in items {
item.score = match &item.data {
InternalCompletionItemData::EntryType { ty } => {
matcher.fuzzy_match(&ty.name, &pattern[1..])
}
InternalCompletionItemData::Field { field } => {
matcher.fuzzy_match(&field.name, &pattern)
}
InternalCompletionItemData::Argument { name, .. } => {
matcher.fuzzy_match(name, &pattern)
}
InternalCompletionItemData::BeginCommand => matcher.fuzzy_match("begin", &pattern[1..]),
InternalCompletionItemData::Citation { key, .. } => matcher.fuzzy_match(key, &pattern),
InternalCompletionItemData::ComponentCommand { name, .. } => {
matcher.fuzzy_match(name, &pattern[1..])
}
InternalCompletionItemData::ComponentEnvironment { name, .. } => {
matcher.fuzzy_match(name, &pattern)
}
InternalCompletionItemData::Class { name } => matcher.fuzzy_match(name, &pattern),
InternalCompletionItemData::Package { name } => matcher.fuzzy_match(name, &pattern),
InternalCompletionItemData::Color { name } => matcher.fuzzy_match(name, &pattern),
InternalCompletionItemData::ColorModel { name } => matcher.fuzzy_match(name, &pattern),
InternalCompletionItemData::Acronym { name } => matcher.fuzzy_match(name, &pattern),
InternalCompletionItemData::GlossaryEntry { name } => {
matcher.fuzzy_match(name, &pattern)
}
InternalCompletionItemData::File { name } => matcher.fuzzy_match(name, file_pattern),
InternalCompletionItemData::Directory { name } => {
matcher.fuzzy_match(name, file_pattern)
}
InternalCompletionItemData::Label { name, .. } => matcher.fuzzy_match(name, &pattern),
InternalCompletionItemData::UserCommand { name } => matcher.fuzzy_match(name, &pattern),
InternalCompletionItemData::UserEnvironment { name } => {
matcher.fuzzy_match(name, &pattern)
}
InternalCompletionItemData::PgfLibrary { name } => matcher.fuzzy_match(name, &pattern),
InternalCompletionItemData::TikzLibrary { name } => matcher.fuzzy_match(name, &pattern),
};
}
}
fn preselect(
context: &CursorContext<CompletionParams>,
items: &mut [InternalCompletionItem],
) -> Option<()> {
let name = context.cursor.as_latex()?;
let group = latex::CurlyGroupWord::cast(name.parent()?)?;
let end = latex::End::cast(group.syntax().parent()?)?;
let environment = latex::Environment::cast(end.syntax().parent()?)?;
let name = environment.begin()?.name()?.key()?.to_string();
for item in items {
if item.data.label() == name {
item.preselect = true;
}
}
Some(())
}
fn convert_internal_items(
context: &CursorContext<CompletionParams>,
item: InternalCompletionItem,
) -> CompletionItem {
let range = context
.request
.main_document()
.line_index()
.line_col_lsp_range(item.range);
let mut new_item = match item.data {
InternalCompletionItemData::EntryType { ty } => {
let text_edit = TextEdit::new(range, (&ty.name).into());
let kind = Structure::Entry(ty.category).completion_kind();
CompletionItem {
label: (&ty.name).into(),
kind: Some(kind),
documentation: ty.documentation.as_ref().map(|doc| {
Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value: doc.into(),
})
}),
text_edit: Some(CompletionTextEdit::Edit(text_edit)),
data: Some(serde_json::to_value(CompletionItemData::EntryType).unwrap()),
..CompletionItem::default()
}
}
InternalCompletionItemData::Field { field } => {
let text_edit = TextEdit::new(range, (&field.name).into());
CompletionItem {
label: (&field.name).into(),
kind: Some(Structure::Field.completion_kind()),
documentation: Some(Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value: field.documentation.clone(),
})),
text_edit: Some(CompletionTextEdit::Edit(text_edit)),
data: Some(serde_json::to_value(CompletionItemData::FieldName).unwrap()),
..CompletionItem::default()
}
}
InternalCompletionItemData::Argument { name, image } => {
let text_edit = TextEdit::new(range, name.into());
CompletionItem {
label: name.into(),
kind: Some(Structure::Argument.completion_kind()),
data: Some(serde_json::to_value(CompletionItemData::Argument).unwrap()),
text_edit: Some(CompletionTextEdit::Edit(text_edit)),
documentation: image
.and_then(|image| image_documentation(&context.request, name, image)),
..CompletionItem::default()
}
}
InternalCompletionItemData::BeginCommand => {
if context
.request
.workspace
.environment
.client_capabilities
.text_document
.as_ref()
.and_then(|cap| cap.completion.as_ref())
.and_then(|cap| cap.completion_item.as_ref())
.and_then(|cap| cap.snippet_support)
== Some(true)
{
let text_edit = TextEdit::new(range, "begin{$1}\n\t$0\n\\end{$1}".into());
CompletionItem {
kind: Some(Structure::Snippet.completion_kind()),
data: Some(serde_json::to_value(CompletionItemData::CommandSnippet).unwrap()),
text_edit: Some(CompletionTextEdit::Edit(text_edit)),
insert_text_format: Some(InsertTextFormat::SNIPPET),
..CompletionItem::new_simple("begin".into(), component_detail(&[]))
}
} else {
let text_edit = TextEdit::new(range, "begin".to_string());
CompletionItem {
kind: Some(adjust_kind(
&context.request,
Structure::Command.completion_kind(),
)),
data: Some(serde_json::to_value(CompletionItemData::Command).unwrap()),
text_edit: Some(CompletionTextEdit::Edit(text_edit)),
..CompletionItem::new_simple("begin".to_string(), component_detail(&[]))
}
}
}
InternalCompletionItemData::Citation { uri, key, text, ty } => {
let text_edit = TextEdit::new(range, key.to_string());
CompletionItem {
label: key.to_string(),
kind: Some(ty.completion_kind()),
filter_text: Some(text.clone()),
sort_text: Some(text),
data: Some(
serde_json::to_value(CompletionItemData::Citation {
uri: uri.as_ref().clone(),
key: key.into(),
})
.unwrap(),
),
text_edit: Some(CompletionTextEdit::Edit(text_edit)),
..CompletionItem::default()
}
}
InternalCompletionItemData::ComponentCommand {
name,
image,
glyph,
file_names,
} => {
let detail = glyph.map_or_else(
|| component_detail(file_names),
|glyph| format!("{}, {}", glyph, component_detail(file_names)),
);
let documentation =
image.and_then(|img| image_documentation(&context.request, name, img));
let text_edit = TextEdit::new(range, name.to_string());
CompletionItem {
kind: Some(Structure::Command.completion_kind()),
data: Some(serde_json::to_value(CompletionItemData::Command).unwrap()),
documentation,
text_edit: Some(CompletionTextEdit::Edit(text_edit)),
..CompletionItem::new_simple(name.to_string(), detail)
}
}
InternalCompletionItemData::ComponentEnvironment { name, file_names } => {
let text_edit = TextEdit::new(range, name.to_string());
CompletionItem {
kind: Some(Structure::Environment.completion_kind()),
data: Some(serde_json::to_value(CompletionItemData::Environment).unwrap()),
text_edit: Some(CompletionTextEdit::Edit(text_edit)),
..CompletionItem::new_simple(name.to_string(), component_detail(file_names))
}
}
InternalCompletionItemData::Class { name } => {
let text_edit = TextEdit::new(range, name.to_string());
CompletionItem {
label: name.into(),
kind: Some(Structure::Class.completion_kind()),
data: Some(serde_json::to_value(CompletionItemData::Class).unwrap()),
text_edit: Some(CompletionTextEdit::Edit(text_edit)),
..CompletionItem::default()
}
}
InternalCompletionItemData::Package { name } => {
let text_edit = TextEdit::new(range, name.to_string());
CompletionItem {
label: name.into(),
kind: Some(Structure::Package.completion_kind()),
data: Some(serde_json::to_value(CompletionItemData::Package).unwrap()),
text_edit: Some(CompletionTextEdit::Edit(text_edit)),
..CompletionItem::default()
}
}
InternalCompletionItemData::Color { name } => {
let text_edit = TextEdit::new(range, name.into());
CompletionItem {
label: name.into(),
kind: Some(Structure::Color.completion_kind()),
data: Some(serde_json::to_value(CompletionItemData::Color).unwrap()),
text_edit: Some(CompletionTextEdit::Edit(text_edit)),
..CompletionItem::default()
}
}
InternalCompletionItemData::ColorModel { name } => {
let text_edit = TextEdit::new(range, name.into());
CompletionItem {
label: name.into(),
kind: Some(Structure::ColorModel.completion_kind()),
data: Some(serde_json::to_value(CompletionItemData::ColorModel).unwrap()),
text_edit: Some(CompletionTextEdit::Edit(text_edit)),
..CompletionItem::default()
}
}
InternalCompletionItemData::Acronym { name } => {
let text_edit = TextEdit::new(range, name.to_string());
CompletionItem {
label: name,
kind: Some(Structure::GlossaryEntry.completion_kind()),
data: Some(serde_json::to_value(CompletionItemData::Acronym).unwrap()),
text_edit: Some(CompletionTextEdit::Edit(text_edit)),
..CompletionItem::default()
}
}
InternalCompletionItemData::GlossaryEntry { name } => {
let text_edit = TextEdit::new(range, name.to_string());
CompletionItem {
label: name,
kind: Some(Structure::GlossaryEntry.completion_kind()),
data: Some(serde_json::to_value(CompletionItemData::GlossaryEntry).unwrap()),
text_edit: Some(CompletionTextEdit::Edit(text_edit)),
..CompletionItem::default()
}
}
InternalCompletionItemData::File { name } => {
let text_edit = TextEdit::new(range, name.to_string());
CompletionItem {
label: name.into(),
kind: Some(Structure::File.completion_kind()),
data: Some(serde_json::to_value(CompletionItemData::File).unwrap()),
text_edit: Some(CompletionTextEdit::Edit(text_edit)),
..CompletionItem::default()
}
}
InternalCompletionItemData::Directory { name } => {
let text_edit = TextEdit::new(range, name.to_string());
CompletionItem {
label: name.into(),
kind: Some(Structure::Folder.completion_kind()),
data: Some(serde_json::to_value(CompletionItemData::Folder).unwrap()),
text_edit: Some(CompletionTextEdit::Edit(text_edit)),
..CompletionItem::default()
}
}
InternalCompletionItemData::Label {
name,
kind,
header,
footer,
text,
} => {
let text_edit = TextEdit::new(range, name.to_string());
CompletionItem {
label: name,
kind: Some(kind.completion_kind()),
detail: header,
documentation: footer.map(Documentation::String),
sort_text: Some(text.clone()),
filter_text: Some(text),
text_edit: Some(CompletionTextEdit::Edit(text_edit)),
data: Some(serde_json::to_value(CompletionItemData::Label).unwrap()),
..CompletionItem::default()
}
}
InternalCompletionItemData::UserCommand { name } => {
let detail = "user-defined".into();
let name = &name[1..];
let text_edit = TextEdit::new(range, name.to_string());
CompletionItem {
kind: Some(Structure::Command.completion_kind()),
data: Some(serde_json::to_value(CompletionItemData::Command).unwrap()),
text_edit: Some(CompletionTextEdit::Edit(text_edit)),
..CompletionItem::new_simple(name.into(), detail)
}
}
InternalCompletionItemData::UserEnvironment { name } => {
let detail = "user-defined".into();
let text_edit = TextEdit::new(range, name.clone());
CompletionItem {
kind: Some(Structure::Environment.completion_kind()),
data: Some(serde_json::to_value(CompletionItemData::Environment).unwrap()),
text_edit: Some(CompletionTextEdit::Edit(text_edit)),
..CompletionItem::new_simple(name, detail)
}
}
InternalCompletionItemData::PgfLibrary { name } => {
let text_edit = TextEdit::new(range, name.into());
CompletionItem {
label: name.into(),
kind: Some(Structure::PgfLibrary.completion_kind()),
data: Some(serde_json::to_value(CompletionItemData::PgfLibrary).unwrap()),
text_edit: Some(CompletionTextEdit::Edit(text_edit)),
..CompletionItem::default()
}
}
InternalCompletionItemData::TikzLibrary { name } => {
let text_edit = TextEdit::new(range, name.into());
CompletionItem {
label: name.into(),
kind: Some(Structure::TikzLibrary.completion_kind()),
data: Some(serde_json::to_value(CompletionItemData::TikzLibrary).unwrap()),
text_edit: Some(CompletionTextEdit::Edit(text_edit)),
..CompletionItem::default()
}
}
};
new_item.kind = new_item
.kind
.map(|kind| adjust_kind(&context.request, kind));
new_item.preselect = Some(item.preselect);
new_item
}
fn append_sort_text(mut item: CompletionItem, index: usize) -> CompletionItem {
let sort_prefix = format!("{:0>2}", index);
match &item.sort_text {
Some(sort_text) => {
item.sort_text = Some(format!("{} {}", sort_prefix, sort_text));
}
None => {
item.sort_text = Some(sort_prefix);
}
};
item
}