diff options
Diffstat (limited to 'support/texlab/src/features/completion.rs')
-rw-r--r-- | support/texlab/src/features/completion.rs | 567 |
1 files changed, 567 insertions, 0 deletions
diff --git a/support/texlab/src/features/completion.rs b/support/texlab/src/features/completion.rs new file mode 100644 index 0000000000..13c7aad22b --- /dev/null +++ b/support/texlab/src/features/completion.rs @@ -0,0 +1,567 @@ +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 cancellation::CancellationToken; +use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; +use lsp_types::{ + CompletionItem, CompletionList, CompletionParams, CompletionTextEdit, Documentation, + InsertTextFormat, MarkupContent, MarkupKind, TextEdit, +}; +use rustc_hash::FxHashSet; + +use crate::{ + syntax::{bibtex, latex, CstNode}, + 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; + +pub fn complete( + request: FeatureRequest<CompletionParams>, + cancellation_token: &CancellationToken, +) -> Option<CompletionList> { + let mut items = Vec::new(); + let context = CursorContext::new(request); + log::debug!("[Completion] Cursor: {:?}", context.cursor); + complete_entry_types(&context, &mut items, cancellation_token); + complete_fields(&context, &mut items, cancellation_token); + complete_arguments(&context, &mut items, cancellation_token); + complete_citations(&context, &mut items, cancellation_token); + complete_imports(&context, &mut items, cancellation_token); + complete_colors(&context, &mut items, cancellation_token); + complete_color_models(&context, &mut items, cancellation_token); + complete_acronyms(&context, &mut items, cancellation_token); + complete_glossary_entries(&context, &mut items, cancellation_token); + complete_includes(&context, &mut items, cancellation_token); + complete_labels(&context, &mut items, cancellation_token); + complete_tikz_libraries(&context, &mut items, cancellation_token); + complete_component_environments(&context, &mut items, cancellation_token); + complete_theorem_environments(&context, &mut items, cancellation_token); + complete_user_environments(&context, &mut items, cancellation_token); + complete_begin_command(&context, &mut items, cancellation_token); + complete_component_commands(&context, &mut items, cancellation_token); + complete_user_commands(&context, &mut items, cancellation_token); + + cancellation_token.result().ok()?; + + let mut items = dedup(items); + preselect(&context, &mut items); + score(&context, &mut items); + + items.sort_by_key(|item| (!item.preselect, -item.score.unwrap_or(std::i64::MIN + 1))); + 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 + .context + .client_info + .lock() + .unwrap() + .as_ref() + .map(|info| info.name.as_str()) + .unwrap_or_default() + == "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 = match &context.cursor { + Cursor::Latex(token) if token.kind().is_command_name() => token.text().trim_end(), + Cursor::Latex(token) if token.kind() == latex::WORD => token.text(), + Cursor::Latex(_) => "", + Cursor::Bibtex(token) if token.kind().is_type() => token.text(), + Cursor::Bibtex(token) if token.kind() == bibtex::WORD => token.text(), + Cursor::Bibtex(token) if token.kind() == bibtex::COMMAND_NAME => token.text().trim_end(), + Cursor::Bibtex(_) => "", + Cursor::Nothing => "", + }; + + 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[1..]) + } + 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()?.word()?.text(); + + 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(adjust_kind(&context.request, 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(adjust_kind( + &context.request, + Structure::Field.completion_kind(), + )), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: (&field.documentation).into(), + })), + 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(adjust_kind( + &context.request, + 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 + .context + .client_capabilities + .lock() + .unwrap() + .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) + { + CompletionItem { + kind: Some(adjust_kind( + &context.request, + Structure::Snippet.completion_kind(), + )), + data: Some(serde_json::to_value(CompletionItemData::CommandSnippet).unwrap()), + insert_text: Some("begin{$1}\n\t$0\n\\end{$1}".into()), + 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(adjust_kind(&context.request, 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(adjust_kind( + &context.request, + 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(adjust_kind( + &context.request, + 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(adjust_kind( + &context.request, + 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(adjust_kind( + &context.request, + 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(adjust_kind( + &context.request, + 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(adjust_kind( + &context.request, + 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.into(), + kind: Some(adjust_kind( + &context.request, + 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.into(), + kind: Some(adjust_kind( + &context.request, + 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(adjust_kind( + &context.request, + 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(adjust_kind( + &context.request, + 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.into(), + kind: Some(adjust_kind(&context.request, 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 text_edit = TextEdit::new(range, name.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(name.into(), detail) + } + } + InternalCompletionItemData::UserEnvironment { name } => { + let detail = "user-defined".into(); + let text_edit = TextEdit::new(range, name.to_string()); + CompletionItem { + kind: Some(adjust_kind( + &context.request, + Structure::Environment.completion_kind(), + )), + data: Some(serde_json::to_value(CompletionItemData::Environment).unwrap()), + text_edit: Some(CompletionTextEdit::Edit(text_edit)), + ..CompletionItem::new_simple(name.into(), detail) + } + } + InternalCompletionItemData::PgfLibrary { name } => { + let text_edit = TextEdit::new(range, name.into()); + CompletionItem { + label: name.into(), + kind: Some(adjust_kind( + &context.request, + 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(adjust_kind( + &context.request, + Structure::TikzLibrary.completion_kind(), + )), + data: Some(serde_json::to_value(CompletionItemData::TikzLibrary).unwrap()), + text_edit: Some(CompletionTextEdit::Edit(text_edit)), + ..CompletionItem::default() + } + } + }; + 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 +} |