diff options
Diffstat (limited to 'support/texlab/src/features/completion/label.rs')
-rw-r--r-- | support/texlab/src/features/completion/label.rs | 196 |
1 files changed, 196 insertions, 0 deletions
diff --git a/support/texlab/src/features/completion/label.rs b/support/texlab/src/features/completion/label.rs new file mode 100644 index 0000000000..2f7e2d8f77 --- /dev/null +++ b/support/texlab/src/features/completion/label.rs @@ -0,0 +1,196 @@ +use cancellation::CancellationToken; +use cstree::TextRange; +use lsp_types::CompletionParams; + +use crate::{ + features::{cursor::CursorContext, lsp_kinds::Structure}, + render_label, + syntax::{latex, CstNode}, + LabelledObject, +}; + +use super::types::{InternalCompletionItem, InternalCompletionItemData}; + +pub fn complete_labels<'a>( + context: &'a CursorContext<CompletionParams>, + items: &mut Vec<InternalCompletionItem<'a>>, + cancellation_token: &CancellationToken, +) -> Option<()> { + cancellation_token.result().ok()?; + + let token = context.cursor.as_latex()?; + let is_math = latex::CurlyGroupWordList::cast(token.parent()) + .filter(|group| context.is_inside_latex_curly(group)) + .and_then(|group| group.syntax().parent()) + .and_then(|reference| latex::LabelReference::cast(reference)) + .and_then(|refernce| refernce.command()) + .map(|reference| reference.text() == "\\eqref") + .or_else(|| { + latex::CurlyGroupWord::cast(token.parent()) + .filter(|group| context.is_inside_latex_curly(group)) + .and_then(|group| group.syntax().parent()) + .and_then(|reference| latex::LabelReferenceRange::cast(reference)) + .map(|_| false) + })?; + + let range = if token.kind() == latex::WORD { + token.text_range() + } else { + TextRange::empty(context.offset) + }; + + for document in &context.request.subset.documents { + if let Some(data) = document.data.as_latex() { + for label in data + .root + .descendants() + .filter(|_| !cancellation_token.is_canceled()) + .filter_map(latex::LabelDefinition::cast) + { + if let Some(name) = label + .name() + .and_then(|name| name.word()) + .map(|name| name.text()) + { + match render_label(&context.request.subset, name, Some(label)) { + Some(rendered_label) => { + let kind = match &rendered_label.object { + LabelledObject::Section { .. } => Structure::Section, + LabelledObject::Float { .. } => Structure::Float, + LabelledObject::Theorem { .. } => Structure::Theorem, + LabelledObject::Equation => Structure::Equation, + LabelledObject::EnumItem => Structure::Item, + }; + + if is_math && kind != Structure::Equation { + continue; + } + + let header = rendered_label.detail(); + let footer = match &rendered_label.object { + LabelledObject::Float { caption, .. } => Some(caption.clone()), + _ => None, + }; + + let text = format!("{} {}", name, rendered_label.reference()); + + let item = InternalCompletionItem::new( + range, + InternalCompletionItemData::Label { + name, + kind, + header, + footer, + text, + }, + ); + items.push(item); + } + None => { + let kind = Structure::Label; + let header = None; + let footer = None; + let text = name.to_string(); + let item = InternalCompletionItem::new( + range, + InternalCompletionItemData::Label { + name, + kind, + header, + footer, + text, + }, + ); + items.push(item); + } + } + } + } + } + } + + Some(()) +} + +#[cfg(test)] +mod tests { + use cstree::TextRange; + + use crate::features::testing::FeatureTester; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_labels(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_labels(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_simple() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\ref{}\\label{foo}")]) + .main("main.tex") + .line(0) + .character(5) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_labels(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(5.into(), 5.into())); + } + } + + #[test] + fn test_simple_range() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\crefrange{\n\\label{foo}")]) + .main("main.tex") + .line(0) + .character(11) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_labels(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(11.into(), 11.into())); + } + } +} |