diff options
Diffstat (limited to 'support/texlab/src/completion/latex')
-rw-r--r-- | support/texlab/src/completion/latex/argument.rs | 117 | ||||
-rw-r--r-- | support/texlab/src/completion/latex/begin_command.rs | 30 | ||||
-rw-r--r-- | support/texlab/src/completion/latex/citation.rs | 144 | ||||
-rw-r--r-- | support/texlab/src/completion/latex/color.rs | 74 | ||||
-rw-r--r-- | support/texlab/src/completion/latex/color_model.rs | 94 | ||||
-rw-r--r-- | support/texlab/src/completion/latex/combinators.rs | 152 | ||||
-rw-r--r-- | support/texlab/src/completion/latex/component.rs | 278 | ||||
-rw-r--r-- | support/texlab/src/completion/latex/glossary.rs | 53 | ||||
-rw-r--r-- | support/texlab/src/completion/latex/import.rs | 110 | ||||
-rw-r--r-- | support/texlab/src/completion/latex/include.rs | 129 | ||||
-rw-r--r-- | support/texlab/src/completion/latex/label.rs | 156 | ||||
-rw-r--r-- | support/texlab/src/completion/latex/mod.rs | 14 | ||||
-rw-r--r-- | support/texlab/src/completion/latex/theorem.rs | 70 | ||||
-rw-r--r-- | support/texlab/src/completion/latex/tikz.rs | 90 | ||||
-rw-r--r-- | support/texlab/src/completion/latex/user.rs | 156 |
15 files changed, 1667 insertions, 0 deletions
diff --git a/support/texlab/src/completion/latex/argument.rs b/support/texlab/src/completion/latex/argument.rs new file mode 100644 index 0000000000..db05bca824 --- /dev/null +++ b/support/texlab/src/completion/latex/argument.rs @@ -0,0 +1,117 @@ +use super::combinators::{self, Parameter}; +use crate::completion::factory; +use crate::completion::DATABASE; +use crate::workspace::*; +use futures_boxed::boxed; +use lsp_types::*; +use std::iter; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct LatexArgumentCompletionProvider; + +impl FeatureProvider for LatexArgumentCompletionProvider { + type Params = CompletionParams; + type Output = Vec<CompletionItem>; + + #[boxed] + async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output { + let mut all_items = Vec::new(); + for component in DATABASE.related_components(request.related_documents()) { + for command in &component.commands { + let name = format!("\\{}", command.name); + for (i, parameter) in command.parameters.iter().enumerate() { + let mut items = combinators::argument( + request, + iter::once(Parameter::new(&name, i)), + |context| { + async move { + let mut items = Vec::new(); + for argument in ¶meter.0 { + let text_edit = + TextEdit::new(context.range, (&argument.name).into()); + let item = factory::argument( + request, + &argument.name, + text_edit, + argument.image.as_ref().map(AsRef::as_ref), + ); + items.push(item); + } + items + } + }, + ) + .await; + all_items.append(&mut items); + } + } + } + all_items + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::range::RangeExt; + use lsp_types::{Position, Range}; + + #[test] + fn test_inside_mathbb_empty() { + let items = test_feature( + LatexArgumentCompletionProvider, + FeatureSpec { + files: vec![FeatureSpec::file( + "foo.tex", + "\\usepackage{amsfonts}\n\\mathbb{}", + )], + main_file: "foo.tex", + position: Position::new(1, 8), + ..FeatureSpec::default() + }, + ); + assert!(!items.is_empty()); + assert_eq!( + items[0].text_edit.as_ref().map(|edit| edit.range), + Some(Range::new_simple(1, 8, 1, 8)) + ); + } + + #[test] + fn test_inside_mathbb_non_empty() { + let items = test_feature( + LatexArgumentCompletionProvider, + FeatureSpec { + files: vec![FeatureSpec::file( + "foo.tex", + "\\usepackage{amsfonts}\n\\mathbb{foo}", + )], + main_file: "foo.tex", + position: Position::new(1, 8), + ..FeatureSpec::default() + }, + ); + assert!(!items.is_empty()); + assert_eq!( + items[0].text_edit.as_ref().map(|edit| edit.range), + Some(Range::new_simple(1, 8, 1, 11)) + ); + } + + #[test] + fn test_outside_mathbb() { + let items = test_feature( + LatexArgumentCompletionProvider, + FeatureSpec { + files: vec![FeatureSpec::file( + "foo.tex", + "\\usepackage{amsfonts}\n\\mathbb{}", + )], + main_file: "foo.tex", + position: Position::new(1, 9), + ..FeatureSpec::default() + }, + ); + assert!(items.is_empty()); + } +} diff --git a/support/texlab/src/completion/latex/begin_command.rs b/support/texlab/src/completion/latex/begin_command.rs new file mode 100644 index 0000000000..ce6ba17afa --- /dev/null +++ b/support/texlab/src/completion/latex/begin_command.rs @@ -0,0 +1,30 @@ +use super::combinators; +use crate::completion::factory::{self, LatexComponentId}; +use crate::workspace::*; +use futures_boxed::boxed; +use lsp_types::{CompletionItem, CompletionParams}; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct LatexBeginCommandCompletionProvider; + +impl FeatureProvider for LatexBeginCommandCompletionProvider { + type Params = CompletionParams; + type Output = Vec<CompletionItem>; + + #[boxed] + async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output { + combinators::command(request, |_| { + async move { + let snippet = factory::command_snippet( + request, + "begin", + None, + "begin{$1}\n\t$0\n\\end{$1}", + &LatexComponentId::kernel(), + ); + vec![snippet] + } + }) + .await + } +} diff --git a/support/texlab/src/completion/latex/citation.rs b/support/texlab/src/completion/latex/citation.rs new file mode 100644 index 0000000000..a217c24bff --- /dev/null +++ b/support/texlab/src/completion/latex/citation.rs @@ -0,0 +1,144 @@ +use super::combinators::{self, Parameter}; +use crate::completion::factory; +use crate::syntax::*; +use crate::workspace::*; +use futures_boxed::boxed; +use lsp_types::{CompletionItem, CompletionParams, TextEdit}; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct LatexCitationCompletionProvider; + +impl FeatureProvider for LatexCitationCompletionProvider { + type Params = CompletionParams; + type Output = Vec<CompletionItem>; + + #[boxed] + async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output { + let parameters = LANGUAGE_DATA + .citation_commands + .iter() + .map(|cmd| Parameter::new(&cmd.name, cmd.index)); + + combinators::argument(request, parameters, |context| { + async move { + let mut items = Vec::new(); + for document in request.related_documents() { + if let SyntaxTree::Bibtex(tree) = &document.tree { + for entry in &tree.entries() { + if !entry.is_comment() { + if let Some(key) = &entry.key { + let key = key.text().to_owned(); + let text_edit = TextEdit::new(context.range, key.clone()); + let item = factory::citation( + request, + document.uri.clone(), + entry, + key, + text_edit, + ); + items.push(item); + } + } + } + } + } + items + } + }) + .await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::range::RangeExt; + use lsp_types::{Position, Range}; + + #[test] + fn test_empty() { + let items = test_feature( + LatexCitationCompletionProvider, + FeatureSpec { + files: vec![ + FeatureSpec::file("foo.tex", "\\addbibresource{bar.bib}\n\\cite{}"), + FeatureSpec::file("bar.bib", "@article{foo,}"), + FeatureSpec::file("baz.bib", "@article{bar,}"), + ], + main_file: "foo.tex", + position: Position::new(1, 6), + ..FeatureSpec::default() + }, + ); + assert_eq!(items.len(), 1); + assert_eq!(items[0].label, "foo"); + assert_eq!( + items[0].text_edit.as_ref().map(|edit| edit.range), + Some(Range::new_simple(1, 6, 1, 6)) + ); + } + + #[test] + fn test_single_key() { + let items = test_feature( + LatexCitationCompletionProvider, + FeatureSpec { + files: vec![ + FeatureSpec::file("foo.tex", "\\addbibresource{bar.bib}\n\\cite{foo}"), + FeatureSpec::file("bar.bib", "@article{foo,}"), + FeatureSpec::file("baz.bib", "@article{bar,}"), + ], + main_file: "foo.tex", + position: Position::new(1, 6), + ..FeatureSpec::default() + }, + ); + assert_eq!(items.len(), 1); + assert_eq!(items[0].label, "foo"); + assert_eq!( + items[0].text_edit.as_ref().map(|edit| edit.range), + Some(Range::new_simple(1, 6, 1, 9)) + ); + } + + #[test] + fn test_second_key() { + let items = test_feature( + LatexCitationCompletionProvider, + FeatureSpec { + files: vec![ + FeatureSpec::file("foo.tex", "\\addbibresource{bar.bib}\n\\cite{foo,}"), + FeatureSpec::file("bar.bib", "@article{foo,}"), + FeatureSpec::file("baz.bib", "@article{bar,}"), + ], + main_file: "foo.tex", + position: Position::new(1, 10), + ..FeatureSpec::default() + }, + ); + assert_eq!(items.len(), 1); + assert_eq!(items[0].label, "foo"); + assert_eq!( + items[0].text_edit.as_ref().map(|edit| edit.range), + Some(Range::new_simple(1, 10, 1, 10)) + ); + } + + #[test] + fn test_outside_cite() { + let items = test_feature( + LatexCitationCompletionProvider, + FeatureSpec { + files: vec![ + FeatureSpec::file("foo.tex", "\\addbibresource{bar.bib}\n\\cite{}"), + FeatureSpec::file("bar.bib", "@article{foo,}"), + FeatureSpec::file("baz.bib", "@article{bar,}"), + ], + main_file: "foo.tex", + position: Position::new(1, 7), + ..FeatureSpec::default() + }, + ); + assert!(items.is_empty()); + } +} diff --git a/support/texlab/src/completion/latex/color.rs b/support/texlab/src/completion/latex/color.rs new file mode 100644 index 0000000000..b2d390b7d8 --- /dev/null +++ b/support/texlab/src/completion/latex/color.rs @@ -0,0 +1,74 @@ +use super::combinators::{self, Parameter}; +use crate::completion::factory; +use crate::syntax::*; +use crate::workspace::*; +use futures_boxed::boxed; +use lsp_types::{CompletionItem, CompletionParams, TextEdit}; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct LatexColorCompletionProvider; + +impl FeatureProvider for LatexColorCompletionProvider { + type Params = CompletionParams; + type Output = Vec<CompletionItem>; + + #[boxed] + async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output { + let parameters = LANGUAGE_DATA + .color_commands + .iter() + .map(|cmd| Parameter::new(&cmd.name, cmd.index)); + + combinators::argument(request, parameters, |context| { + async move { + let mut items = Vec::new(); + for name in &LANGUAGE_DATA.colors { + let text_edit = TextEdit::new(context.range, name.into()); + let item = factory::color(request, name, text_edit); + items.push(item); + } + items + } + }) + .await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::range::RangeExt; + use lsp_types::{Position, Range}; + + #[test] + fn test_inside_color() { + let items = test_feature( + LatexColorCompletionProvider, + FeatureSpec { + files: vec![FeatureSpec::file("foo.tex", "\\color{}")], + main_file: "foo.tex", + position: Position::new(0, 7), + ..FeatureSpec::default() + }, + ); + assert!(!items.is_empty()); + assert_eq!( + items[0].text_edit.as_ref().map(|edit| edit.range), + Some(Range::new_simple(0, 7, 0, 7)) + ); + } + + #[test] + fn test_outside_color() { + let items = test_feature( + LatexColorCompletionProvider, + FeatureSpec { + files: vec![FeatureSpec::file("foo.tex", "\\color{}")], + main_file: "foo.tex", + position: Position::new(0, 8), + ..FeatureSpec::default() + }, + ); + assert!(items.is_empty()); + } +} diff --git a/support/texlab/src/completion/latex/color_model.rs b/support/texlab/src/completion/latex/color_model.rs new file mode 100644 index 0000000000..5b6e85fa31 --- /dev/null +++ b/support/texlab/src/completion/latex/color_model.rs @@ -0,0 +1,94 @@ +use super::combinators::{self, Parameter}; +use crate::completion::factory; +use crate::syntax::LANGUAGE_DATA; +use crate::workspace::*; +use futures_boxed::boxed; +use lsp_types::{CompletionItem, CompletionParams, TextEdit}; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct LatexColorModelCompletionProvider; + +impl FeatureProvider for LatexColorModelCompletionProvider { + type Params = CompletionParams; + type Output = Vec<CompletionItem>; + + #[boxed] + async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output { + let parameters = LANGUAGE_DATA + .color_model_commands + .iter() + .map(|cmd| Parameter::new(&cmd.name, cmd.index)); + + combinators::argument(&request, parameters, |context| { + async move { + let mut items = Vec::new(); + for name in MODEL_NAMES { + let text_edit = TextEdit::new(context.range, (*name).into()); + let item = factory::color_model(request, name, text_edit); + items.push(item); + } + items + } + }) + .await + } +} + +const MODEL_NAMES: &[&str] = &["gray", "rgb", "RGB", "HTML", "cmyk"]; + +#[cfg(test)] +mod tests { + use super::*; + use crate::range::RangeExt; + use lsp_types::{Position, Range}; + + #[test] + fn test_inside_define_color() { + let items = test_feature( + LatexColorModelCompletionProvider, + FeatureSpec { + files: vec![FeatureSpec::file("foo.tex", "\\definecolor{name}{}")], + main_file: "foo.tex", + position: Position::new(0, 19), + ..FeatureSpec::default() + }, + ); + assert!(!items.is_empty()); + assert_eq!( + items[0].text_edit.as_ref().map(|edit| edit.range), + Some(Range::new_simple(0, 19, 0, 19)) + ); + } + + #[test] + fn test_outside_define_color() { + let items = test_feature( + LatexColorModelCompletionProvider, + FeatureSpec { + files: vec![FeatureSpec::file("foo.tex", "\\definecolor{name}{}")], + main_file: "foo.tex", + position: Position::new(0, 18), + ..FeatureSpec::default() + }, + ); + assert!(items.is_empty()); + } + + #[test] + fn tet_inside_define_color_set() { + let items = test_feature( + LatexColorModelCompletionProvider, + FeatureSpec { + files: vec![FeatureSpec::file("foo.tex", "\\definecolorset{}")], + main_file: "foo.tex", + position: Position::new(0, 16), + ..FeatureSpec::default() + }, + ); + assert!(!items.is_empty()); + assert_eq!( + items[0].text_edit.as_ref().map(|edit| edit.range), + Some(Range::new_simple(0, 16, 0, 16)) + ); + } +} diff --git a/support/texlab/src/completion/latex/combinators.rs b/support/texlab/src/completion/latex/combinators.rs new file mode 100644 index 0000000000..74337f85b6 --- /dev/null +++ b/support/texlab/src/completion/latex/combinators.rs @@ -0,0 +1,152 @@ +use crate::range::RangeExt; +use crate::syntax::*; +use crate::workspace::*; +use lsp_types::*; +use std::future::Future; +use std::sync::Arc; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct Parameter<'a> { + pub name: &'a str, + pub index: usize, +} + +impl<'a> Parameter<'a> { + pub fn new(name: &'a str, index: usize) -> Self { + Self { name, index } + } +} + +pub async fn command<E, F>( + request: &FeatureRequest<CompletionParams>, + execute: E, +) -> Vec<CompletionItem> +where + E: FnOnce(Arc<LatexCommand>) -> F, + F: Future<Output = Vec<CompletionItem>>, +{ + if let SyntaxTree::Latex(tree) = &request.document().tree { + if let Some(command) = + tree.find_command_by_name(request.params.text_document_position.position) + { + return execute(command).await; + } + } + Vec::new() +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct ArgumentContext<'a> { + pub parameter: Parameter<'a>, + pub command: Arc<LatexCommand>, + pub range: Range, +} + +pub async fn argument<'a, I, E, F>( + request: &'a FeatureRequest<CompletionParams>, + mut parameters: I, + execute: E, +) -> Vec<CompletionItem> +where + I: Iterator<Item = Parameter<'a>>, + E: FnOnce(ArgumentContext<'a>) -> F, + F: Future<Output = Vec<CompletionItem>>, +{ + if let SyntaxTree::Latex(tree) = &request.document().tree { + let position = request.params.text_document_position.position; + if let Some(command) = find_command(tree, position) { + for parameter in parameters.by_ref() { + if command.name.text() != parameter.name { + continue; + } + + if let Some(args) = command.args.get(parameter.index) { + if args.right.is_some() && !args.range().contains_exclusive(position) { + continue; + } + + let mut range = None; + for child in &args.children { + if let LatexContent::Text(text) = &child { + for word in &text.words { + if word.range().contains(position) { + range = Some(word.range()); + break; + } + } + } + } + let text_range = range.unwrap_or_else(|| Range::new(position, position)); + let context = ArgumentContext { + parameter, + command: Arc::clone(&command), + range: text_range, + }; + return execute(context).await; + } + } + } + } + Vec::new() +} + +pub async fn argument_word<'a, I, E, F>( + request: &'a FeatureRequest<CompletionParams>, + mut parameters: I, + execute: E, +) -> Vec<CompletionItem> +where + I: Iterator<Item = Parameter<'a>>, + E: FnOnce(Arc<LatexCommand>, usize) -> F, + F: Future<Output = Vec<CompletionItem>>, +{ + if let SyntaxTree::Latex(tree) = &request.document().tree { + let position = request.params.text_document_position.position; + if let Some(command) = find_command(tree, position) { + for parameter in parameters.by_ref() { + if command.name.text() != parameter.name { + continue; + } + + if let Some(args) = command.args.get(parameter.index) { + if args.right.is_some() && !args.range().contains_exclusive(position) { + continue; + } + + if !args.children.is_empty() && !command.has_word(parameter.index) { + continue; + } + + return execute(Arc::clone(&command), parameter.index).await; + } + } + } + } + Vec::new() +} + +pub async fn environment<'a, E, F>( + request: &'a FeatureRequest<CompletionParams>, + execute: E, +) -> Vec<CompletionItem> +where + E: FnOnce(ArgumentContext<'a>) -> F, + F: Future<Output = Vec<CompletionItem>>, +{ + let parameters = LANGUAGE_DATA + .environment_commands + .iter() + .map(|cmd| Parameter::new(&cmd.name, cmd.index)); + argument(request, parameters, execute).await +} + +fn find_command(tree: &LatexSyntaxTree, position: Position) -> Option<Arc<LatexCommand>> { + let mut nodes = tree.find(position); + nodes.reverse(); + for node in nodes { + if let LatexNode::Command(command) = node { + return Some(command); + } + } + None +} diff --git a/support/texlab/src/completion/latex/component.rs b/support/texlab/src/completion/latex/component.rs new file mode 100644 index 0000000000..3699a4959e --- /dev/null +++ b/support/texlab/src/completion/latex/component.rs @@ -0,0 +1,278 @@ +use super::combinators; +use crate::completion::factory::{self, LatexComponentId}; +use crate::completion::DATABASE; +use crate::workspace::*; +use futures_boxed::boxed; +use lsp_types::*; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct LatexComponentCommandCompletionProvider; + +impl FeatureProvider for LatexComponentCommandCompletionProvider { + type Params = CompletionParams; + type Output = Vec<CompletionItem>; + + #[boxed] + async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output { + combinators::command(request, |command| { + async move { + let range = command.short_name_range(); + let mut items = Vec::new(); + for component in DATABASE.related_components(request.related_documents()) { + let file_names = component.file_names.iter().map(AsRef::as_ref).collect(); + let id = LatexComponentId::Component(file_names); + for command in &component.commands { + let text_edit = TextEdit::new(range, (&command.name).into()); + let item = factory::command( + request, + (&command.name).into(), + command.image.as_ref().map(AsRef::as_ref), + command.glyph.as_ref().map(AsRef::as_ref), + text_edit, + &id, + ); + items.push(item); + } + } + items + } + }) + .await + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct LatexComponentEnvironmentCompletionProvider; + +impl FeatureProvider for LatexComponentEnvironmentCompletionProvider { + type Params = CompletionParams; + type Output = Vec<CompletionItem>; + + #[boxed] + async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output { + combinators::environment(request, |context| { + async move { + let mut items = Vec::new(); + for component in DATABASE.related_components(request.related_documents()) { + let file_names = component.file_names.iter().map(AsRef::as_ref).collect(); + let id = LatexComponentId::Component(file_names); + for environment in &component.environments { + let text_edit = TextEdit::new(context.range, environment.into()); + let item = + factory::environment(request, environment.into(), text_edit, &id); + items.push(item); + } + } + items + } + }) + .await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::range::RangeExt; + use lsp_types::{Position, Range}; + + #[test] + fn test_command_start() { + let items = test_feature( + LatexComponentCommandCompletionProvider, + FeatureSpec { + files: vec![FeatureSpec::file("foo.tex", "\\use")], + main_file: "foo.tex", + position: Position::new(0, 0), + ..FeatureSpec::default() + }, + ); + assert!(items.is_empty()); + } + + #[test] + fn test_command_end() { + let items = test_feature( + LatexComponentCommandCompletionProvider, + FeatureSpec { + files: vec![FeatureSpec::file("foo.tex", "\\use")], + main_file: "foo.tex", + position: Position::new(0, 4), + ..FeatureSpec::default() + }, + ); + assert!(!items.is_empty()); + assert_eq!( + items[0].text_edit.as_ref().map(|edit| edit.range), + Some(Range::new_simple(0, 1, 0, 4)) + ); + } + + #[test] + fn test_command_word() { + let items = test_feature( + LatexComponentCommandCompletionProvider, + FeatureSpec { + files: vec![FeatureSpec::file("foo.tex", "use")], + main_file: "foo.tex", + position: Position::new(0, 2), + ..FeatureSpec::default() + }, + ); + assert!(items.is_empty()); + } + + #[test] + fn test_command_package() { + let items = test_feature( + LatexComponentCommandCompletionProvider, + FeatureSpec { + files: vec![FeatureSpec::file("foo.tex", "\\usepackage{lipsum}\n\\lips")], + main_file: "foo.tex", + position: Position::new(1, 2), + ..FeatureSpec::default() + }, + ); + assert!(items.iter().any(|item| item.label == "lipsum")); + } + + #[test] + fn test_command_package_comma_separated() { + let items = test_feature( + LatexComponentCommandCompletionProvider, + FeatureSpec { + files: vec![FeatureSpec::file( + "foo.tex", + "\\usepackage{geometry, lipsum}\n\\lips", + )], + main_file: "foo.tex", + position: Position::new(1, 2), + ..FeatureSpec::default() + }, + ); + assert!(items.iter().any(|item| item.label == "lipsum")); + } + + #[test] + fn test_command_class() { + let items = test_feature( + LatexComponentCommandCompletionProvider, + FeatureSpec { + files: vec![FeatureSpec::file( + "foo.tex", + "\\documentclass{book}\n\\chap", + )], + main_file: "foo.tex", + position: Position::new(1, 2), + ..FeatureSpec::default() + }, + ); + assert!(items.iter().any(|item| item.label == "chapter")); + } + + #[test] + fn test_environment_inside_of_empty_begin() { + let items = test_feature( + LatexComponentEnvironmentCompletionProvider, + FeatureSpec { + files: vec![FeatureSpec::file("foo.tex", "\\begin{}")], + main_file: "foo.tex", + position: Position::new(0, 7), + ..FeatureSpec::default() + }, + ); + assert!(!items.is_empty()); + assert_eq!( + items[0].text_edit.as_ref().map(|edit| edit.range), + Some(Range::new_simple(0, 7, 0, 7)) + ); + } + + #[test] + fn test_environment_inside_of_non_empty_end() { + let items = test_feature( + LatexComponentEnvironmentCompletionProvider, + FeatureSpec { + files: vec![FeatureSpec::file("foo.tex", "\\end{foo}")], + main_file: "foo.tex", + position: Position::new(0, 6), + ..FeatureSpec::default() + }, + ); + assert!(!items.is_empty()); + assert_eq!( + items[0].text_edit.as_ref().map(|edit| edit.range), + Some(Range::new_simple(0, 5, 0, 8)) + ); + } + + #[test] + fn test_environment_outside_of_empty_begin() { + let items = test_feature( + LatexComponentEnvironmentCompletionProvider, + FeatureSpec { + files: vec![FeatureSpec::file("foo.tex", "\\begin{}")], + main_file: "foo.tex", + position: Position::new(0, 6), + ..FeatureSpec::default() + }, + ); + assert!(items.is_empty()); + } + + #[test] + fn test_environment_outside_of_empty_end() { + let items = test_feature( + LatexComponentEnvironmentCompletionProvider, + FeatureSpec { + files: vec![FeatureSpec::file("foo.tex", "\\end{}")], + main_file: "foo.tex", + position: Position::new(0, 6), + ..FeatureSpec::default() + }, + ); + assert!(items.is_empty()); + } + + #[test] + fn test_environment_inside_of_other_command() { + let items = test_feature( + LatexComponentEnvironmentCompletionProvider, + FeatureSpec { + files: vec![FeatureSpec::file("foo.tex", "\\foo{bar}")], + main_file: "foo.tex", + position: Position::new(0, 6), + ..FeatureSpec::default() + }, + ); + assert!(items.is_empty()); + } + + #[test] + fn test_environment_inside_second_argument() { + let items = test_feature( + LatexComponentEnvironmentCompletionProvider, + FeatureSpec { + files: vec![FeatureSpec::file("foo.tex", "\\begin{foo}{bar}")], + main_file: "foo.tex", + position: Position::new(0, 14), + ..FeatureSpec::default() + }, + ); + assert!(items.is_empty()); + } + + #[test] + fn test_environment_unterminated() { + let items = test_feature( + LatexComponentEnvironmentCompletionProvider, + FeatureSpec { + files: vec![FeatureSpec::file("foo.tex", "\\begin{ foo")], + main_file: "foo.tex", + position: Position::new(0, 7), + ..FeatureSpec::default() + }, + ); + assert!(!items.is_empty()); + } +} diff --git a/support/texlab/src/completion/latex/glossary.rs b/support/texlab/src/completion/latex/glossary.rs new file mode 100644 index 0000000000..ca6600b690 --- /dev/null +++ b/support/texlab/src/completion/latex/glossary.rs @@ -0,0 +1,53 @@ +use super::combinators::{self, Parameter}; +use crate::completion::factory; +use crate::syntax::LatexGlossaryEntryKind::*; +use crate::syntax::*; +use crate::workspace::*; +use futures_boxed::boxed; +use lsp_types::{CompletionItem, CompletionParams, TextEdit}; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct LatexGlossaryCompletionProvider; + +impl FeatureProvider for LatexGlossaryCompletionProvider { + type Params = CompletionParams; + type Output = Vec<CompletionItem>; + + #[boxed] + async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output { + let parameters = LANGUAGE_DATA + .glossary_entry_reference_commands + .iter() + .map(|cmd| Parameter::new(&cmd.name, cmd.index)); + + combinators::argument(request, parameters, |context| { + async move { + let cmd_kind = LANGUAGE_DATA + .glossary_entry_reference_commands + .iter() + .find(|cmd| cmd.name == context.parameter.name) + .unwrap() + .kind; + + let mut items = Vec::new(); + for document in request.related_documents() { + if let SyntaxTree::Latex(tree) = &document.tree { + for entry in &tree.glossary.entries { + match (cmd_kind, entry.kind) { + (Acronym, Acronym) | (General, General) | (General, Acronym) => { + let label = entry.label().text().to_owned(); + let text_edit = TextEdit::new(context.range, label.clone()); + let item = factory::glossary_entry(request, label, text_edit); + items.push(item); + } + (Acronym, General) => {} + } + } + } + } + items + } + }) + .await + } +} diff --git a/support/texlab/src/completion/latex/import.rs b/support/texlab/src/completion/latex/import.rs new file mode 100644 index 0000000000..7f333f17ae --- /dev/null +++ b/support/texlab/src/completion/latex/import.rs @@ -0,0 +1,110 @@ +use super::combinators::{self, Parameter}; +use crate::completion::factory; +use crate::completion::DATABASE; +use crate::syntax::*; +use crate::workspace::*; +use futures_boxed::boxed; +use lsp_types::{CompletionItem, CompletionParams, TextEdit}; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct LatexClassImportProvider; + +impl FeatureProvider for LatexClassImportProvider { + type Params = CompletionParams; + type Output = Vec<CompletionItem>; + + #[boxed] + async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output { + import(request, LatexIncludeKind::Class, factory::class).await + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct LatexPackageImportProvider; + +impl FeatureProvider for LatexPackageImportProvider { + type Params = CompletionParams; + type Output = Vec<CompletionItem>; + + #[boxed] + async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output { + import(request, LatexIncludeKind::Package, factory::package).await + } +} + +async fn import<F>( + request: &FeatureRequest<CompletionParams>, + kind: LatexIncludeKind, + factory: F, +) -> Vec<CompletionItem> +where + F: Fn(&FeatureRequest<CompletionParams>, &'static str, TextEdit) -> CompletionItem, +{ + let extension = if kind == LatexIncludeKind::Package { + "sty" + } else { + "cls" + }; + + let parameters = LANGUAGE_DATA + .include_commands + .iter() + .filter(|cmd| cmd.kind == kind) + .map(|cmd| Parameter::new(&cmd.name, cmd.index)); + + combinators::argument(request, parameters, |context| { + async move { + let mut items = Vec::new(); + for component in &DATABASE.components { + for file_name in &component.file_names { + if file_name.ends_with(extension) { + let stem = &file_name[0..file_name.len() - 4]; + let text_edit = TextEdit::new(context.range, stem.into()); + let item = factory(request, stem, text_edit); + items.push(item); + } + } + } + items + } + }) + .await +} + +#[cfg(test)] +mod tests { + use super::*; + use lsp_types::Position; + + #[test] + fn test_class() { + let items = test_feature( + LatexClassImportProvider, + FeatureSpec { + files: vec![FeatureSpec::file("foo.tex", "\\documentclass{}")], + main_file: "foo.tex", + position: Position::new(0, 15), + ..FeatureSpec::default() + }, + ); + + assert!(items.iter().any(|item| item.label == "beamer")); + assert!(items.iter().all(|item| item.label != "amsmath")); + } + + #[test] + fn test_package() { + let items = test_feature( + LatexPackageImportProvider, + FeatureSpec { + files: vec![FeatureSpec::file("foo.tex", "\\usepackage{}")], + main_file: "foo.tex", + position: Position::new(0, 12), + ..FeatureSpec::default() + }, + ); + + assert!(items.iter().all(|item| item.label != "beamer")); + assert!(items.iter().any(|item| item.label == "amsmath")); + } +} diff --git a/support/texlab/src/completion/latex/include.rs b/support/texlab/src/completion/latex/include.rs new file mode 100644 index 0000000000..2f4090b3f3 --- /dev/null +++ b/support/texlab/src/completion/latex/include.rs @@ -0,0 +1,129 @@ +use super::combinators::{self, Parameter}; +use crate::completion::factory; +use crate::range::RangeExt; +use crate::syntax::*; +use crate::workspace::*; +use futures_boxed::boxed; +use lsp_types::{CompletionItem, CompletionParams, Range, TextEdit}; +use std::path::{Path, PathBuf}; +use walkdir::WalkDir; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct LatexIncludeCompletionProvider; + +impl FeatureProvider for LatexIncludeCompletionProvider { + type Params = CompletionParams; + type Output = Vec<CompletionItem>; + + #[boxed] + async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output { + let parameters = LANGUAGE_DATA + .include_commands + .iter() + .map(|cmd| Parameter::new(&cmd.name, cmd.index)); + + combinators::argument_word(request, parameters, |command, index| { + async move { + if !request.document().is_file() { + return Vec::new(); + } + + let position = request.params.text_document_position.position; + let mut items = Vec::new(); + let path_word = command.extract_word(index); + let name_range = match path_word { + Some(path_word) => Range::new_simple( + path_word.start().line, + path_word.end().character + - path_word.text().split('/').last().unwrap().chars().count() as u64, + path_word.end().line, + path_word.end().character, + ), + None => Range::new(position, position), + }; + let directory = current_directory(&request, &command); + + for entry in WalkDir::new(directory) + .min_depth(1) + .max_depth(1) + .follow_links(false) + .into_iter() + .filter_map(std::result::Result::ok) + { + if entry.file_type().is_file() && is_included(&command, &entry.path()) { + let mut path = entry.into_path(); + let include_extension = LANGUAGE_DATA + .include_commands + .iter() + .find(|cmd| command.name.text() == cmd.name) + .unwrap() + .include_extension; + + if !include_extension { + remove_extension(&mut path); + } + let text_edit = make_text_edit(name_range, &path); + items.push(factory::file(request, &path, text_edit)); + } else if entry.file_type().is_dir() { + let path = entry.into_path(); + let text_edit = make_text_edit(name_range, &path); + items.push(factory::folder(request, &path, text_edit)); + } + } + items + } + }) + .await + } +} + +fn current_directory( + request: &FeatureRequest<CompletionParams>, + command: &LatexCommand, +) -> PathBuf { + let mut path = request.document().uri.to_file_path().unwrap(); + path = PathBuf::from(path.to_string_lossy().into_owned().replace('\\', "/")); + + path.pop(); + if let Some(include) = command.extract_word(0) { + path.push(include.text()); + if !include.text().ends_with('/') { + path.pop(); + } + } + path +} + +fn is_included(command: &LatexCommand, file: &Path) -> bool { + if let Some(allowed_extensions) = LANGUAGE_DATA + .include_commands + .iter() + .find(|cmd| command.name.text() == cmd.name) + .unwrap() + .kind + .extensions() + { + file.extension() + .map(|extension| extension.to_string_lossy().to_lowercase()) + .map(|extension| allowed_extensions.contains(&extension.as_str())) + .unwrap_or(false) + } else { + true + } +} + +fn remove_extension(path: &mut PathBuf) { + let stem = path + .file_stem() + .map(|stem| stem.to_string_lossy().into_owned()); + + if let Some(stem) = stem { + path.pop(); + path.push(PathBuf::from(stem)); + } +} + +fn make_text_edit(range: Range, path: &Path) -> TextEdit { + let text = path.file_name().unwrap().to_string_lossy().into_owned(); + TextEdit::new(range, text) +} diff --git a/support/texlab/src/completion/latex/label.rs b/support/texlab/src/completion/latex/label.rs new file mode 100644 index 0000000000..f8d74f4a22 --- /dev/null +++ b/support/texlab/src/completion/latex/label.rs @@ -0,0 +1,156 @@ +use super::combinators::{self, ArgumentContext, Parameter}; +use crate::completion::factory; +use crate::range::RangeExt; +use crate::syntax::*; +use crate::workspace::*; +use futures_boxed::boxed; +use lsp_types::*; +use std::sync::Arc; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct LatexLabelCompletionProvider; + +impl FeatureProvider for LatexLabelCompletionProvider { + type Params = CompletionParams; + type Output = Vec<CompletionItem>; + + #[boxed] + async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output { + let parameters = LANGUAGE_DATA + .label_commands + .iter() + .filter(|cmd| cmd.kind.is_reference()) + .map(|cmd| Parameter::new(&cmd.name, cmd.index)); + + combinators::argument(request, parameters, |context| { + async move { + let source = Self::find_source(&context); + let mut items = Vec::new(); + for document in request.related_documents() { + let workspace = Arc::clone(&request.view.workspace); + let view = DocumentView::new(workspace, Arc::clone(&document)); + let outline = Outline::from(&view); + + if let SyntaxTree::Latex(tree) = &document.tree { + for label in tree + .structure + .labels + .iter() + .filter(|label| label.kind == LatexLabelKind::Definition) + .filter(|label| Self::is_included(tree, label, source)) + { + let outline_context = OutlineContext::parse(&view, &label, &outline); + for name in label.names() { + let text = name.text().to_owned(); + let text_edit = TextEdit::new(context.range, text.clone()); + let item = factory::label( + request, + text, + text_edit, + outline_context.as_ref(), + ); + items.push(item); + } + } + } + } + items + } + }) + .await + } +} + +impl LatexLabelCompletionProvider { + fn find_source(context: &ArgumentContext) -> LatexLabelReferenceSource { + match LANGUAGE_DATA + .label_commands + .iter() + .find(|cmd| cmd.name == context.parameter.name && cmd.index == context.parameter.index) + .map(|cmd| cmd.kind) + .unwrap() + { + LatexLabelKind::Definition => unreachable!(), + LatexLabelKind::Reference(source) => source, + } + } + + fn is_included( + tree: &LatexSyntaxTree, + label: &LatexLabel, + source: LatexLabelReferenceSource, + ) -> bool { + match source { + LatexLabelReferenceSource::Everything => true, + LatexLabelReferenceSource::Math => tree + .env + .environments + .iter() + .filter(|env| env.left.is_math()) + .any(|env| env.range().contains_exclusive(label.start())), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use lsp_types::Position; + + #[test] + fn test_inside_of_ref() { + let items = test_feature( + LatexLabelCompletionProvider, + FeatureSpec { + files: vec![ + FeatureSpec::file( + "foo.tex", + "\\addbibresource{bar.bib}\\include{baz}\n\\ref{}", + ), + FeatureSpec::file("bar.bib", ""), + FeatureSpec::file("baz.tex", "\\label{foo}\\label{bar}\\ref{baz}"), + ], + main_file: "foo.tex", + position: Position::new(1, 5), + ..FeatureSpec::default() + }, + ); + let labels: Vec<&str> = items.iter().map(|item| item.label.as_ref()).collect(); + assert_eq!(labels, vec!["foo", "bar"]); + } + + #[test] + fn test_outside_of_ref() { + let items = test_feature( + LatexLabelCompletionProvider, + FeatureSpec { + files: vec![ + FeatureSpec::file("foo.tex", "\\include{bar}\\ref{}"), + FeatureSpec::file("bar.tex", "\\label{foo}\\label{bar}"), + ], + main_file: "foo.tex", + position: Position::new(1, 6), + ..FeatureSpec::default() + }, + ); + assert!(items.is_empty()); + } + + #[test] + fn test_eqref() { + let items = test_feature( + LatexLabelCompletionProvider, + FeatureSpec { + files: vec![FeatureSpec::file( + "foo.tex", + "\\begin{align}\\label{foo}\\end{align}\\label{bar}\n\\eqref{}", + )], + main_file: "foo.tex", + position: Position::new(1, 7), + ..FeatureSpec::default() + }, + ); + let labels: Vec<&str> = items.iter().map(|item| item.label.as_ref()).collect(); + assert_eq!(labels, vec!["foo"]); + } +} diff --git a/support/texlab/src/completion/latex/mod.rs b/support/texlab/src/completion/latex/mod.rs new file mode 100644 index 0000000000..2902678c11 --- /dev/null +++ b/support/texlab/src/completion/latex/mod.rs @@ -0,0 +1,14 @@ +pub mod argument; +pub mod begin_command; +pub mod citation; +pub mod color; +pub mod color_model; +pub mod combinators; +pub mod component; +pub mod glossary; +pub mod import; +pub mod include; +pub mod label; +pub mod theorem; +pub mod tikz; +pub mod user; diff --git a/support/texlab/src/completion/latex/theorem.rs b/support/texlab/src/completion/latex/theorem.rs new file mode 100644 index 0000000000..993ef5c7e8 --- /dev/null +++ b/support/texlab/src/completion/latex/theorem.rs @@ -0,0 +1,70 @@ +use super::combinators; +use crate::completion::factory::{self, LatexComponentId}; +use crate::syntax::*; +use crate::workspace::*; +use futures_boxed::boxed; +use lsp_types::{CompletionItem, CompletionParams, TextEdit}; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct LatexTheoremEnvironmentCompletionProvider; + +impl FeatureProvider for LatexTheoremEnvironmentCompletionProvider { + type Params = CompletionParams; + type Output = Vec<CompletionItem>; + + #[boxed] + async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output { + combinators::environment(request, |context| { + async move { + let mut items = Vec::new(); + for document in request.related_documents() { + if let SyntaxTree::Latex(tree) = &document.tree { + for theorem in &tree.math.theorem_definitions { + let name = theorem.name().text().to_owned(); + let text_edit = TextEdit::new(context.range, name.clone()); + let item = factory::environment( + request, + name, + text_edit, + &LatexComponentId::User, + ); + items.push(item); + } + } + } + items + } + }) + .await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::range::RangeExt; + use lsp_types::{Position, Range}; + use std::borrow::Cow; + + #[test] + fn test() { + let items = test_feature( + LatexTheoremEnvironmentCompletionProvider, + FeatureSpec { + files: vec![FeatureSpec::file( + "foo.tex", + "\\newtheorem{theorem}{Theorem}\n\\begin{th}", + )], + main_file: "foo.tex", + position: Position::new(1, 8), + ..FeatureSpec::default() + }, + ); + assert_eq!(items.len(), 1); + assert_eq!(items[0].label, Cow::from("theorem")); + assert_eq!( + items[0].text_edit.as_ref().map(|edit| edit.range), + Some(Range::new_simple(1, 7, 1, 9)) + ); + } +} diff --git a/support/texlab/src/completion/latex/tikz.rs b/support/texlab/src/completion/latex/tikz.rs new file mode 100644 index 0000000000..2d6740e1a8 --- /dev/null +++ b/support/texlab/src/completion/latex/tikz.rs @@ -0,0 +1,90 @@ +use super::combinators::{self, Parameter}; +use crate::completion::factory; +use crate::syntax::LANGUAGE_DATA; +use crate::workspace::*; +use futures_boxed::boxed; +use lsp_types::{CompletionItem, CompletionParams, TextEdit}; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct LatexPgfLibraryCompletionProvider; + +impl FeatureProvider for LatexPgfLibraryCompletionProvider { + type Params = CompletionParams; + type Output = Vec<CompletionItem>; + + #[boxed] + async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output { + let parameter = Parameter::new("\\usepgflibrary", 0); + combinators::argument(request, std::iter::once(parameter), |context| { + async move { + let mut items = Vec::new(); + for name in &LANGUAGE_DATA.pgf_libraries { + let text_edit = TextEdit::new(context.range, name.into()); + let item = factory::pgf_library(request, name, text_edit); + items.push(item); + } + items + } + }) + .await + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct LatexTikzLibraryCompletionProvider; + +impl FeatureProvider for LatexTikzLibraryCompletionProvider { + type Params = CompletionParams; + type Output = Vec<CompletionItem>; + + #[boxed] + async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output { + let parameter = Parameter::new("\\usetikzlibrary", 0); + combinators::argument(request, std::iter::once(parameter), |context| { + async move { + let mut items = Vec::new(); + for name in &LANGUAGE_DATA.tikz_libraries { + let text_edit = TextEdit::new(context.range, name.into()); + let item = factory::tikz_library(request, name, text_edit); + items.push(item); + } + items + } + }) + .await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use lsp_types::Position; + + #[test] + fn test_pgf_library() { + let items = test_feature( + LatexPgfLibraryCompletionProvider, + FeatureSpec { + files: vec![FeatureSpec::file("foo.tex", "\\usepgflibrary{}")], + main_file: "foo.tex", + position: Position::new(0, 15), + ..FeatureSpec::default() + }, + ); + assert!(!items.is_empty()); + } + + #[test] + fn test_tikz_library() { + let items = test_feature( + LatexTikzLibraryCompletionProvider, + FeatureSpec { + files: vec![FeatureSpec::file("foo.tex", "\\usetikzlibrary{}")], + main_file: "foo.tex", + position: Position::new(0, 16), + ..FeatureSpec::default() + }, + ); + assert!(!items.is_empty()); + } +} diff --git a/support/texlab/src/completion/latex/user.rs b/support/texlab/src/completion/latex/user.rs new file mode 100644 index 0000000000..890a75882b --- /dev/null +++ b/support/texlab/src/completion/latex/user.rs @@ -0,0 +1,156 @@ +use super::combinators; +use crate::completion::factory::{self, LatexComponentId}; +use crate::syntax::*; +use crate::workspace::*; +use futures_boxed::boxed; +use itertools::Itertools; +use lsp_types::*; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct LatexUserCommandCompletionProvider; + +impl FeatureProvider for LatexUserCommandCompletionProvider { + type Params = CompletionParams; + type Output = Vec<CompletionItem>; + + #[boxed] + async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output { + combinators::command(request, |current_command| { + async move { + let mut items = Vec::new(); + for document in request.related_documents() { + if let SyntaxTree::Latex(tree) = &document.tree { + tree.commands + .iter() + .filter(|command| command.range() != current_command.range()) + .map(|command| &command.name.text()[1..]) + .unique() + .map(|command| { + let text_edit = TextEdit::new( + current_command.short_name_range(), + command.to_owned(), + ); + factory::command( + request, + command.to_owned(), + None, + None, + text_edit, + &LatexComponentId::User, + ) + }) + .for_each(|item| items.push(item)); + } + } + items + } + }) + .await + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct LatexUserEnvironmentCompletionProvider; + +impl FeatureProvider for LatexUserEnvironmentCompletionProvider { + type Params = CompletionParams; + type Output = Vec<CompletionItem>; + + #[boxed] + async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output { + combinators::environment(request, |context| { + async move { + let mut items = Vec::new(); + for document in request.related_documents() { + if let SyntaxTree::Latex(tree) = &document.tree { + for environment in &tree.env.environments { + if environment.left.command == context.command + || environment.right.command == context.command + { + continue; + } + + if let Some(item) = + Self::make_item(request, &environment.left, context.range) + { + items.push(item); + } + + if let Some(item) = + Self::make_item(request, &environment.right, context.range) + { + items.push(item); + } + } + } + } + items + } + }) + .await + } +} + +impl LatexUserEnvironmentCompletionProvider { + fn make_item( + request: &FeatureRequest<CompletionParams>, + delimiter: &LatexEnvironmentDelimiter, + name_range: Range, + ) -> Option<CompletionItem> { + if let Some(name) = delimiter.name() { + let text = name.text().to_owned(); + let text_edit = TextEdit::new(name_range, text.clone()); + let item = factory::environment(request, text, text_edit, &LatexComponentId::User); + return Some(item); + } + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + use lsp_types::Position; + + #[test] + fn test_command() { + let items = test_feature( + LatexUserCommandCompletionProvider, + FeatureSpec { + files: vec![ + FeatureSpec::file("foo.tex", "\\include{bar.tex}\n\\foo"), + FeatureSpec::file("bar.tex", "\\bar"), + FeatureSpec::file("baz.tex", "\\baz"), + ], + main_file: "foo.tex", + position: Position::new(1, 2), + ..FeatureSpec::default() + }, + ); + let labels: Vec<&str> = items.iter().map(|item| item.label.as_ref()).collect(); + assert_eq!(labels, vec!["include", "bar"]); + } + + #[test] + fn test_environment() { + let items = test_feature( + LatexUserEnvironmentCompletionProvider, + FeatureSpec { + files: vec![ + FeatureSpec::file("foo.tex", "\\include{bar.tex}\n\\begin{foo}"), + FeatureSpec::file("bar.tex", "\\begin{bar}\\end{bar}"), + FeatureSpec::file("baz.tex", "\\begin{baz}\\end{baz}"), + ], + main_file: "foo.tex", + position: Position::new(1, 9), + ..FeatureSpec::default() + }, + ); + let labels: Vec<&str> = items + .iter() + .map(|item| item.label.as_ref()) + .unique() + .collect(); + assert_eq!(labels, vec!["bar"]); + } +} |