use crate::formatting::bibtex::{self, BibtexFormattingParams}; use crate::lsp_kind::Structure; use crate::syntax::*; use crate::workspace::*; use lsp_types::*; use once_cell::sync::Lazy; use regex::Regex; use serde::{Deserialize, Serialize}; use std::path::Path; static WHITESPACE_REGEX: Lazy = Lazy::new(|| Regex::new("\\s+").unwrap()); #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum CompletionItemData { Command, CommandSnippet, Environment, Label, Folder, File, PgfLibrary, TikzLibrary, Color, ColorModel, Package, Class, EntryType, FieldName, Citation { uri: Uri, key: String }, Argument, GlossaryEntry, } impl Into for CompletionItemData { fn into(self) -> serde_json::Value { serde_json::to_value(self).unwrap() } } #[derive(Debug, PartialEq, Eq, Clone)] pub enum LatexComponentId<'a> { User, Component(Vec<&'a str>), } impl<'a> LatexComponentId<'a> { pub fn kernel() -> Self { LatexComponentId::Component(vec![]) } pub fn detail(&self) -> String { match self { LatexComponentId::User => "user-defined".to_owned(), LatexComponentId::Component(files) => { if files.is_empty() { "built-in".to_owned() } else { files.join(", ") } } } } } fn supports_images(request: &FeatureRequest) -> bool { request .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.documentation_format.as_ref()) .map_or(true, |formats| formats.contains(&MarkupKind::Markdown)) } pub fn command( request: &FeatureRequest, name: String, image: Option<&str>, glyph: Option<&str>, text_edit: TextEdit, component: &LatexComponentId, ) -> CompletionItem { let detail = glyph.map_or_else( || component.detail(), |glyph| format!("{}, {}", glyph, component.detail()), ); CompletionItem { kind: Some(adjust_kind(request, Structure::Command.completion_kind())), data: Some(CompletionItemData::Command.into()), documentation: image.and_then(|image| image_documentation(&request, &name, image)), text_edit: Some(text_edit), ..CompletionItem::new_simple(name, detail) } } pub fn command_snippet( request: &FeatureRequest, name: &'static str, image: Option<&str>, template: &'static str, component: &LatexComponentId, ) -> CompletionItem { CompletionItem { kind: Some(adjust_kind(request, Structure::Snippet.completion_kind())), data: Some(CompletionItemData::CommandSnippet.into()), documentation: image.and_then(|image| image_documentation(&request, &name, image)), insert_text: Some(template.into()), insert_text_format: Some(InsertTextFormat::Snippet), ..CompletionItem::new_simple(name.into(), component.detail()) } } pub fn environment( request: &FeatureRequest, name: String, text_edit: TextEdit, component: &LatexComponentId, ) -> CompletionItem { CompletionItem { kind: Some(adjust_kind( request, Structure::Environment.completion_kind(), )), data: Some(CompletionItemData::Environment.into()), text_edit: Some(text_edit), ..CompletionItem::new_simple(name, component.detail()) } } pub fn label( request: &FeatureRequest, name: String, text_edit: TextEdit, context: Option<&OutlineContext>, ) -> CompletionItem { let kind = match context.as_ref().map(|ctx| &ctx.item) { Some(OutlineContextItem::Section { .. }) => Structure::Section.completion_kind(), Some(OutlineContextItem::Caption { .. }) => Structure::Float.completion_kind(), Some(OutlineContextItem::Theorem { .. }) => Structure::Theorem.completion_kind(), Some(OutlineContextItem::Equation) => Structure::Equation.completion_kind(), Some(OutlineContextItem::Item) => Structure::Item.completion_kind(), None => Structure::Label.completion_kind(), }; let detail = context.as_ref().and_then(|ctx| ctx.detail()); let filter_text = context .as_ref() .map(|ctx| format!("{} {}", name, ctx.reference())); let documentation = context .and_then(|ctx| match &ctx.item { OutlineContextItem::Caption { text, .. } => Some(text.clone()), _ => None, }) .map(Documentation::String); CompletionItem { label: name, kind: Some(adjust_kind(request, kind)), data: Some(CompletionItemData::Label.into()), text_edit: Some(text_edit), filter_text, detail, documentation, ..CompletionItem::default() } } pub fn folder( request: &FeatureRequest, path: &Path, text_edit: TextEdit, ) -> CompletionItem { CompletionItem { label: path.file_name().unwrap().to_string_lossy().into_owned(), kind: Some(adjust_kind(request, Structure::Folder.completion_kind())), data: Some(CompletionItemData::Folder.into()), text_edit: Some(text_edit), ..CompletionItem::default() } } pub fn file( request: &FeatureRequest, path: &Path, text_edit: TextEdit, ) -> CompletionItem { CompletionItem { label: path.file_name().unwrap().to_string_lossy().into_owned(), kind: Some(adjust_kind(request, Structure::File.completion_kind())), data: Some(CompletionItemData::File.into()), text_edit: Some(text_edit), ..CompletionItem::default() } } pub fn pgf_library( request: &FeatureRequest, name: &'static str, text_edit: TextEdit, ) -> CompletionItem { CompletionItem { label: name.into(), kind: Some(adjust_kind( request, Structure::PgfLibrary.completion_kind(), )), data: Some(CompletionItemData::PgfLibrary.into()), text_edit: Some(text_edit), ..CompletionItem::default() } } pub fn tikz_library( request: &FeatureRequest, name: &'static str, text_edit: TextEdit, ) -> CompletionItem { CompletionItem { label: name.into(), kind: Some(adjust_kind( request, Structure::TikzLibrary.completion_kind(), )), data: Some(CompletionItemData::TikzLibrary.into()), text_edit: Some(text_edit), ..CompletionItem::default() } } pub fn color( request: &FeatureRequest, name: &'static str, text_edit: TextEdit, ) -> CompletionItem { CompletionItem { label: name.into(), kind: Some(adjust_kind(request, Structure::Color.completion_kind())), data: Some(CompletionItemData::Color.into()), text_edit: Some(text_edit), ..CompletionItem::default() } } pub fn color_model( request: &FeatureRequest, name: &'static str, text_edit: TextEdit, ) -> CompletionItem { CompletionItem { label: name.into(), kind: Some(adjust_kind( request, Structure::ColorModel.completion_kind(), )), data: Some(CompletionItemData::ColorModel.into()), text_edit: Some(text_edit), ..CompletionItem::default() } } pub fn package( request: &FeatureRequest, name: &'static str, text_edit: TextEdit, ) -> CompletionItem { CompletionItem { label: name.into(), kind: Some(adjust_kind(request, Structure::Package.completion_kind())), data: Some(CompletionItemData::Package.into()), text_edit: Some(text_edit), ..CompletionItem::default() } } pub fn class( request: &FeatureRequest, name: &'static str, text_edit: TextEdit, ) -> CompletionItem { CompletionItem { label: name.into(), kind: Some(adjust_kind(request, Structure::Class.completion_kind())), data: Some(CompletionItemData::Class.into()), text_edit: Some(text_edit), ..CompletionItem::default() } } pub fn citation( request: &FeatureRequest, uri: Uri, entry: &BibtexEntry, key: String, text_edit: TextEdit, ) -> CompletionItem { let params = BibtexFormattingParams::default(); let entry_code = bibtex::format_entry(&entry, ¶ms); let filter_text = format!( "{} {}", &key, WHITESPACE_REGEX .replace_all( &entry_code .replace('{', "") .replace('}', "") .replace(',', " ") .replace('=', " "), " ", ) .trim() ); let kind = LANGUAGE_DATA .find_entry_type(&entry.ty.text()[1..]) .map(|ty| Structure::Entry(ty.category).completion_kind()) .unwrap_or_else(|| Structure::Entry(BibtexEntryTypeCategory::Misc).completion_kind()); CompletionItem { label: key.to_owned(), kind: Some(adjust_kind(request, kind)), filter_text: Some(filter_text), data: Some(CompletionItemData::Citation { uri, key }.into()), text_edit: Some(text_edit), ..CompletionItem::default() } } pub fn entry_type( request: &FeatureRequest, ty: &BibtexEntryTypeDoc, text_edit: TextEdit, ) -> CompletionItem { let kind = Structure::Entry(ty.category).completion_kind(); CompletionItem { label: (&ty.name).into(), kind: Some(adjust_kind(request, kind)), data: Some(CompletionItemData::EntryType.into()), text_edit: Some(text_edit), documentation: ty.documentation.as_ref().map(|doc| { Documentation::MarkupContent(MarkupContent { kind: MarkupKind::Markdown, value: doc.into(), }) }), ..CompletionItem::default() } } pub fn field_name( request: &FeatureRequest, field: &'static BibtexFieldDoc, text_edit: TextEdit, ) -> CompletionItem { CompletionItem { label: (&field.name).into(), kind: Some(adjust_kind(request, Structure::Field.completion_kind())), data: Some(CompletionItemData::FieldName.into()), text_edit: Some(text_edit), documentation: Some(Documentation::MarkupContent(MarkupContent { kind: MarkupKind::Markdown, value: (&field.documentation).into(), })), ..CompletionItem::default() } } pub fn argument( request: &FeatureRequest, name: &'static str, text_edit: TextEdit, image: Option<&str>, ) -> CompletionItem { CompletionItem { label: name.into(), kind: Some(adjust_kind(request, Structure::Argument.completion_kind())), data: Some(CompletionItemData::Argument.into()), text_edit: Some(text_edit), documentation: image.and_then(|image| image_documentation(&request, &name, image)), ..CompletionItem::default() } } pub fn glossary_entry( request: &FeatureRequest, label: String, text_edit: TextEdit, ) -> CompletionItem { CompletionItem { label, kind: Some(adjust_kind( request, Structure::GlossaryEntry.completion_kind(), )), data: Some(CompletionItemData::GlossaryEntry.into()), text_edit: Some(text_edit), ..CompletionItem::default() } } fn image_documentation( request: &FeatureRequest, name: &str, image: &str, ) -> Option { if supports_images(request) { Some(Documentation::MarkupContent(MarkupContent { kind: MarkupKind::Markdown, value: format!( "![{}](data:image/png;base64,{}|width=48,height=48)", name, image ), })) } else { None } } fn adjust_kind( request: &FeatureRequest, kind: CompletionItemKind, ) -> CompletionItemKind { if let Some(value_set) = request .client_capabilities .text_document .as_ref() .and_then(|cap| cap.completion.as_ref()) .and_then(|cap| cap.completion_item_kind.as_ref()) .and_then(|cap| cap.value_set.as_ref()) { if value_set.contains(&kind) { return kind; } } CompletionItemKind::Text }