diff options
Diffstat (limited to 'support/texlab/src/completion/latex')
17 files changed, 2065 insertions, 1364 deletions
diff --git a/support/texlab/src/completion/latex/argument.rs b/support/texlab/src/completion/latex/argument.rs index db05bca824..5736099ca6 100644 --- a/support/texlab/src/completion/latex/argument.rs +++ b/support/texlab/src/completion/latex/argument.rs @@ -1,44 +1,204 @@ use super::combinators::{self, Parameter}; -use crate::completion::factory; -use crate::completion::DATABASE; -use crate::workspace::*; -use futures_boxed::boxed; -use lsp_types::*; +use crate::{ + completion::types::{Item, ItemData}, + feature::FeatureRequest, + protocol::CompletionParams, +}; use std::iter; -#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub async fn complete_latex_arguments<'a>( + req: &'a FeatureRequest<CompletionParams>, + items: &mut Vec<Item<'a>>, +) { + for comp in req.view.components() { + for cmd in &comp.commands { + for (i, param) in cmd.parameters.iter().enumerate() { + complete_internal(req, items, &cmd.name, i, param).await; + } + } + } +} + +async fn complete_internal<'a>( + req: &'a FeatureRequest<CompletionParams>, + items: &mut Vec<Item<'a>>, + name: &'a str, + index: usize, + param: &'a crate::components::Parameter, +) { + combinators::argument( + req, + iter::once(Parameter { name, index }), + |ctx| async move { + for arg in ¶m.0 { + let item = Item::new( + ctx.range, + ItemData::Argument { + name: &arg.name, + image: arg.image.as_deref(), + }, + ); + items.push(item); + } + }, + ) + .await; +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + feature::FeatureTester, + protocol::{Range, RangeExt}, + }; + use indoc::indoc; + + #[tokio::test] + async fn empty_latex_document() { + let req = FeatureTester::new() + .file("main.tex", "") + .main("main.tex") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_arguments(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); + } + + #[tokio::test] + async fn empty_bibtex_document() { + let req = FeatureTester::new() + .file("main.bib", "") + .main("main.bib") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_arguments(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); + } + + #[tokio::test] + async fn inside_mathbb_empty() { + let req = FeatureTester::new() + .file( + "main.tex", + indoc!( + r#" + \usepackage{amsfonts} + \mathbb{} + "# + ), + ) + .main("main.tex") + .position(1, 8) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_arguments(&req, &mut actual_items).await; + + assert!(!actual_items.is_empty()); + assert_eq!(actual_items[0].range, Range::new_simple(1, 8, 1, 8)); + } + + #[tokio::test] + async fn inside_mathbb_non_empty() { + let req = FeatureTester::new() + .file( + "main.tex", + indoc!( + r#" + \usepackage{amsfonts} + \mathbb{foo} + "# + ), + ) + .main("main.tex") + .position(1, 8) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_arguments(&req, &mut actual_items).await; + + assert!(!actual_items.is_empty()); + assert_eq!(actual_items[0].range, Range::new_simple(1, 8, 1, 11)); + } + + #[tokio::test] + async fn outside_mathbb_empty() { + let req = FeatureTester::new() + .file( + "main.tex", + indoc!( + r#" + \usepackage{amsfonts} + \mathbb{} + "# + ), + ) + .main("main.tex") + .position(1, 9) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_arguments(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); + } +} + +/* +use super::combinators::{self, Parameter}; +use crate::{ + completion::factory, + feature::{FeatureProvider, FeatureRequest}, + protocol::{CompletionItem, CompletionParams, TextEdit}, +}; +use async_trait::async_trait; +use std::iter; + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] pub struct LatexArgumentCompletionProvider; +#[async_trait] 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 { + async fn execute<'a>(&'a self, req: &'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() { + for comp in req.view.components() { + for cmd in &comp.commands { + let name = format!("\\{}", cmd.name); + for (i, param) in cmd.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 + req, + iter::once(Parameter { + name: &name, + index: i, + }), + |ctx| async move { + let mut items = Vec::new(); + for arg in ¶m.0 { + let text_edit = TextEdit::new(ctx.range, (&arg.name).into()); + let item = factory::argument( + req, + &arg.name, + text_edit, + arg.image.as_ref().map(AsRef::as_ref), + ); + items.push(item); } + items }, ) .await; @@ -50,68 +210,6 @@ impl FeatureProvider for LatexArgumentCompletionProvider { } } -#[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_cmd.rs b/support/texlab/src/completion/latex/begin_cmd.rs new file mode 100644 index 0000000000..44e7a4f60d --- /dev/null +++ b/support/texlab/src/completion/latex/begin_cmd.rs @@ -0,0 +1,70 @@ +use super::combinators; +use crate::{ + completion::types::{Item, ItemData}, + feature::FeatureRequest, + protocol::CompletionParams, +}; + +pub async fn complete_latex_begin_command<'a>( + req: &'a FeatureRequest<CompletionParams>, + items: &mut Vec<Item<'a>>, +) { + combinators::command(req, |cmd_node| async move { + let table = req.current().content.as_latex().unwrap(); + let cmd = table.as_command(cmd_node).unwrap(); + let range = cmd.short_name_range(); + items.push(Item::new(range, ItemData::BeginCommand)); + }) + .await; +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::feature::FeatureTester; + + #[tokio::test] + async fn empty_latex_document() { + let req = FeatureTester::new() + .file("main.tex", "") + .main("main.tex") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_begin_command(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); + } + + #[tokio::test] + async fn empty_bibtex_document() { + let req = FeatureTester::new() + .file("main.bib", "") + .main("main.bib") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_begin_command(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); + } + + #[tokio::test] + async fn after_backslash() { + let req = FeatureTester::new() + .file("main.tex", r#"\"#) + .main("main.tex") + .position(0, 1) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_begin_command(&req, &mut actual_items).await; + + assert_eq!(actual_items.len(), 1); + } +} diff --git a/support/texlab/src/completion/latex/begin_command.rs b/support/texlab/src/completion/latex/begin_command.rs deleted file mode 100644 index ce6ba17afa..0000000000 --- a/support/texlab/src/completion/latex/begin_command.rs +++ /dev/null @@ -1,30 +0,0 @@ -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 index a217c24bff..3e7b1851d1 100644 --- a/support/texlab/src/completion/latex/citation.rs +++ b/support/texlab/src/completion/latex/citation.rs @@ -1,144 +1,261 @@ -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); - } - } - } +use super::combinators::{self, ArgumentContext, Parameter}; +use crate::{ + completion::types::{Item, ItemData}, + feature::FeatureRequest, + protocol::{BibtexFormattingOptions, CompletionParams}, + syntax::{bibtex, BibtexEntryTypeCategory, Structure, LANGUAGE_DATA}, + workspace::{Document, DocumentContent}, +}; +use once_cell::sync::Lazy; +use petgraph::graph::NodeIndex; +use regex::Regex; + +pub async fn complete_latex_citations<'a>( + req: &'a FeatureRequest<CompletionParams>, + items: &mut Vec<Item<'a>>, +) { + let parameters = LANGUAGE_DATA.citation_commands.iter().map(|cmd| Parameter { + name: &cmd.name[1..], + index: cmd.index, + }); + + combinators::argument(req, parameters, |ctx| async move { + for doc in req.related() { + if let DocumentContent::Bibtex(tree) = &doc.content { + for entry_node in tree.children(tree.root) { + if let Some(item) = make_item(ctx, doc, tree, entry_node) { + items.push(item); } } - items } - }) - .await + } + }) + .await; +} + +fn make_item<'a>( + ctx: ArgumentContext, + doc: &'a Document, + tree: &'a bibtex::Tree, + entry_node: NodeIndex, +) -> Option<Item<'a>> { + let entry = tree.as_entry(entry_node)?; + if entry.is_comment() { + return None; } + + let key = entry.key.as_ref()?.text(); + let options = BibtexFormattingOptions::default(); + let params = bibtex::FormattingParams { + insert_spaces: true, + tab_size: 4, + options: &options, + }; + let entry_code = bibtex::format(tree, entry_node, params); + let text = format!( + "{} {}", + &key, + WHITESPACE_REGEX + .replace_all( + &entry_code + .replace('{', "") + .replace('}', "") + .replace(',', " ") + .replace('=', " "), + " ", + ) + .trim() + ); + + let ty = LANGUAGE_DATA + .find_entry_type(&entry.ty.text()[1..]) + .map(|ty| Structure::Entry(ty.category)) + .unwrap_or_else(|| Structure::Entry(BibtexEntryTypeCategory::Misc)); + + let item = Item::new( + ctx.range, + ItemData::Citation { + uri: &doc.uri, + key, + text, + ty, + }, + ); + Some(item) } +static WHITESPACE_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("\\s+").unwrap()); + #[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)) - ); + use crate::{ + feature::FeatureTester, + protocol::{Range, RangeExt}, + }; + use indoc::indoc; + + #[tokio::test] + async fn empty_latex_document() { + let req = FeatureTester::new() + .file("main.tex", "") + .main("main.tex") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_citations(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); } - #[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)) - ); + #[tokio::test] + async fn empty_bibtex_document() { + let req = FeatureTester::new() + .file("main.bib", "") + .main("main.bib") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_citations(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); } - #[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)) - ); + #[tokio::test] + async fn incomplete() { + let req = FeatureTester::new() + .file( + "main.tex", + indoc!( + r#" + \addbibresource{main.bib} + \cite{ + \begin{foo} + \end{bar} + "# + ), + ) + .file("main.bib", "@article{foo,}") + .main("main.tex") + .position(1, 6) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_citations(&req, &mut actual_items).await; + + assert_eq!(actual_items.len(), 1); + assert_eq!(actual_items[0].data.label(), "foo"); + assert_eq!(actual_items[0].range, Range::new_simple(1, 6, 1, 6)); } - #[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()); + #[tokio::test] + async fn empty_key() { + let req = FeatureTester::new() + .file( + "foo.tex", + indoc!( + r#" + \addbibresource{bar.bib} + \cite{} + "# + ), + ) + .file("bar.bib", "@article{foo,}") + .file("baz.bib", "@article{bar,}") + .main("foo.tex") + .position(1, 6) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_citations(&req, &mut actual_items).await; + + assert_eq!(actual_items.len(), 1); + assert_eq!(actual_items[0].data.label(), "foo"); + assert_eq!(actual_items[0].range, Range::new_simple(1, 6, 1, 6)); + } + + #[tokio::test] + async fn single_key() { + let req = FeatureTester::new() + .file( + "foo.tex", + indoc!( + r#" + \addbibresource{bar.bib} + \cite{foo} + "# + ), + ) + .file("bar.bib", "@article{foo,}") + .file("baz.bib", "@article{bar,}") + .main("foo.tex") + .position(1, 6) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_citations(&req, &mut actual_items).await; + + assert_eq!(actual_items.len(), 1); + assert_eq!(actual_items[0].data.label(), "foo"); + assert_eq!(actual_items[0].range, Range::new_simple(1, 6, 1, 9)); + } + + #[tokio::test] + async fn second_key() { + let req = FeatureTester::new() + .file( + "foo.tex", + indoc!( + r#" + \addbibresource{bar.bib} + \cite{foo,} + "# + ), + ) + .file("bar.bib", "@article{foo,}") + .file("baz.bib", "@article{bar,}") + .main("foo.tex") + .position(1, 10) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_citations(&req, &mut actual_items).await; + + assert_eq!(actual_items.len(), 1); + assert_eq!(actual_items[0].data.label(), "foo"); + assert_eq!(actual_items[0].range, Range::new_simple(1, 10, 1, 10)); + } + + #[tokio::test] + async fn outside_cite() { + let req = FeatureTester::new() + .file( + "foo.tex", + indoc!( + r#" + \addbibresource{bar.bib} + \cite{} + "# + ), + ) + .file("bar.bib", "@article{foo,}") + .file("baz.bib", "@article{bar,}") + .main("foo.tex") + .position(1, 7) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_citations(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); } } diff --git a/support/texlab/src/completion/latex/color.rs b/support/texlab/src/completion/latex/color.rs index b2d390b7d8..93b06b178e 100644 --- a/support/texlab/src/completion/latex/color.rs +++ b/support/texlab/src/completion/latex/color.rs @@ -1,74 +1,91 @@ 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}; +use crate::{ + completion::types::{Item, ItemData}, + feature::FeatureRequest, + protocol::CompletionParams, + syntax::LANGUAGE_DATA, +}; -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub struct LatexColorCompletionProvider; +pub async fn complete_latex_colors<'a>( + req: &'a FeatureRequest<CompletionParams>, + items: &mut Vec<Item<'a>>, +) { + let parameters = LANGUAGE_DATA.color_commands.iter().map(|cmd| Parameter { + name: &cmd.name[1..], + index: cmd.index, + }); -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 - } + combinators::argument(req, parameters, |ctx| async move { + for name in &LANGUAGE_DATA.colors { + let item = Item::new(ctx.range, ItemData::Color { name }); + items.push(item); + } + }) + .await; } #[cfg(test)] mod tests { use super::*; - use crate::range::RangeExt; - use lsp_types::{Position, Range}; + use crate::{ + feature::FeatureTester, + protocol::{Range, RangeExt}, + }; - #[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)) - ); + #[tokio::test] + async fn empty_latex_document() { + let req = FeatureTester::new() + .file("main.tex", "") + .main("main.tex") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + complete_latex_colors(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); } - #[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()); + #[tokio::test] + async fn empty_bibtex_document() { + let req = FeatureTester::new() + .file("main.bib", "") + .main("main.bib") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + complete_latex_colors(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); + } + + #[tokio::test] + async fn inside_color() { + let req = FeatureTester::new() + .file("main.tex", r#"\color{}"#) + .main("main.tex") + .position(0, 7) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + complete_latex_colors(&req, &mut actual_items).await; + + assert!(!actual_items.is_empty()); + assert_eq!(actual_items[0].range, Range::new_simple(0, 7, 0, 7)); + } + + #[tokio::test] + async fn inside_define_color_set() { + let req = FeatureTester::new() + .file("main.tex", r#"\color{}"#) + .main("main.tex") + .position(0, 8) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + complete_latex_colors(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); } } diff --git a/support/texlab/src/completion/latex/color_model.rs b/support/texlab/src/completion/latex/color_model.rs index 5b6e85fa31..e0151da2f0 100644 --- a/support/texlab/src/completion/latex/color_model.rs +++ b/support/texlab/src/completion/latex/color_model.rs @@ -1,94 +1,101 @@ 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 - } -} +use crate::{ + completion::types::{Item, ItemData}, + feature::FeatureRequest, + protocol::CompletionParams, + syntax::LANGUAGE_DATA, +}; const MODEL_NAMES: &[&str] = &["gray", "rgb", "RGB", "HTML", "cmyk"]; +pub async fn complete_latex_color_models<'a>( + req: &'a FeatureRequest<CompletionParams>, + items: &mut Vec<Item<'a>>, +) { + let parameters = LANGUAGE_DATA + .color_model_commands + .iter() + .map(|cmd| Parameter { + name: &cmd.name[1..], + index: cmd.index, + }); + + combinators::argument(req, parameters, |ctx| async move { + for name in MODEL_NAMES { + let item = Item::new(ctx.range, ItemData::ColorModel { name }); + items.push(item); + } + }) + .await; +} + #[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)) - ); + use crate::{ + feature::FeatureTester, + protocol::{Range, RangeExt}, + }; + + #[tokio::test] + async fn empty_latex_document() { + let req = FeatureTester::new() + .file("main.tex", "") + .main("main.tex") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_color_models(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); } - #[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()); + #[tokio::test] + async fn empty_bibtex_document() { + let req = FeatureTester::new() + .file("main.bib", "") + .main("main.bib") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_color_models(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); + } + + #[tokio::test] + async fn inside_define_color() { + let req = FeatureTester::new() + .file("main.tex", r#"\definecolor{name}{}"#) + .main("main.tex") + .position(0, 19) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_color_models(&req, &mut actual_items).await; + + assert!(!actual_items.is_empty()); + assert_eq!(actual_items[0].range, Range::new_simple(0, 19, 0, 19)); } - #[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)) - ); + #[tokio::test] + async fn inside_define_color_set() { + let req = FeatureTester::new() + .file("main.tex", r#"\definecolorset{}"#) + .main("main.tex") + .position(0, 16) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_color_models(&req, &mut actual_items).await; + + assert!(!actual_items.is_empty()); + assert_eq!(actual_items[0].range, 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 index 74337f85b6..d8ef336421 100644 --- a/support/texlab/src/completion/latex/combinators.rs +++ b/support/texlab/src/completion/latex/combinators.rs @@ -1,9 +1,10 @@ -use crate::range::RangeExt; -use crate::syntax::*; -use crate::workspace::*; -use lsp_types::*; +use crate::{ + feature::FeatureRequest, + protocol::{CompletionParams, Position, Range, RangeExt}, + syntax::{latex, AstNodeIndex, SyntaxNode, LANGUAGE_DATA}, + workspace::DocumentContent, +}; use std::future::Future; -use std::sync::Arc; #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub struct Parameter<'a> { @@ -11,142 +12,132 @@ pub struct Parameter<'a> { 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> +pub async fn command<E, F>(req: &FeatureRequest<CompletionParams>, execute: E) where - E: FnOnce(Arc<LatexCommand>) -> F, - F: Future<Output = Vec<CompletionItem>>, + E: FnOnce(AstNodeIndex) -> F, + F: Future<Output = ()>, { - 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; + if let DocumentContent::Latex(table) = &req.current().content { + let pos = req.params.text_document_position.position; + if let Some(cmd) = table.find_command_by_short_name_range(pos) { + execute(cmd).await; } } - Vec::new() } -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub struct ArgumentContext<'a> { pub parameter: Parameter<'a>, - pub command: Arc<LatexCommand>, + pub node: AstNodeIndex, pub range: Range, } pub async fn argument<'a, I, E, F>( - request: &'a FeatureRequest<CompletionParams>, + req: &'a FeatureRequest<CompletionParams>, mut parameters: I, execute: E, -) -> Vec<CompletionItem> -where +) where I: Iterator<Item = Parameter<'a>>, E: FnOnce(ArgumentContext<'a>) -> F, - F: Future<Output = Vec<CompletionItem>>, + F: Future<Output = ()>, { - 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) { + if let DocumentContent::Latex(table) = &req.current().content { + let pos = req.params.text_document_position.position; + if let Some(node) = find_command(&table, pos) { + let cmd = table.as_command(node).unwrap(); + for parameter in parameters + .by_ref() + .filter(|param| param.name == &cmd.name.text()[1..]) + { + if let Some(args_node) = + table.extract_group(node, latex::GroupKind::Group, parameter.index) + { + let args = table.as_group(args_node).unwrap(); + if args.right.is_some() && !args.range().contains_exclusive(pos) { 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 range = table + .children(args_node) + .filter_map(|child| table.as_text(child)) + .flat_map(|text| text.words.iter()) + .map(|word| word.range()) + .find(|range| range.contains(pos)) + .unwrap_or_else(|| Range::new(pos, pos)); + let context = ArgumentContext { parameter, - command: Arc::clone(&command), - range: text_range, + node, + range, }; - return execute(context).await; + execute(context).await; + return; } } } } - Vec::new() } pub async fn argument_word<'a, I, E, F>( - request: &'a FeatureRequest<CompletionParams>, + req: &'a FeatureRequest<CompletionParams>, mut parameters: I, execute: E, -) -> Vec<CompletionItem> -where +) where I: Iterator<Item = Parameter<'a>>, - E: FnOnce(Arc<LatexCommand>, usize) -> F, - F: Future<Output = Vec<CompletionItem>>, + E: FnOnce(AstNodeIndex, usize) -> F, + F: Future<Output = ()>, { - 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) { + if let DocumentContent::Latex(table) = &req.current().content { + let pos = req.params.text_document_position.position; + if let Some(node) = find_command(&table, pos) { + let cmd = table.as_command(node).unwrap(); + for parameter in parameters + .by_ref() + .filter(|param| param.name == &cmd.name.text()[1..]) + { + if let Some(args_node) = + table.extract_group(node, latex::GroupKind::Group, parameter.index) + { + let args = table.as_group(args_node).unwrap(); + if args.right.is_some() && !args.range().contains_exclusive(pos) { continue; } - if !args.children.is_empty() && !command.has_word(parameter.index) { + if table.children(args_node).next().is_some() + && table + .extract_word(node, latex::GroupKind::Group, parameter.index) + .is_none() + { continue; } - return execute(Arc::clone(&command), parameter.index).await; + execute(node, parameter.index).await; + return; } } } } - Vec::new() } -pub async fn environment<'a, E, F>( - request: &'a FeatureRequest<CompletionParams>, - execute: E, -) -> Vec<CompletionItem> +pub async fn environment<'a, E, F>(req: &'a FeatureRequest<CompletionParams>, execute: E) where E: FnOnce(ArgumentContext<'a>) -> F, - F: Future<Output = Vec<CompletionItem>>, + F: Future<Output = ()>, { let parameters = LANGUAGE_DATA .environment_commands .iter() - .map(|cmd| Parameter::new(&cmd.name, cmd.index)); - argument(request, parameters, execute).await + .map(|cmd| Parameter { + name: &cmd.name[1..], + index: cmd.index, + }); + argument(req, 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 +fn find_command(table: &latex::SymbolTable, pos: Position) -> Option<AstNodeIndex> { + table + .find(pos) + .into_iter() + .rev() + .find(|node| table.as_command(*node).is_some()) } diff --git a/support/texlab/src/completion/latex/component.rs b/support/texlab/src/completion/latex/component.rs index 3699a4959e..8cf9f494a4 100644 --- a/support/texlab/src/completion/latex/component.rs +++ b/support/texlab/src/completion/latex/component.rs @@ -1,278 +1,351 @@ 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 +use crate::{ + completion::types::{Item, ItemData}, + feature::FeatureRequest, + protocol::CompletionParams, +}; + +pub async fn complete_latex_component_commands<'a>( + req: &'a FeatureRequest<CompletionParams>, + items: &mut Vec<Item<'a>>, +) { + combinators::command(req, |cmd_node| async move { + let table = req.current().content.as_latex().unwrap(); + let cmd = table.as_command(cmd_node).unwrap(); + let range = cmd.short_name_range(); + + for comp in req.view.components() { + for cmd in &comp.commands { + items.push(Item::new( + range, + ItemData::ComponentCommand { + name: &cmd.name, + image: cmd.image.as_deref(), + glyph: cmd.glyph.as_deref(), + file_names: &comp.file_names, + }, + )); } - }) - .await - } + } + }) + .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 +pub async fn complete_latex_component_environments<'a>( + req: &'a FeatureRequest<CompletionParams>, + items: &mut Vec<Item<'a>>, +) { + combinators::environment(req, |ctx| async move { + for comp in req.view.components() { + for env in &comp.environments { + items.push(Item::new( + ctx.range, + ItemData::ComponentEnvironment { + name: env, + file_names: &comp.file_names, + }, + )); } - }) - .await - } + } + }) + .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()); + use crate::{ + feature::FeatureTester, + protocol::{Range, RangeExt}, + }; + use indoc::indoc; + + #[tokio::test] + async fn empty_latex_document_command() { + let req = FeatureTester::new() + .file("main.tex", "") + .main("main.tex") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_component_commands(&req, &mut actual_items).await; + + assert!(actual_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)) - ); + #[tokio::test] + async fn empty_bibtex_document_command() { + let req = FeatureTester::new() + .file("main.bib", "") + .main("main.bib") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_component_commands(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); } - #[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()); + #[tokio::test] + async fn empty_latex_document_environment() { + let req = FeatureTester::new() + .file("main.tex", "") + .main("main.tex") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_component_environments(&req, &mut actual_items).await; + + assert!(actual_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")); + #[tokio::test] + async fn empty_bibtex_document_environment() { + let req = FeatureTester::new() + .file("main.bib", "") + .main("main.bib") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_component_environments(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); } - #[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")); + #[tokio::test] + async fn command_start() { + let req = FeatureTester::new() + .file("main.tex", r#"\use"#) + .main("main.tex") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_component_commands(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); } - #[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")); + #[tokio::test] + async fn command_end() { + let req = FeatureTester::new() + .file("main.tex", r#"\use"#) + .main("main.tex") + .position(0, 4) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_component_commands(&req, &mut actual_items).await; + + assert!(!actual_items.is_empty()); + assert_eq!(actual_items[0].range, Range::new_simple(0, 1, 0, 4)); } - #[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)) - ); + #[tokio::test] + async fn command_word() { + let req = FeatureTester::new() + .file("main.tex", r#"use"#) + .main("main.tex") + .position(0, 2) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_component_commands(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); } - #[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)) - ); + #[tokio::test] + async fn command_package() { + let req = FeatureTester::new() + .file( + "main.tex", + indoc!( + r#" + \usepackage{lipsum} + \lips + "# + ), + ) + .main("main.tex") + .position(1, 2) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_component_commands(&req, &mut actual_items).await; + + assert!(actual_items + .iter() + .any(|item| item.data.label() == "lipsum")); } - #[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()); + #[tokio::test] + async fn command_package_comma_separated() { + let req = FeatureTester::new() + .file( + "main.tex", + indoc!( + r#" + \usepackage{geometry, lipsum} + \lips + "# + ), + ) + .main("main.tex") + .position(1, 2) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_component_commands(&req, &mut actual_items).await; + + assert!(actual_items + .iter() + .any(|item| item.data.label() == "lipsum")); } - #[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()); + #[tokio::test] + async fn command_class() { + let req = FeatureTester::new() + .file( + "main.tex", + indoc!( + r#" + \documentclass{book} + \chap + "# + ), + ) + .main("main.tex") + .position(1, 2) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_component_commands(&req, &mut actual_items).await; + + assert!(actual_items + .iter() + .any(|item| item.data.label() == "chapter")); + } + + #[tokio::test] + async fn environment_inside_of_empty_begin() { + let req = FeatureTester::new() + .file("main.tex", r#"\begin{}"#) + .main("main.tex") + .position(0, 7) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_component_environments(&req, &mut actual_items).await; + + assert!(!actual_items.is_empty()); + assert_eq!(actual_items[0].range, Range::new_simple(0, 7, 0, 7)); } - #[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()); + #[tokio::test] + async fn environment_inside_of_non_empty_end() { + let req = FeatureTester::new() + .file("main.tex", r#"\end{foo}"#) + .main("main.tex") + .position(0, 6) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_component_environments(&req, &mut actual_items).await; + + assert!(!actual_items.is_empty()); + assert_eq!(actual_items[0].range, Range::new_simple(0, 5, 0, 8)); } - #[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()); + #[tokio::test] + async fn environment_outside_of_empty_begin() { + let req = FeatureTester::new() + .file("main.tex", r#"\begin{}"#) + .main("main.tex") + .position(0, 6) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_component_environments(&req, &mut actual_items).await; + + assert!(actual_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()); + #[tokio::test] + async fn environment_outside_of_empty_end() { + let req = FeatureTester::new() + .file("main.tex", r#"\end{}"#) + .main("main.tex") + .position(0, 6) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_component_environments(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); + } + + #[tokio::test] + async fn environment_inside_of_other_command() { + let req = FeatureTester::new() + .file("main.tex", r#"\foo{bar}"#) + .main("main.tex") + .position(0, 6) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_component_environments(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); + } + + #[tokio::test] + async fn environment_inside_second_argument() { + let req = FeatureTester::new() + .file("main.tex", r#"\begin{foo}{bar}"#) + .main("main.tex") + .position(0, 14) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_component_environments(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); + } + + #[tokio::test] + async fn environment_unterminated() { + let req = FeatureTester::new() + .file("main.tex", r#"\begin{foo"#) + .main("main.tex") + .position(0, 7) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_component_environments(&req, &mut actual_items).await; + + assert!(!actual_items.is_empty()); + assert_eq!(actual_items[0].range, Range::new_simple(0, 7, 0, 10)); } } diff --git a/support/texlab/src/completion/latex/glossary.rs b/support/texlab/src/completion/latex/glossary.rs index ca6600b690..417152dd21 100644 --- a/support/texlab/src/completion/latex/glossary.rs +++ b/support/texlab/src/completion/latex/glossary.rs @@ -1,53 +1,114 @@ 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 +use crate::{ + completion::types::{Item, ItemData}, + feature::FeatureRequest, + protocol::CompletionParams, + syntax::{ + LatexGlossaryEntryKind::{Acronym, General}, + LANGUAGE_DATA, + }, + workspace::DocumentContent, +}; + +pub async fn complete_latex_glossary_entries<'a>( + req: &'a FeatureRequest<CompletionParams>, + items: &mut Vec<Item<'a>>, +) { + let parameters = LANGUAGE_DATA + .glossary_entry_reference_commands + .iter() + .map(|cmd| Parameter { + name: &cmd.name[1..], + index: cmd.index, + }); + + combinators::argument(req, parameters, |ctx| async move { + let cmd_kind = 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) => {} - } + .find(|cmd| &cmd.name[1..] == ctx.parameter.name) + .unwrap() + .kind; + + for doc in req.related() { + if let DocumentContent::Latex(table) = &doc.content { + for entry in &table.glossary_entries { + match (cmd_kind, entry.kind) { + (Acronym, Acronym) | (General, General) | (General, Acronym) => { + let name = entry.label(&table).text(); + let item = Item::new(ctx.range, ItemData::GlossaryEntry { name }); + items.push(item); } + (Acronym, General) => {} } } - items } - }) - .await + } + }) + .await +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + feature::FeatureTester, + protocol::{Range, RangeExt}, + }; + use indoc::indoc; + + #[tokio::test] + async fn empty_latex_document() { + let req = FeatureTester::new() + .file("main.tex", "") + .main("main.tex") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_glossary_entries(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); + } + + #[tokio::test] + async fn empty_bibtex_document() { + let req = FeatureTester::new() + .file("main.bib", "") + .main("main.bib") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_glossary_entries(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); + } + + #[tokio::test] + async fn acronym() { + let req = FeatureTester::new() + .file( + "main.tex", + indoc!( + r#" + \newacronym{lvm}{LVM}{Logical Volume Manager} + \acrfull{foo} + "# + ), + ) + .main("main.tex") + .position(1, 9) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_glossary_entries(&req, &mut actual_items).await; + + assert_eq!(actual_items.len(), 1); + assert_eq!(actual_items[0].data.label(), "lvm"); + assert_eq!(actual_items[0].range, Range::new_simple(1, 9, 1, 12)); } } diff --git a/support/texlab/src/completion/latex/import.rs b/support/texlab/src/completion/latex/import.rs index 7f333f17ae..06c28e189b 100644 --- a/support/texlab/src/completion/latex/import.rs +++ b/support/texlab/src/completion/latex/import.rs @@ -1,44 +1,40 @@ 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 - } -} +use crate::{ + completion::types::{Item, ItemData}, + components::COMPONENT_DATABASE, + feature::FeatureRequest, + protocol::CompletionParams, + syntax::{LatexIncludeKind, LANGUAGE_DATA}, +}; +use std::{borrow::Cow, collections::HashSet}; -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct LatexPackageImportProvider; - -impl FeatureProvider for LatexPackageImportProvider { - type Params = CompletionParams; - type Output = Vec<CompletionItem>; +pub async fn complete_latex_classes<'a>( + req: &'a FeatureRequest<CompletionParams>, + items: &mut Vec<Item<'a>>, +) { + complete_latex_imports(req, items, LatexIncludeKind::Class, |name| { + ItemData::Class { name } + }) + .await; +} - #[boxed] - async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output { - import(request, LatexIncludeKind::Package, factory::package).await - } +pub async fn complete_latex_packages<'a>( + req: &'a FeatureRequest<CompletionParams>, + items: &mut Vec<Item<'a>>, +) { + complete_latex_imports(req, items, LatexIncludeKind::Package, |name| { + ItemData::Package { name } + }) + .await; } -async fn import<F>( - request: &FeatureRequest<CompletionParams>, +async fn complete_latex_imports<'a, F>( + req: &'a FeatureRequest<CompletionParams>, + items: &mut Vec<Item<'a>>, kind: LatexIncludeKind, - factory: F, -) -> Vec<CompletionItem> -where - F: Fn(&FeatureRequest<CompletionParams>, &'static str, TextEdit) -> CompletionItem, + mut factory: F, +) where + F: FnMut(Cow<'a, str>) -> ItemData<'a>, { let extension = if kind == LatexIncludeKind::Package { "sty" @@ -50,61 +46,143 @@ where .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 - } + .map(|cmd| Parameter { + name: &cmd.name[1..], + index: cmd.index, + }); + + combinators::argument(req, parameters, |ctx| async move { + let resolver = req.distro.resolver().await; + let mut file_names = HashSet::new(); + COMPONENT_DATABASE + .components + .iter() + .flat_map(|comp| comp.file_names.iter()) + .filter(|file_name| file_name.ends_with(extension)) + .for_each(|file_name| { + file_names.insert(file_name); + let stem = &file_name[0..file_name.len() - 4]; + let data = factory(stem.into()); + let item = Item::new(ctx.range, data); + items.push(item); + }); + + resolver + .files_by_name + .keys() + .filter(|file_name| file_name.ends_with(extension) && !file_names.contains(file_name)) + .for_each(|file_name| { + let stem = &file_name[0..file_name.len() - 4]; + let data = factory(stem.to_owned().into()); + let item = Item::new(ctx.range, data); + items.push(item); + }); }) - .await + .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")); + use crate::feature::FeatureTester; + + #[tokio::test] + async fn empty_latex_document_class() { + let req = FeatureTester::new() + .file("main.tex", "") + .main("main.tex") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_classes(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); + } + + #[tokio::test] + async fn empty_bibtex_document_class() { + let req = FeatureTester::new() + .file("main.bib", "") + .main("main.bib") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_classes(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); + } + + #[tokio::test] + async fn empty_latex_document_package() { + let req = FeatureTester::new() + .file("main.tex", "") + .main("main.tex") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_packages(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); } - #[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")); + #[tokio::test] + async fn empty_bibtex_document_package() { + let req = FeatureTester::new() + .file("main.bib", "") + .main("main.bib") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_packages(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); + } + + #[tokio::test] + async fn class() { + let req = FeatureTester::new() + .file("main.tex", r#"\documentclass{}"#) + .main("main.tex") + .position(0, 15) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_classes(&req, &mut actual_items).await; + + assert!(actual_items + .iter() + .any(|item| item.data.label() == "beamer")); + assert!(actual_items + .iter() + .all(|item| item.data.label() != "amsmath")); + } + + #[tokio::test] + async fn package() { + let req = FeatureTester::new() + .file("main.tex", r#"\usepackage{}"#) + .main("main.tex") + .position(0, 12) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_packages(&req, &mut actual_items).await; + + assert!(actual_items + .iter() + .all(|item| item.data.label() != "beamer")); + assert!(actual_items + .iter() + .any(|item| item.data.label() == "amsmath")); } } diff --git a/support/texlab/src/completion/latex/include.rs b/support/texlab/src/completion/latex/include.rs index 2f4090b3f3..1a8555d07c 100644 --- a/support/texlab/src/completion/latex/include.rs +++ b/support/texlab/src/completion/latex/include.rs @@ -1,129 +1,133 @@ 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 crate::{ + completion::types::{Item, ItemData}, + feature::FeatureRequest, + protocol::{CompletionParams, Range, RangeExt}, + syntax::{latex, AstNodeIndex, SyntaxNode, LANGUAGE_DATA}, +}; use std::path::{Path, PathBuf}; -use walkdir::WalkDir; +use tokio::fs; -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub struct LatexIncludeCompletionProvider; +pub async fn complete_latex_includes<'a>( + req: &'a FeatureRequest<CompletionParams>, + items: &mut Vec<Item<'a>>, +) { + let parameters = LANGUAGE_DATA.include_commands.iter().map(|cmd| Parameter { + name: &cmd.name[1..], + index: cmd.index, + }); -impl FeatureProvider for LatexIncludeCompletionProvider { - type Params = CompletionParams; - type Output = Vec<CompletionItem>; + combinators::argument_word(req, parameters, |cmd_node, index| async move { + if !req.current().is_file() { + return; + } - #[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)); + make_items(req, items, cmd_node, index).await; + }) + .await; +} - combinators::argument_word(request, parameters, |command, index| { - async move { - if !request.document().is_file() { - return Vec::new(); - } +async fn make_items<'a>( + req: &'a FeatureRequest<CompletionParams>, + items: &mut Vec<Item<'a>>, + cmd_node: AstNodeIndex, + index: usize, +) -> Option<()> { + let table = req.current().content.as_latex()?; + let pos = req.params.text_document_position.position; + let path_word = table.extract_word(cmd_node, latex::GroupKind::Group, 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()?.chars().count() as u64, + path_word.end().line, + path_word.end().character, + ), + None => Range::new(pos, pos), + }; - 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); + let cmd = table.as_command(cmd_node)?; + let current_dir = current_dir(req, table, cmd_node)?; + let mut entries = fs::read_dir(current_dir).await.ok()?; + while let Some(entry) = entries.next_entry().await.ok()? { + let mut path = entry.path(); - 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; + let file_type = entry.file_type().await.ok()?; + if file_type.is_file() && is_included(&cmd, &path) { + let include_extension = LANGUAGE_DATA + .include_commands + .iter() + .find(|c| cmd.name.text() == c.name)? + .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 + if !include_extension { + remove_extension(&mut path); } - }) - .await + let name = path.file_name().unwrap().to_string_lossy().into_owned(); + let item = Item::new(name_range, ItemData::File { name }); + items.push(item); + } else if file_type.is_dir() { + let name = path.file_name().unwrap().to_string_lossy().into_owned(); + let item = Item::new(name_range, ItemData::Directory { name }); + items.push(item); + } } + Some(()) } -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('\\', "/")); +fn current_dir( + req: &FeatureRequest<CompletionParams>, + table: &latex::SymbolTable, + cmd_node: AstNodeIndex, +) -> Option<PathBuf> { + let mut path = req + .options + .latex + .as_ref() + .and_then(|latex| latex.root_directory.as_ref()) + .map_or_else( + || { + let mut path = req.current().uri.to_file_path().unwrap(); + path.pop(); + path + }, + Clone::clone, + ); - path.pop(); - if let Some(include) = command.extract_word(0) { + path = PathBuf::from(path.to_str()?.replace('\\', "/")); + if let Some(include) = table.extract_word(cmd_node, latex::GroupKind::Group, 0) { path.push(include.text()); if !include.text().ends_with('/') { path.pop(); } } - path + Some(path) } -fn is_included(command: &LatexCommand, file: &Path) -> bool { +fn is_included(cmd: &latex::Command, file: &Path) -> bool { if let Some(allowed_extensions) = LANGUAGE_DATA .include_commands .iter() - .find(|cmd| command.name.text() == cmd.name) - .unwrap() - .kind - .extensions() + .find(|c| c.name == cmd.name.text()) + .and_then(|c| c.kind.extensions()) { file.extension() - .map(|extension| extension.to_string_lossy().to_lowercase()) - .map(|extension| allowed_extensions.contains(&extension.as_str())) - .unwrap_or(false) + .and_then(|ext| ext.to_str()) + .map(|ext| ext.to_lowercase()) + .map(|ext| allowed_extensions.contains(&ext.as_str())) + .unwrap_or_default() } else { true } } fn remove_extension(path: &mut PathBuf) { - let stem = path + if let Some(stem) = path .file_stem() - .map(|stem| stem.to_string_lossy().into_owned()); - - if let Some(stem) = stem { + .and_then(|stem| stem.to_str()) + .map(ToOwned::to_owned) + { path.pop(); - path.push(PathBuf::from(stem)); + path.push(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 index f8d74f4a22..91aa80e532 100644 --- a/support/texlab/src/completion/latex/label.rs +++ b/support/texlab/src/completion/latex/label.rs @@ -1,156 +1,230 @@ 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 crate::{ + completion::types::{Item, ItemData}, + feature::{DocumentView, FeatureRequest}, + outline::{Outline, OutlineContext, OutlineContextItem}, + protocol::{CompletionParams, RangeExt}, + syntax::{ + latex, LatexLabelKind, LatexLabelReferenceSource, Structure, SyntaxNode, LANGUAGE_DATA, + }, + workspace::DocumentContent, +}; use std::sync::Arc; -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub struct LatexLabelCompletionProvider; +pub async fn complete_latex_labels<'a>( + req: &'a FeatureRequest<CompletionParams>, + items: &mut Vec<Item<'a>>, +) { + let parameters = LANGUAGE_DATA + .label_commands + .iter() + .filter(|cmd| cmd.kind.is_reference()) + .map(|cmd| Parameter { + name: &cmd.name[1..], + index: cmd.index, + }); -impl FeatureProvider for LatexLabelCompletionProvider { - type Params = CompletionParams; - type Output = Vec<CompletionItem>; + combinators::argument(req, parameters, |ctx| async move { + let source = find_source(ctx); + for doc in req.related() { + let snapshot = Arc::clone(&req.view.snapshot); + let view = + DocumentView::analyze(snapshot, Arc::clone(&doc), &req.options, &req.current_dir); + let outline = Outline::analyze(&view, &req.options, &req.current_dir); - #[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); - } - } + if let DocumentContent::Latex(table) = &doc.content { + for label in table + .labels + .iter() + .filter(|label| label.kind == LatexLabelKind::Definition) + .filter(|label| is_included(&table, label, source)) + { + let outline_ctx = OutlineContext::parse(&view, &outline, *label); + + let kind = match outline_ctx.as_ref().map(|ctx| &ctx.item) { + Some(OutlineContextItem::Section { .. }) => Structure::Section, + Some(OutlineContextItem::Caption { .. }) => Structure::Float, + Some(OutlineContextItem::Theorem { .. }) => Structure::Theorem, + Some(OutlineContextItem::Equation) => Structure::Equation, + Some(OutlineContextItem::Item) => Structure::Item, + None => Structure::Label, + }; + + for name in label.names(&table) { + let header = outline_ctx.as_ref().and_then(|ctx| ctx.detail()); + let footer = outline_ctx.as_ref().and_then(|ctx| match &ctx.item { + OutlineContextItem::Caption { text, .. } => Some(text.clone()), + _ => None, + }); + + let text = outline_ctx + .as_ref() + .map(|ctx| format!("{} {}", name.text(), ctx.reference())) + .unwrap_or_else(|| name.text().into()); + + let item = Item::new( + ctx.range, + ItemData::Label { + name: name.text(), + kind, + header, + footer, + text, + }, + ); + items.push(item); } } - items } - }) - .await - } + } + }) + .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 find_source(ctx: ArgumentContext) -> LatexLabelReferenceSource { + match LANGUAGE_DATA + .label_commands + .iter() + .find(|cmd| &cmd.name[1..] == ctx.parameter.name && cmd.index == ctx.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())), - } +fn is_included( + table: &latex::SymbolTable, + label: &latex::Label, + source: LatexLabelReferenceSource, +) -> bool { + let label_range = table[label.parent].range(); + match source { + LatexLabelReferenceSource::Everything => true, + LatexLabelReferenceSource::Math => table + .environments + .iter() + .filter(|env| env.left.is_math(&table)) + .any(|env| env.range(&table).contains_exclusive(label_range.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"]); + use crate::feature::FeatureTester; + use indoc::indoc; + + #[tokio::test] + async fn empty_latex_document() { + let req = FeatureTester::new() + .file("main.tex", "") + .main("main.tex") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_labels(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); + } + + #[tokio::test] + async fn empty_bibtex_document() { + let req = FeatureTester::new() + .file("main.bib", "") + .main("main.bib") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_labels(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); + } + + #[tokio::test] + async fn inside_of_ref() { + let req = FeatureTester::new() + .file( + "foo.tex", + indoc!( + r#" + \addbibresource{bar.bib} + \include{baz} + \ref{} + "# + ), + ) + .file("bar.bib", "") + .file("baz.tex", r#"\label{foo}\label{bar}\ref{baz}"#) + .main("foo.tex") + .position(2, 5) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_labels(&req, &mut actual_items).await; + + let actual_labels: Vec<_> = actual_items + .into_iter() + .map(|item| item.data.label().to_owned()) + .collect(); + assert_eq!(actual_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()); + #[tokio::test] + async fn outside_of_ref() { + let req = FeatureTester::new() + .file( + "foo.tex", + indoc!( + r#" + \include{bar} + \ref{} + "# + ), + ) + .file("bar.tex", r#"\label{foo}\label{bar}"#) + .main("foo.tex") + .position(1, 6) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_labels(&req, &mut actual_items).await; + + assert!(actual_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"]); + #[tokio::test] + async fn eqref() { + let req = FeatureTester::new() + .file( + "main.tex", + indoc!( + r#" + \begin{align}\label{foo}\end{align}\label{bar} + \eqref{} + "# + ), + ) + .main("main.tex") + .position(1, 7) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_labels(&req, &mut actual_items).await; + + let actual_labels: Vec<_> = actual_items + .into_iter() + .map(|item| item.data.label().to_owned()) + .collect(); + + assert_eq!(actual_labels, vec!["foo"]); } } diff --git a/support/texlab/src/completion/latex/mod.rs b/support/texlab/src/completion/latex/mod.rs index 2902678c11..519d00e5f7 100644 --- a/support/texlab/src/completion/latex/mod.rs +++ b/support/texlab/src/completion/latex/mod.rs @@ -1,14 +1,14 @@ pub mod argument; -pub mod begin_command; +pub mod begin_cmd; pub mod citation; pub mod color; pub mod color_model; -pub mod combinators; +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 tikz_lib; pub mod user; diff --git a/support/texlab/src/completion/latex/theorem.rs b/support/texlab/src/completion/latex/theorem.rs index 993ef5c7e8..a8706bed0b 100644 --- a/support/texlab/src/completion/latex/theorem.rs +++ b/support/texlab/src/completion/latex/theorem.rs @@ -1,70 +1,115 @@ 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}; +use crate::{ + completion::types::{Item, ItemData}, + feature::FeatureRequest, + protocol::CompletionParams, +}; -#[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 +pub async fn complete_latex_theorem_environments<'a>( + req: &'a FeatureRequest<CompletionParams>, + items: &mut Vec<Item<'a>>, +) { + combinators::environment(req, |ctx| async move { + for table in req + .related() + .into_iter() + .filter_map(|doc| doc.content.as_latex()) + { + for theorem in &table.theorem_definitions { + let name = theorem.name(&table).text(); + let data = ItemData::UserEnvironment { name }; + let item = Item::new(ctx.range, data); + items.push(item); } - }) - .await - } + } + }) + .await; } #[cfg(test)] mod tests { use super::*; - use crate::range::RangeExt; - use lsp_types::{Position, Range}; - use std::borrow::Cow; + use crate::{ + feature::FeatureTester, + protocol::{Range, RangeExt}, + }; + use indoc::indoc; + + #[tokio::test] + async fn empty_latex_document() { + let req = FeatureTester::new() + .file("main.tex", "") + .main("main.tex") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_theorem_environments(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); + } + + #[tokio::test] + async fn empty_bibtex_document() { + let req = FeatureTester::new() + .file("main.bib", "") + .main("main.bib") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_theorem_environments(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); + } + + #[tokio::test] + async fn inside_begin() { + let req = FeatureTester::new() + .file( + "main.tex", + indoc!( + r#" + \newtheorem{theorem}{Theorem} + \begin{th} + "# + ), + ) + .main("main.tex") + .position(1, 8) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_theorem_environments(&req, &mut actual_items).await; + + assert_eq!(actual_items.len(), 1); + assert_eq!(actual_items[0].data.label(), "theorem"); + assert_eq!(actual_items[0].range, Range::new_simple(1, 7, 1, 9)); + } + + #[tokio::test] + async fn outside_begin() { + let req = FeatureTester::new() + .file( + "main.tex", + indoc!( + r#" + \newtheorem{theorem}{Theorem} + \begin{th} + "# + ), + ) + .main("main.tex") + .position(1, 10) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_theorem_environments(&req, &mut actual_items).await; - #[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)) - ); + assert!(actual_items.is_empty()); } } diff --git a/support/texlab/src/completion/latex/tikz.rs b/support/texlab/src/completion/latex/tikz.rs deleted file mode 100644 index 2d6740e1a8..0000000000 --- a/support/texlab/src/completion/latex/tikz.rs +++ /dev/null @@ -1,90 +0,0 @@ -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/tikz_lib.rs b/support/texlab/src/completion/latex/tikz_lib.rs new file mode 100644 index 0000000000..fb51fd7229 --- /dev/null +++ b/support/texlab/src/completion/latex/tikz_lib.rs @@ -0,0 +1,138 @@ +use super::combinators::{self, Parameter}; +use crate::{ + completion::types::{Item, ItemData}, + feature::FeatureRequest, + protocol::CompletionParams, + syntax::LANGUAGE_DATA, +}; +use std::iter; + +pub async fn complete_latex_pgf_libraries<'a>( + req: &'a FeatureRequest<CompletionParams>, + items: &mut Vec<Item<'a>>, +) { + let param = Parameter { + name: "usepgflibrary", + index: 0, + }; + combinators::argument(req, iter::once(param), |ctx| async move { + for name in &LANGUAGE_DATA.pgf_libraries { + let item = Item::new(ctx.range, ItemData::PgfLibrary { name }); + items.push(item); + } + }) + .await; +} + +pub async fn complete_latex_tikz_libraries<'a>( + req: &'a FeatureRequest<CompletionParams>, + items: &mut Vec<Item<'a>>, +) { + let param = Parameter { + name: "usetikzlibrary", + index: 0, + }; + combinators::argument(req, iter::once(param), |ctx| async move { + for name in &LANGUAGE_DATA.tikz_libraries { + let item = Item::new(ctx.range, ItemData::TikzLibrary { name }); + items.push(item); + } + }) + .await; +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::feature::FeatureTester; + + #[tokio::test] + async fn empty_latex_document_pgf() { + let req = FeatureTester::new() + .file("main.tex", "") + .main("main.tex") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_pgf_libraries(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); + } + + #[tokio::test] + async fn empty_bibtex_document_pgf() { + let req = FeatureTester::new() + .file("main.bib", "") + .main("main.bib") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_pgf_libraries(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); + } + + #[tokio::test] + async fn empty_latex_document_tikz() { + let req = FeatureTester::new() + .file("main.tex", "") + .main("main.tex") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_tikz_libraries(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); + } + + #[tokio::test] + async fn empty_bibtex_document_tikz() { + let req = FeatureTester::new() + .file("main.bib", "") + .main("main.bib") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_tikz_libraries(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); + } + + #[tokio::test] + async fn pgf_library() { + let req = FeatureTester::new() + .file("main.tex", r#"\usepgflibrary{}"#) + .main("main.tex") + .position(0, 15) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_pgf_libraries(&req, &mut actual_items).await; + + assert!(!actual_items.is_empty()); + } + + #[tokio::test] + async fn tikz_library() { + let req = FeatureTester::new() + .file("main.tex", r#"\usetikzlibrary{}"#) + .main("main.tex") + .position(0, 16) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_tikz_libraries(&req, &mut actual_items).await; + + assert!(!actual_items.is_empty()); + } +} diff --git a/support/texlab/src/completion/latex/user.rs b/support/texlab/src/completion/latex/user.rs index 890a75882b..0a5706958a 100644 --- a/support/texlab/src/completion/latex/user.rs +++ b/support/texlab/src/completion/latex/user.rs @@ -1,156 +1,204 @@ 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 - } -} +use crate::{ + completion::{Item, ItemData}, + feature::FeatureRequest, + protocol::{CompletionParams, Range}, + syntax::latex, + workspace::DocumentContent, +}; -#[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 - } +pub async fn complete_latex_user_commands<'a>( + req: &'a FeatureRequest<CompletionParams>, + items: &mut Vec<Item<'a>>, +) { + combinators::command(req, |current_cmd_node| async move { + let current_cmd = req + .current() + .content + .as_latex() + .unwrap() + .as_command(current_cmd_node) + .unwrap(); + + for table in req + .related() + .into_iter() + .flat_map(|doc| doc.content.as_latex()) + { + table + .commands + .iter() + .filter(|cmd_node| **cmd_node != current_cmd_node) + .map(|cmd_node| { + let name = &table.as_command(*cmd_node).unwrap().name.text()[1..]; + Item::new( + current_cmd.short_name_range(), + ItemData::UserCommand { name }, + ) + }) + .for_each(|item| items.push(item)); + } + }) + .await; } -impl LatexUserEnvironmentCompletionProvider { +pub async fn complete_latex_user_environments<'a>( + req: &'a FeatureRequest<CompletionParams>, + items: &mut Vec<Item<'a>>, +) { fn make_item( - request: &FeatureRequest<CompletionParams>, - delimiter: &LatexEnvironmentDelimiter, + table: &latex::SymbolTable, + delim: latex::EnvironmentDelimiter, 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 + ) -> Option<Item> { + delim + .name(&table) + .map(|name| Item::new(name_range, ItemData::UserEnvironment { name: &name.text() })) } + + combinators::environment(req, |ctx| async move { + for doc in req.related() { + if let DocumentContent::Latex(table) = &doc.content { + for env in &table.environments { + if (env.left.parent == ctx.node || env.right.parent == ctx.node) + && doc.uri == req.current().uri + { + continue; + } + + if let Some(item) = make_item(&table, env.left, ctx.range) { + items.push(item); + } + + if let Some(item) = make_item(&table, env.right, ctx.range) { + items.push(item); + } + } + } + } + }) + .await; } #[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"]); + use crate::feature::FeatureTester; + use indoc::indoc; + use itertools::Itertools; + + #[tokio::test] + async fn empty_latex_document_command() { + let req = FeatureTester::new() + .file("main.tex", "") + .main("main.tex") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + complete_latex_user_commands(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); + } + + #[tokio::test] + async fn empty_bibtex_document_command() { + let req = FeatureTester::new() + .file("main.bib", "") + .main("main.bib") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + complete_latex_user_commands(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); } - #[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()) + #[tokio::test] + async fn empty_latex_document_environment() { + let req = FeatureTester::new() + .file("main.tex", "") + .main("main.tex") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + complete_latex_user_environments(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); + } + + #[tokio::test] + async fn empty_bibtex_document_environment() { + let req = FeatureTester::new() + .file("main.bib", "") + .main("main.bib") + .position(0, 0) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + complete_latex_user_environments(&req, &mut actual_items).await; + + assert!(actual_items.is_empty()); + } + + #[tokio::test] + async fn command() { + let req = FeatureTester::new() + .file( + "foo.tex", + indoc!( + r#" + \include{bar} + \foo + "# + ), + ) + .file("bar.tex", r#"\bar"#) + .file("baz.tex", r#"\baz"#) + .main("foo.tex") + .position(1, 2) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_user_commands(&req, &mut actual_items).await; + + let actual_labels: Vec<_> = actual_items + .into_iter() + .map(|item| item.data.label().to_owned()) + .collect(); + assert_eq!(actual_labels, vec!["include", "bar"]); + } + + #[tokio::test] + async fn environment() { + let req = FeatureTester::new() + .file( + "foo.tex", + indoc!( + r#" + \include{bar} + \begin{foo} + "# + ), + ) + .file("bar.tex", r#"\begin{bar}\end{bar}"#) + .file("baz.tex", r#"\begin{baz}\end{baz}"#) + .main("foo.tex") + .position(1, 9) + .test_completion_request() + .await; + let mut actual_items = Vec::new(); + + complete_latex_user_environments(&req, &mut actual_items).await; + + let actual_labels: Vec<_> = actual_items + .into_iter() + .map(|item| item.data.label().to_owned()) .unique() .collect(); - assert_eq!(labels, vec!["bar"]); + assert_eq!(actual_labels, vec!["bar"]); } } |