diff options
Diffstat (limited to 'support/texlab/src/hover')
-rw-r--r-- | support/texlab/src/hover/bibtex/entry_type.rs | 116 | ||||
-rw-r--r-- | support/texlab/src/hover/bibtex/field.rs | 111 | ||||
-rw-r--r-- | support/texlab/src/hover/bibtex/mod.rs | 3 | ||||
-rw-r--r-- | support/texlab/src/hover/bibtex/string_reference.rs | 165 | ||||
-rw-r--r-- | support/texlab/src/hover/bibtex_entry_type.rs | 111 | ||||
-rw-r--r-- | support/texlab/src/hover/bibtex_field.rs | 108 | ||||
-rw-r--r-- | support/texlab/src/hover/bibtex_string_reference.rs | 106 | ||||
-rw-r--r-- | support/texlab/src/hover/latex/citation.rs | 179 | ||||
-rw-r--r-- | support/texlab/src/hover/latex/component.rs | 95 | ||||
-rw-r--r-- | support/texlab/src/hover/latex/label.rs | 118 | ||||
-rw-r--r-- | support/texlab/src/hover/latex/mod.rs | 6 | ||||
-rw-r--r-- | support/texlab/src/hover/latex/preview.rs | 338 | ||||
-rw-r--r-- | support/texlab/src/hover/latex_citation.rs | 79 | ||||
-rw-r--r-- | support/texlab/src/hover/latex_component.rs | 39 | ||||
-rw-r--r-- | support/texlab/src/hover/latex_include.rs | 104 | ||||
-rw-r--r-- | support/texlab/src/hover/latex_label.rs | 71 | ||||
-rw-r--r-- | support/texlab/src/hover/latex_preview.rs | 357 | ||||
-rw-r--r-- | support/texlab/src/hover/mod.rs | 50 |
18 files changed, 1155 insertions, 1001 deletions
diff --git a/support/texlab/src/hover/bibtex/entry_type.rs b/support/texlab/src/hover/bibtex/entry_type.rs new file mode 100644 index 0000000000..1e009565d7 --- /dev/null +++ b/support/texlab/src/hover/bibtex/entry_type.rs @@ -0,0 +1,116 @@ +use crate::{ + feature::{FeatureProvider, FeatureRequest}, + protocol::{ + Hover, HoverContents, MarkupContent, MarkupKind, RangeExt, TextDocumentPositionParams, + }, + syntax::{SyntaxNode, LANGUAGE_DATA}, +}; +use async_trait::async_trait; + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] +pub struct BibtexEntryTypeHoverProvider; + +#[async_trait] +impl FeatureProvider for BibtexEntryTypeHoverProvider { + type Params = TextDocumentPositionParams; + type Output = Option<Hover>; + + async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { + let tree = req.current().content.as_bibtex()?; + for entry in tree + .children(tree.root) + .filter_map(|node| tree.as_entry(node)) + { + if entry.ty.range().contains(req.params.position) { + let ty = &entry.ty.text()[1..]; + let docs = LANGUAGE_DATA.entry_type_documentation(ty)?; + return Some(Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value: docs.into(), + }), + range: Some(entry.ty.range()), + }); + } + } + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{feature::FeatureTester, protocol::Range}; + + #[tokio::test] + async fn empty_latex_document() { + let actual_hover = FeatureTester::new() + .file("main.tex", "") + .main("main.tex") + .position(0, 0) + .test_position(BibtexEntryTypeHoverProvider) + .await; + + assert_eq!(actual_hover, None); + } + + #[tokio::test] + async fn empty_bibtex_document() { + let actual_hover = FeatureTester::new() + .file("main.bib", "") + .main("main.bib") + .position(0, 0) + .test_position(BibtexEntryTypeHoverProvider) + .await; + + assert_eq!(actual_hover, None); + } + + #[tokio::test] + async fn known_entry_type() { + let actual_hover = FeatureTester::new() + .file("main.bib", "@article{foo,}") + .main("main.bib") + .position(0, 3) + .test_position(BibtexEntryTypeHoverProvider) + .await + .unwrap(); + + let expected_hover = Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value: LANGUAGE_DATA + .entry_type_documentation("article") + .unwrap() + .into(), + }), + range: Some(Range::new_simple(0, 0, 0, 8)), + }; + + assert_eq!(actual_hover, expected_hover); + } + + #[tokio::test] + async fn unknown_entry_type() { + let actual_hover = FeatureTester::new() + .file("main.bib", "@foo{bar,}") + .main("main.bib") + .position(0, 3) + .test_position(BibtexEntryTypeHoverProvider) + .await; + + assert_eq!(actual_hover, None); + } + + #[tokio::test] + async fn entry_key() { + let actual_hover = FeatureTester::new() + .file("main.bib", "@article{foo,}") + .main("main.bib") + .position(0, 11) + .test_position(BibtexEntryTypeHoverProvider) + .await; + + assert_eq!(actual_hover, None); + } +} diff --git a/support/texlab/src/hover/bibtex/field.rs b/support/texlab/src/hover/bibtex/field.rs new file mode 100644 index 0000000000..4b00f3364c --- /dev/null +++ b/support/texlab/src/hover/bibtex/field.rs @@ -0,0 +1,111 @@ +use crate::{ + feature::{FeatureProvider, FeatureRequest}, + protocol::{ + Hover, HoverContents, MarkupContent, MarkupKind, RangeExt, TextDocumentPositionParams, + }, + syntax::{SyntaxNode, LANGUAGE_DATA}, +}; +use async_trait::async_trait; + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] +pub struct BibtexFieldHoverProvider; + +#[async_trait] +impl FeatureProvider for BibtexFieldHoverProvider { + type Params = TextDocumentPositionParams; + type Output = Option<Hover>; + + async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { + let tree = req.current().content.as_bibtex()?; + let name = tree + .find(req.params.position) + .into_iter() + .filter_map(|node| tree.as_field(node)) + .map(|field| &field.name) + .find(|name| name.range().contains(req.params.position))?; + + let docs = LANGUAGE_DATA.field_documentation(name.text())?; + Some(Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value: docs.into(), + }), + range: Some(name.range()), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{feature::FeatureTester, protocol::Range}; + + #[tokio::test] + async fn empty_latex_document() { + let actual_hover = FeatureTester::new() + .file("main.tex", "") + .main("main.tex") + .position(0, 0) + .test_position(BibtexFieldHoverProvider) + .await; + + assert_eq!(actual_hover, None); + } + + #[tokio::test] + async fn empty_bibtex_document() { + let actual_hover = FeatureTester::new() + .file("main.bib", "") + .main("main.bib") + .position(0, 0) + .test_position(BibtexFieldHoverProvider) + .await; + + assert_eq!(actual_hover, None); + } + + #[tokio::test] + async fn known_field() { + let actual_hover = FeatureTester::new() + .file("main.bib", "@article{foo, author = bar}") + .main("main.bib") + .position(0, 15) + .test_position(BibtexFieldHoverProvider) + .await + .unwrap(); + + let expected_hover = Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value: LANGUAGE_DATA.field_documentation("author").unwrap().into(), + }), + range: Some(Range::new_simple(0, 14, 0, 20)), + }; + + assert_eq!(actual_hover, expected_hover); + } + + #[tokio::test] + async fn unknown_field() { + let actual_hover = FeatureTester::new() + .file("main.bib", "@article{foo, bar = baz}") + .main("main.bib") + .position(0, 15) + .test_position(BibtexFieldHoverProvider) + .await; + + assert_eq!(actual_hover, None); + } + + #[tokio::test] + async fn entry_key() { + let actual_hover = FeatureTester::new() + .file("main.bib", "@article{foo, bar = baz}") + .main("main.bib") + .position(0, 11) + .test_position(BibtexFieldHoverProvider) + .await; + + assert_eq!(actual_hover, None); + } +} diff --git a/support/texlab/src/hover/bibtex/mod.rs b/support/texlab/src/hover/bibtex/mod.rs new file mode 100644 index 0000000000..59ff356fa0 --- /dev/null +++ b/support/texlab/src/hover/bibtex/mod.rs @@ -0,0 +1,3 @@ +pub mod entry_type; +pub mod field; +pub mod string_reference; diff --git a/support/texlab/src/hover/bibtex/string_reference.rs b/support/texlab/src/hover/bibtex/string_reference.rs new file mode 100644 index 0000000000..46724a4f60 --- /dev/null +++ b/support/texlab/src/hover/bibtex/string_reference.rs @@ -0,0 +1,165 @@ +use crate::{ + feature::{FeatureProvider, FeatureRequest}, + protocol::{ + BibtexFormattingOptions, Hover, HoverContents, MarkupContent, MarkupKind, Position, + TextDocumentPositionParams, + }, + syntax::{bibtex, SyntaxNode}, +}; +use async_trait::async_trait; +use petgraph::graph::NodeIndex; + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] +pub struct BibtexStringReferenceHoverProvider; + +#[async_trait] +impl FeatureProvider for BibtexStringReferenceHoverProvider { + type Params = TextDocumentPositionParams; + type Output = Option<Hover>; + + async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { + let tree = req.current().content.as_bibtex()?; + let reference = Self::find_reference(tree, req.params.position)?; + for string_node in tree.children(tree.root) { + let hover = Self::find_definition(tree, string_node, reference); + if hover.is_some() { + return hover; + } + } + None + } +} + +impl BibtexStringReferenceHoverProvider { + fn find_reference(tree: &bibtex::Tree, pos: Position) -> Option<&bibtex::Token> { + let mut results = tree.find(pos); + results.reverse(); + match ( + &tree.graph[results[0]], + results.get(1).map(|node| &tree.graph[*node]), + ) { + (bibtex::Node::Word(reference), Some(bibtex::Node::Concat(_))) => { + Some(&reference.token) + } + (bibtex::Node::Word(reference), Some(bibtex::Node::Field(_))) => Some(&reference.token), + _ => None, + } + } + + fn find_definition( + tree: &bibtex::Tree, + string_node: NodeIndex, + reference: &bibtex::Token, + ) -> Option<Hover> { + let string = tree.as_string(string_node)?; + if string.name.as_ref()?.text() != reference.text() { + return None; + } + + let options = BibtexFormattingOptions { + line_length: None, + formatter: None, + }; + let text = bibtex::format( + tree, + tree.children(string_node).next()?, + bibtex::FormattingParams { + tab_size: 4, + insert_spaces: true, + options: &options, + }, + ); + Some(Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::PlainText, + value: text, + }), + range: Some(reference.range()), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + feature::FeatureTester, + protocol::{Range, RangeExt}, + }; + use indoc::indoc; + + #[tokio::test] + async fn empty_latex_document() { + let actual_hover = FeatureTester::new() + .file("main.tex", "") + .main("main.tex") + .position(0, 0) + .test_position(BibtexStringReferenceHoverProvider) + .await; + + assert_eq!(actual_hover, None); + } + + #[tokio::test] + async fn empty_bibtex_document() { + let actual_hover = FeatureTester::new() + .file("main.bib", "") + .main("main.bib") + .position(0, 0) + .test_position(BibtexStringReferenceHoverProvider) + .await; + + assert_eq!(actual_hover, None); + } + + #[tokio::test] + async fn inside_reference() { + let actual_hover = FeatureTester::new() + .file( + "main.bib", + indoc!( + r#" + @string{foo = "Foo"} + @string{bar = "Bar"} + @article{baz, author = bar} + "# + ), + ) + .main("main.bib") + .position(2, 24) + .test_position(BibtexStringReferenceHoverProvider) + .await + .unwrap(); + + let expected_hover = Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::PlainText, + value: "\"Bar\"".into(), + }), + range: Some(Range::new_simple(2, 23, 2, 26)), + }; + + assert_eq!(actual_hover, expected_hover); + } + + #[tokio::test] + async fn outside_reference() { + let actual_hover = FeatureTester::new() + .file( + "main.bib", + indoc!( + r#" + @string{foo = "Foo"} + @string{bar = "Bar"} + @article{baz, author = bar} + "# + ), + ) + .main("main.bib") + .position(2, 20) + .test_position(BibtexStringReferenceHoverProvider) + .await; + + assert_eq!(actual_hover, None); + } +} diff --git a/support/texlab/src/hover/bibtex_entry_type.rs b/support/texlab/src/hover/bibtex_entry_type.rs deleted file mode 100644 index adf833b689..0000000000 --- a/support/texlab/src/hover/bibtex_entry_type.rs +++ /dev/null @@ -1,111 +0,0 @@ -use crate::range::RangeExt; -use crate::syntax::*; -use crate::workspace::*; -use futures_boxed::boxed; -use lsp_types::*; - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct BibtexEntryTypeHoverProvider; - -impl FeatureProvider for BibtexEntryTypeHoverProvider { - type Params = TextDocumentPositionParams; - type Output = Option<Hover>; - - #[boxed] - async fn execute<'a>( - &'a self, - request: &'a FeatureRequest<TextDocumentPositionParams>, - ) -> Option<Hover> { - if let SyntaxTree::Bibtex(tree) = &request.document().tree { - for entry in tree.entries() { - if entry.ty.range().contains(request.params.position) { - let ty = &entry.ty.text()[1..]; - if let Some(documentation) = LANGUAGE_DATA.entry_type_documentation(ty) { - return Some(Hover { - contents: HoverContents::Markup(MarkupContent { - kind: MarkupKind::Markdown, - value: documentation.into(), - }), - range: None, - }); - } - } - } - } - None - } -} - -#[cfg(test)] -mod tests { - use super::*; - use lsp_types::Position; - - #[test] - fn test_known_entry_type() { - let hover = test_feature( - BibtexEntryTypeHoverProvider, - FeatureSpec { - files: vec![FeatureSpec::file("foo.bib", "@article{foo,}")], - main_file: "foo.bib", - position: Position::new(0, 3), - ..FeatureSpec::default() - }, - ); - assert_eq!( - hover, - Some(Hover { - contents: HoverContents::Markup(MarkupContent { - kind: MarkupKind::Markdown, - value: LANGUAGE_DATA - .entry_type_documentation("article") - .unwrap() - .into(), - }), - range: None, - }) - ); - } - - #[test] - fn test_unknown_entry_type() { - let hover = test_feature( - BibtexEntryTypeHoverProvider, - FeatureSpec { - files: vec![FeatureSpec::file("foo.bib", "@foo{bar,}")], - main_file: "foo.bib", - position: Position::new(0, 3), - ..FeatureSpec::default() - }, - ); - assert_eq!(hover, None); - } - - #[test] - fn test_entry_key() { - let hover = test_feature( - BibtexEntryTypeHoverProvider, - FeatureSpec { - files: vec![FeatureSpec::file("foo.bib", "@article{foo,}")], - main_file: "foo.bib", - position: Position::new(0, 11), - ..FeatureSpec::default() - }, - ); - assert_eq!(hover, None); - } - - #[test] - fn test_latex() { - let hover = test_feature( - BibtexEntryTypeHoverProvider, - FeatureSpec { - files: vec![FeatureSpec::file("foo.tex", "\\foo")], - main_file: "foo.tex", - position: Position::new(0, 3), - ..FeatureSpec::default() - }, - ); - assert_eq!(hover, None); - } -} diff --git a/support/texlab/src/hover/bibtex_field.rs b/support/texlab/src/hover/bibtex_field.rs deleted file mode 100644 index cbe03078cf..0000000000 --- a/support/texlab/src/hover/bibtex_field.rs +++ /dev/null @@ -1,108 +0,0 @@ -use crate::range::RangeExt; -use crate::syntax::*; -use crate::workspace::*; -use futures_boxed::boxed; -use lsp_types::*; - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct BibtexFieldHoverProvider; - -impl FeatureProvider for BibtexFieldHoverProvider { - type Params = TextDocumentPositionParams; - type Output = Option<Hover>; - - #[boxed] - async fn execute<'a>( - &'a self, - request: &'a FeatureRequest<TextDocumentPositionParams>, - ) -> Option<Hover> { - if let SyntaxTree::Bibtex(tree) = &request.document().tree { - for node in tree.find(request.params.position) { - if let BibtexNode::Field(field) = node { - if field.name.range().contains(request.params.position) { - let documentation = LANGUAGE_DATA.field_documentation(field.name.text())?; - return Some(Hover { - contents: HoverContents::Markup(MarkupContent { - kind: MarkupKind::Markdown, - value: documentation.into(), - }), - range: Some(field.name.range()), - }); - } - } - } - } - None - } -} - -#[cfg(test)] -mod tests { - use super::*; - use lsp_types::Position; - - #[test] - fn test_known_field() { - let hover = test_feature( - BibtexFieldHoverProvider, - FeatureSpec { - files: vec![FeatureSpec::file("foo.bib", "@article{foo, author = bar}")], - main_file: "foo.bib", - position: Position::new(0, 15), - ..FeatureSpec::default() - }, - ); - assert_eq!( - hover, - Some(Hover { - contents: HoverContents::Markup(MarkupContent { - kind: MarkupKind::Markdown, - value: LANGUAGE_DATA.field_documentation("author").unwrap().into(), - }), - range: Some(Range::new_simple(0, 14, 0, 20)), - }) - ); - } - - #[test] - fn test_unknown_field() { - let hover = test_feature( - BibtexFieldHoverProvider, - FeatureSpec { - files: vec![FeatureSpec::file("foo.bib", "@article{foo, bar = baz}")], - main_file: "foo.bib", - position: Position::new(0, 15), - ..FeatureSpec::default() - }, - ); - assert_eq!(hover, None); - } - - #[test] - fn test_entry_key() { - let hover = test_feature( - BibtexFieldHoverProvider, - FeatureSpec { - files: vec![FeatureSpec::file("foo.bib", "@article{foo, bar = baz}")], - main_file: "foo.bib", - position: Position::new(0, 11), - ..FeatureSpec::default() - }, - ); - assert_eq!(hover, None); - } - - #[test] - fn test_latex() { - let hover = test_feature( - BibtexFieldHoverProvider, - FeatureSpec { - files: vec![FeatureSpec::file("foo.tex", "")], - main_file: "foo.tex", - position: Position::new(0, 0), - ..FeatureSpec::default() - }, - ); - assert_eq!(hover, None); - } -} diff --git a/support/texlab/src/hover/bibtex_string_reference.rs b/support/texlab/src/hover/bibtex_string_reference.rs deleted file mode 100644 index 28df138291..0000000000 --- a/support/texlab/src/hover/bibtex_string_reference.rs +++ /dev/null @@ -1,106 +0,0 @@ -use crate::formatting::bibtex::{self, BibtexFormattingParams}; -use crate::syntax::*; -use crate::workspace::*; -use futures_boxed::boxed; -use lsp_types::*; - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub struct BibtexStringReferenceHoverProvider; - -impl FeatureProvider for BibtexStringReferenceHoverProvider { - type Params = TextDocumentPositionParams; - type Output = Option<Hover>; - - #[boxed] - async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output { - if let SyntaxTree::Bibtex(tree) = &request.document().tree { - let reference = Self::find_reference(tree, request.params.position)?; - for declaration in &tree.root.children { - if let BibtexDeclaration::String(string) = &declaration { - let definition = Self::find_definition(string, reference); - if definition.is_some() { - return definition; - } - } - } - } - None - } -} - -impl BibtexStringReferenceHoverProvider { - fn find_reference(tree: &BibtexSyntaxTree, position: Position) -> Option<&BibtexToken> { - let mut results = tree.find(position); - results.reverse(); - match (&results[0], results.get(1)) { - (BibtexNode::Word(reference), Some(BibtexNode::Concat(_))) => Some(&reference.token), - (BibtexNode::Word(reference), Some(BibtexNode::Field(_))) => Some(&reference.token), - _ => None, - } - } - - fn find_definition(string: &BibtexString, reference: &BibtexToken) -> Option<Hover> { - if string.name.as_ref()?.text() != reference.text() { - return None; - } - - let text = - bibtex::format_content(string.value.as_ref()?, &BibtexFormattingParams::default()); - Some(Hover { - contents: HoverContents::Markup(MarkupContent { - kind: MarkupKind::PlainText, - value: text, - }), - range: Some(reference.range()), - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::range::RangeExt; - - #[test] - fn test_inside_reference() { - let hover = test_feature( - BibtexStringReferenceHoverProvider, - FeatureSpec { - files: vec![FeatureSpec::file( - "foo.bib", - "@string{foo = \"Foo\"}\n@string{bar = \"Bar\"}\n@article{baz, author = bar}", - )], - main_file: "foo.bib", - position: Position::new(2, 24), - ..FeatureSpec::default() - }, - ); - assert_eq!( - hover, - Some(Hover { - contents: HoverContents::Markup(MarkupContent { - kind: MarkupKind::PlainText, - value: "\"Bar\"".into(), - }), - range: Some(Range::new_simple(2, 23, 2, 26)), - }) - ); - } - - #[test] - fn test_outside_reference() { - let hover = test_feature( - BibtexStringReferenceHoverProvider, - FeatureSpec { - files: vec![FeatureSpec::file( - "foo.bib", - "@string{foo = \"Foo\"}\n@string{bar = \"Bar\"}\n@article{baz, author = bar}", - )], - main_file: "foo.bib", - position: Position::new(2, 20), - ..FeatureSpec::default() - }, - ); - assert_eq!(hover, None); - } -} diff --git a/support/texlab/src/hover/latex/citation.rs b/support/texlab/src/hover/latex/citation.rs new file mode 100644 index 0000000000..8b5468e52e --- /dev/null +++ b/support/texlab/src/hover/latex/citation.rs @@ -0,0 +1,179 @@ +use crate::{ + citeproc::render_citation, + feature::{FeatureProvider, FeatureRequest}, + protocol::{Hover, HoverContents, RangeExt, TextDocumentPositionParams}, + syntax::{bibtex, Span, SyntaxNode}, + workspace::DocumentContent, +}; +use async_trait::async_trait; +use log::warn; + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] +pub struct LatexCitationHoverProvider; + +#[async_trait] +impl FeatureProvider for LatexCitationHoverProvider { + type Params = TextDocumentPositionParams; + type Output = Option<Hover>; + + async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { + let (tree, src_key, entry) = Self::get_entry(req)?; + if entry.is_comment() { + None + } else { + let key = entry.key.as_ref()?; + match render_citation(&tree, key.text()) { + Some(markdown) => Some(Hover { + contents: HoverContents::Markup(markdown), + range: Some(src_key.range()), + }), + None => { + warn!("Failed to render entry: {}", key.text()); + None + } + } + } + } +} + +impl LatexCitationHoverProvider { + fn get_entry( + req: &FeatureRequest<TextDocumentPositionParams>, + ) -> Option<(&bibtex::Tree, &Span, &bibtex::Entry)> { + let key = Self::get_key(req)?; + for tree in req + .related() + .iter() + .filter_map(|doc| doc.content.as_bibtex()) + { + for entry in tree + .children(tree.root) + .filter_map(|node| tree.as_entry(node)) + { + if let Some(current_key) = &entry.key { + if current_key.text() == key.text { + return Some((tree, key, entry)); + } + } + } + } + None + } + + fn get_key(req: &FeatureRequest<TextDocumentPositionParams>) -> Option<&Span> { + match &req.current().content { + DocumentContent::Latex(table) => table + .citations + .iter() + .flat_map(|citation| citation.keys(&table)) + .find(|key| key.range().contains(req.params.position)) + .map(|token| &token.span), + DocumentContent::Bibtex(tree) => tree + .children(tree.root) + .filter_map(|node| tree.as_entry(node)) + .filter_map(|entry| entry.key.as_ref()) + .find(|key| key.range().contains(req.params.position)) + .map(|token| &token.span), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + feature::FeatureTester, + protocol::{MarkupContent, MarkupKind, Range, RangeExt}, + }; + use indoc::indoc; + + #[tokio::test] + async fn empty_latex_document() { + let actual_hover = FeatureTester::new() + .file("main.tex", "") + .main("main.tex") + .position(0, 0) + .test_position(LatexCitationHoverProvider) + .await; + + assert_eq!(actual_hover, None); + } + + #[tokio::test] + async fn empty_bibtex_document() { + let actual_hover = FeatureTester::new() + .file("main.bib", "") + .main("main.bib") + .position(0, 0) + .test_position(LatexCitationHoverProvider) + .await; + + assert_eq!(actual_hover, None); + } + + #[tokio::test] + async fn inside_label() { + let actual_hover = FeatureTester::new() + .file( + "main.bib", + "@article{foo, author = {Foo Bar}, title = {Baz Qux}, year = 1337}", + ) + .file( + "main.tex", + indoc!( + r#" + \addbibresource{main.bib} + \cite{foo} + "# + ), + ) + .main("main.tex") + .position(1, 7) + .test_position(LatexCitationHoverProvider) + .await + .unwrap(); + + let expected_hover = Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value: "Bar, F. (1337). *Baz Qux*.".into(), + }), + range: Some(Range::new_simple(1, 6, 1, 9)), + }; + + assert_eq!(actual_hover, expected_hover); + } + + #[tokio::test] + async fn inside_entry() { + let actual_hover = FeatureTester::new() + .file( + "main.bib", + "@article{foo, author = {Foo Bar}, title = {Baz Qux}, year = 1337}", + ) + .file( + "main.tex", + indoc!( + r#" + \addbibresource{main.bib} + \cite{foo} + "# + ), + ) + .main("main.bib") + .position(0, 11) + .test_position(LatexCitationHoverProvider) + .await + .unwrap(); + + let expected_hover = Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value: "Bar, F. (1337). *Baz Qux*.".into(), + }), + range: Some(Range::new_simple(0, 9, 0, 12)), + }; + + assert_eq!(actual_hover, expected_hover); + } +} diff --git a/support/texlab/src/hover/latex/component.rs b/support/texlab/src/hover/latex/component.rs new file mode 100644 index 0000000000..5285ec795a --- /dev/null +++ b/support/texlab/src/hover/latex/component.rs @@ -0,0 +1,95 @@ +use crate::{ + components::COMPONENT_DATABASE, + feature::{FeatureProvider, FeatureRequest}, + protocol::{Hover, HoverContents, RangeExt, TextDocumentPositionParams}, + syntax::{LatexIncludeKind, SyntaxNode}, +}; +use async_trait::async_trait; + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] +pub struct LatexComponentHoverProvider; + +#[async_trait] +impl FeatureProvider for LatexComponentHoverProvider { + type Params = TextDocumentPositionParams; + type Output = Option<Hover>; + + async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { + let table = req.current().content.as_latex()?; + for include in &table.includes { + match include.kind { + LatexIncludeKind::Package | LatexIncludeKind::Class => { + for path in include.paths(&table) { + if path.range().contains(req.params.position) { + let docs = COMPONENT_DATABASE.documentation(path.text())?; + return Some(Hover { + contents: HoverContents::Markup(docs), + range: Some(path.range()), + }); + } + } + } + _ => {} + } + } + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + feature::FeatureTester, + protocol::{Range, RangeExt}, + }; + + #[tokio::test] + async fn empty_latex_document() { + let actual_hover = FeatureTester::new() + .file("main.tex", "") + .main("main.tex") + .position(0, 0) + .test_position(LatexComponentHoverProvider) + .await; + + assert_eq!(actual_hover, None); + } + + #[tokio::test] + async fn empty_bibtex_document() { + let actual_hover = FeatureTester::new() + .file("main.bib", "") + .main("main.bib") + .position(0, 0) + .test_position(LatexComponentHoverProvider) + .await; + + assert_eq!(actual_hover, None); + } + + #[tokio::test] + async fn known_package() { + let actual_hover = FeatureTester::new() + .file("main.tex", r#"\usepackage{amsmath}"#) + .main("main.tex") + .position(0, 15) + .test_position(LatexComponentHoverProvider) + .await + .unwrap(); + + assert_eq!(actual_hover.range.unwrap(), Range::new_simple(0, 12, 0, 19)); + } + + #[tokio::test] + async fn unknown_class() { + let actual_hover = FeatureTester::new() + .file("main.tex", r#"\documentclass{abcdefghijklmnop}"#) + .main("main.tex") + .position(0, 20) + .test_position(LatexComponentHoverProvider) + .await; + + assert_eq!(actual_hover, None); + } +} diff --git a/support/texlab/src/hover/latex/label.rs b/support/texlab/src/hover/latex/label.rs new file mode 100644 index 0000000000..66724d442b --- /dev/null +++ b/support/texlab/src/hover/latex/label.rs @@ -0,0 +1,118 @@ +use crate::{ + feature::{DocumentView, FeatureProvider, FeatureRequest}, + outline::{Outline, OutlineContext}, + protocol::{Hover, HoverContents, Position, RangeExt, TextDocumentPositionParams}, + syntax::{latex, LatexLabelKind, SyntaxNode}, + workspace::{Document, DocumentContent}, +}; +use async_trait::async_trait; +use std::sync::Arc; + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] +pub struct LatexLabelHoverProvider; + +#[async_trait] +impl FeatureProvider for LatexLabelHoverProvider { + type Params = TextDocumentPositionParams; + type Output = Option<Hover>; + + async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { + let table = req.current().content.as_latex()?; + let reference = Self::find_reference(table, req.params.position)?; + let (doc, def) = Self::find_definition(&req.view, reference)?; + + let snapshot = Arc::clone(&req.view.snapshot); + let view = DocumentView::analyze(snapshot, doc, &req.options, &req.current_dir); + let outline = Outline::analyze(&view, &req.options, &req.current_dir); + let outline_ctx = OutlineContext::parse(&view, &outline, def)?; + let markup = outline_ctx.documentation(); + Some(Hover { + contents: HoverContents::Markup(markup), + range: Some(reference.range()), + }) + } +} + +impl LatexLabelHoverProvider { + fn find_reference(table: &latex::SymbolTable, pos: Position) -> Option<&latex::Token> { + for label in &table.labels { + let names = label.names(&table); + if names.len() == 1 && table[label.parent].range().contains(pos) { + return Some(&label.names(&table)[0]); + } + + for name in &names { + if name.range().contains(pos) { + return Some(name); + } + } + } + None + } + + fn find_definition( + view: &DocumentView, + reference: &latex::Token, + ) -> Option<(Arc<Document>, latex::Label)> { + for doc in &view.related { + if let DocumentContent::Latex(table) = &doc.content { + for label in &table.labels { + if label.kind == LatexLabelKind::Definition { + for name in label.names(&table) { + if name.text() == reference.text() { + return Some((Arc::clone(&doc), *label)); + } + } + } + } + } + } + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + feature::FeatureTester, + protocol::{Range, RangeExt}, + }; + + #[tokio::test] + async fn empty_latex_document() { + let actual_hover = FeatureTester::new() + .file("main.tex", "") + .main("main.tex") + .position(0, 0) + .test_position(LatexLabelHoverProvider) + .await; + + assert_eq!(actual_hover, None); + } + + #[tokio::test] + async fn empty_bibtex_document() { + let actual_hover = FeatureTester::new() + .file("main.bib", "") + .main("main.bib") + .position(0, 0) + .test_position(LatexLabelHoverProvider) + .await; + + assert_eq!(actual_hover, None); + } + + #[tokio::test] + async fn section() { + let actual_hover = FeatureTester::new() + .file("main.tex", r#"\section{Foo}\label{sec:foo}"#) + .main("main.tex") + .position(0, 23) + .test_position(LatexLabelHoverProvider) + .await + .unwrap(); + + assert_eq!(actual_hover.range.unwrap(), Range::new_simple(0, 20, 0, 27)); + } +} diff --git a/support/texlab/src/hover/latex/mod.rs b/support/texlab/src/hover/latex/mod.rs new file mode 100644 index 0000000000..c119bfc832 --- /dev/null +++ b/support/texlab/src/hover/latex/mod.rs @@ -0,0 +1,6 @@ +#[cfg(feature = "citation")] +pub mod citation; + +pub mod component; +pub mod label; +pub mod preview; diff --git a/support/texlab/src/hover/latex/preview.rs b/support/texlab/src/hover/latex/preview.rs new file mode 100644 index 0000000000..8d2ca00cb5 --- /dev/null +++ b/support/texlab/src/hover/latex/preview.rs @@ -0,0 +1,338 @@ +use crate::{ + components::COMPONENT_DATABASE, + feature::{FeatureProvider, FeatureRequest}, + protocol::{ + ClientCapabilitiesExt, Hover, HoverContents, MarkupContent, MarkupKind, Range, RangeExt, + TextDocumentPositionParams, + }, + syntax::{latex, CharStream, LatexIncludeKind, SyntaxNode}, + tex::{CompileError, CompileParams, DistributionKind, Format}, + workspace::DocumentContent, +}; +use async_trait::async_trait; +use image::{png::PNGEncoder, ColorType, DynamicImage, GenericImageView, ImageBuffer, RgbaImage}; +use io::Cursor; +use log::warn; +use std::{io, process::Stdio, time::Duration}; +use tempfile::TempDir; +use thiserror::Error; +use tokio::process::Command; + +const PREVIEW_ENVIRONMENTS: &[&str] = &[ + "align", + "alignat", + "aligned", + "alignedat", + "algorithmic", + "array", + "Bmatrix", + "bmatrix", + "cases", + "CD", + "eqnarray", + "equation", + "gather", + "gathered", + "matrix", + "multline", + "pmatrix", + "smallmatrix", + "split", + "subarray", + "Vmatrix", + "vmatrix", +]; + +const IGNORED_PACKAGES: &[&str] = &["biblatex", "pgf", "tikz"]; + +#[derive(Debug, Clone, Copy)] +enum MathElement { + Environment(latex::Environment), + Equation(latex::Equation), + Inline(latex::Inline), +} + +impl MathElement { + fn range(&self, tree: &latex::Tree) -> Range { + match self { + Self::Environment(env) => env.range(tree), + Self::Equation(eq) => eq.range(tree), + Self::Inline(inline) => inline.range(tree), + } + } +} + +#[derive(Debug, Error)] +enum RenderError { + #[error("an I/O error occurred: `{0}`")] + IO(#[from] io::Error), + #[error("a compile error occurred: `{0}`")] + Compile(#[from] CompileError), + #[error("compilation failed")] + DviNotFound, + #[error("dvipng is not installed")] + DviPngNotInstalled, + #[error("calling dvipng failed")] + DviPngFaulty, + #[error("failed to decode image")] + DecodeImage, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] +pub struct LatexPreviewHoverProvider; + +impl LatexPreviewHoverProvider { + fn is_preview_environment( + req: &FeatureRequest<TextDocumentPositionParams>, + table: &latex::SymbolTable, + environment: latex::Environment, + ) -> bool { + let canonical_name = environment + .left + .name(&table) + .map(latex::Token::text) + .unwrap_or_default() + .replace('*', ""); + + PREVIEW_ENVIRONMENTS.contains(&canonical_name.as_ref()) + || Self::theorem_environments(req).contains(&canonical_name.as_ref()) + } + + fn theorem_environments(req: &FeatureRequest<TextDocumentPositionParams>) -> Vec<&str> { + let mut names = Vec::new(); + for doc in req.related() { + if let DocumentContent::Latex(table) = &doc.content { + table + .theorem_definitions + .iter() + .map(|thm| thm.name(&table).text()) + .for_each(|thm| names.push(thm)); + } + } + names + } + + async fn render( + req: &FeatureRequest<TextDocumentPositionParams>, + range: Range, + ) -> Result<Hover, RenderError> { + let code = Self::generate_code(req, range); + let params = CompileParams { + file_name: "preview.tex", + code: &code, + format: Format::Latex, + timeout: Duration::from_secs(10), + }; + let dir = req.distro.compile(params).await?.dir; + if !dir.path().join("preview.dvi").exists() { + return Err(RenderError::DviNotFound); + } + + let img = Self::add_margin(Self::dvipng(&dir).await?); + let base64 = Self::encode_image(img); + let markdown = format!("![preview](data:image/png;base64,{})", base64); + dir.close()?; + Ok(Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value: markdown, + }), + range: Some(range), + }) + } + + fn generate_code(req: &FeatureRequest<TextDocumentPositionParams>, range: Range) -> String { + let mut code = String::new(); + code.push_str("\\documentclass{article}\n"); + code.push_str("\\thispagestyle{empty}\n"); + Self::generate_includes(req, &mut code); + Self::generate_command_definitions(req, &mut code); + Self::generate_math_operators(req, &mut code); + Self::generate_theorem_definitions(req, &mut code); + code.push_str("\\begin{document}\n"); + code.push_str(&CharStream::extract(&req.current().text, range)); + code.push('\n'); + code.push_str("\\end{document}\n"); + code + } + + fn generate_includes(req: &FeatureRequest<TextDocumentPositionParams>, code: &mut String) { + for doc in req.related() { + if let DocumentContent::Latex(table) = &doc.content { + let text = &doc.text; + for include in &table.includes { + if include.kind == LatexIncludeKind::Package { + if include + .paths(&table) + .iter() + .all(|path| IGNORED_PACKAGES.contains(&path.text())) + { + continue; + } + + if include + .paths(&table) + .iter() + .map(|path| format!("{}.sty", path.text())) + .any(|name| !COMPONENT_DATABASE.exists(&name)) + { + continue; + } + + code.push_str(&CharStream::extract(&text, table[include.parent].range())); + code.push('\n'); + } + } + } + } + } + + fn generate_command_definitions( + req: &FeatureRequest<TextDocumentPositionParams>, + code: &mut String, + ) { + for doc in req.related() { + if let DocumentContent::Latex(table) = &doc.content { + table + .command_definitions + .iter() + .filter(|def| !def.definition_name(&table).contains("@")) + .map(|def| CharStream::extract(&doc.text, table[def.parent].range())) + .for_each(|def| { + code.push_str(&def); + code.push('\n'); + }); + } + } + } + + fn generate_math_operators( + req: &FeatureRequest<TextDocumentPositionParams>, + code: &mut String, + ) { + for doc in req.related() { + if let DocumentContent::Latex(table) = &doc.content { + table + .math_operators + .iter() + .filter(|op| !op.definition_name(&table).contains("@")) + .map(|op| CharStream::extract(&doc.text, table[op.parent].range())) + .for_each(|op| { + code.push_str(&op); + code.push('\n'); + }); + } + } + } + + fn generate_theorem_definitions( + req: &FeatureRequest<TextDocumentPositionParams>, + code: &mut String, + ) { + for doc in req.related() { + if let DocumentContent::Latex(table) = &doc.content { + table + .theorem_definitions + .iter() + .map(|thm| CharStream::extract(&doc.text, table[thm.parent].range())) + .for_each(|thm| { + code.push_str(&thm); + code.push('\n'); + }) + } + } + } + + async fn dvipng(dir: &TempDir) -> Result<DynamicImage, RenderError> { + let process = Command::new("dvipng") + .args(&["-D", "175", "-T", "tight", "preview.dvi"]) + .current_dir(dir.path()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .map_err(|_| RenderError::DviPngNotInstalled)?; + + process.await.map_err(|_| RenderError::DviPngFaulty)?; + + let png_file = dir.path().join("preview1.png"); + let png = image::open(png_file).map_err(|_| RenderError::DecodeImage)?; + Ok(png) + } + + fn add_margin(image: DynamicImage) -> RgbaImage { + let margin = 5; + let width = image.width() + 2 * margin; + let height = image.height() + 2 * margin; + let mut result = + ImageBuffer::from_pixel(width, height, image::Rgba([0xFF, 0xFF, 0xFF, 0xFF])); + + for x in 0..image.width() { + for y in 0..image.height() { + let pixel = image.get_pixel(x, y); + result.put_pixel(x + margin, y + margin, pixel); + } + } + result + } + + fn encode_image(image: RgbaImage) -> String { + let mut image_buf = Cursor::new(Vec::new()); + let png_encoder = PNGEncoder::new(&mut image_buf); + let width = image.width(); + let height = image.height(); + png_encoder + .encode(&image.into_raw(), width, height, ColorType::Rgba8) + .unwrap(); + base64::encode(&image_buf.into_inner()) + } +} + +#[async_trait] +impl FeatureProvider for LatexPreviewHoverProvider { + type Params = TextDocumentPositionParams; + type Output = Option<Hover>; + + async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { + if !req.client_capabilities.has_hover_markdown_support() + || req.distro.kind() == DistributionKind::Tectonic + { + return None; + } + + if let DocumentContent::Latex(table) = &req.current().content { + let mut elements = Vec::new(); + table + .inlines + .iter() + .map(|inline| MathElement::Inline(*inline)) + .for_each(|inline| elements.push(inline)); + + table + .equations + .iter() + .map(|eq| MathElement::Equation(*eq)) + .for_each(|eq| elements.push(eq)); + + table + .environments + .iter() + .filter(|env| Self::is_preview_environment(req, table, **env)) + .map(|env| MathElement::Environment(*env)) + .for_each(|env| elements.push(env)); + + let range = elements + .iter() + .map(|elem| elem.range(&table)) + .find(|range| range.contains(req.params.position))?; + + return match Self::render(req, range).await { + Ok(hover) => Some(hover), + Err(why) => { + warn!("Preview failed: {}", why); + None + } + }; + } + None + } +} diff --git a/support/texlab/src/hover/latex_citation.rs b/support/texlab/src/hover/latex_citation.rs deleted file mode 100644 index 339dd1b35f..0000000000 --- a/support/texlab/src/hover/latex_citation.rs +++ /dev/null @@ -1,79 +0,0 @@ -use crate::citeproc::render_citation; -use crate::range::RangeExt; -use crate::syntax::*; -use crate::workspace::*; -use futures_boxed::boxed; -use log::warn; -use lsp_types::*; - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct LatexCitationHoverProvider; - -impl FeatureProvider for LatexCitationHoverProvider { - type Params = TextDocumentPositionParams; - type Output = Option<Hover>; - - #[boxed] - async fn execute<'a>( - &'a self, - request: &'a FeatureRequest<TextDocumentPositionParams>, - ) -> Option<Hover> { - let (tree, entry) = Self::get_entry(request)?; - if entry.is_comment() { - None - } else { - let key = entry.key.as_ref().unwrap().text(); - match render_citation(&tree, key) { - Some(markdown) => Some(Hover { - contents: HoverContents::Markup(markdown), - range: None, - }), - None => { - warn!("Failed to render entry: {}", key); - None - } - } - } - } -} - -impl LatexCitationHoverProvider { - fn get_entry( - request: &FeatureRequest<TextDocumentPositionParams>, - ) -> Option<(&BibtexSyntaxTree, &BibtexEntry)> { - let key = Self::get_key(request)?; - for document in request.related_documents() { - if let SyntaxTree::Bibtex(tree) = &document.tree { - for entry in tree.entries() { - if let Some(current_key) = &entry.key { - if current_key.text() == key { - return Some((tree, entry)); - } - } - } - } - } - None - } - - fn get_key(request: &FeatureRequest<TextDocumentPositionParams>) -> Option<&str> { - match &request.document().tree { - SyntaxTree::Latex(tree) => tree - .citations - .iter() - .flat_map(LatexCitation::keys) - .find(|citation| citation.range().contains(request.params.position)) - .map(LatexToken::text), - SyntaxTree::Bibtex(tree) => { - for entry in tree.entries() { - if let Some(key) = &entry.key { - if key.range().contains(request.params.position) { - return Some(key.text()); - } - } - } - None - } - } - } -} diff --git a/support/texlab/src/hover/latex_component.rs b/support/texlab/src/hover/latex_component.rs deleted file mode 100644 index 4d262a6891..0000000000 --- a/support/texlab/src/hover/latex_component.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::completion::DATABASE; -use crate::range::RangeExt; -use crate::syntax::*; -use crate::workspace::*; -use futures_boxed::boxed; -use lsp_types::{Hover, HoverContents, TextDocumentPositionParams}; - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct LatexComponentHoverProvider; - -impl FeatureProvider for LatexComponentHoverProvider { - type Params = TextDocumentPositionParams; - type Output = Option<Hover>; - - #[boxed] - async fn execute<'a>( - &'a self, - request: &'a FeatureRequest<TextDocumentPositionParams>, - ) -> Option<Hover> { - if let SyntaxTree::Latex(tree) = &request.document().tree { - for include in &tree.includes { - if include.kind == LatexIncludeKind::Package - || include.kind == LatexIncludeKind::Class - { - for path in include.paths() { - if path.range().contains(request.params.position) { - let documentation = DATABASE.documentation(path.text())?; - return Some(Hover { - contents: HoverContents::Markup(documentation), - range: Some(path.range()), - }); - } - } - } - } - } - None - } -} diff --git a/support/texlab/src/hover/latex_include.rs b/support/texlab/src/hover/latex_include.rs deleted file mode 100644 index 9073d99465..0000000000 --- a/support/texlab/src/hover/latex_include.rs +++ /dev/null @@ -1,104 +0,0 @@ -use crate::range::RangeExt; -use crate::syntax::*; -use crate::workspace::*; -use futures_boxed::boxed; -use lsp_types::*; - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub struct LatexIncludeHoverProvider; - -impl FeatureProvider for LatexIncludeHoverProvider { - type Params = TextDocumentPositionParams; - type Output = Option<Hover>; - - #[boxed] - async fn execute<'a>( - &'a self, - request: &'a FeatureRequest<TextDocumentPositionParams>, - ) -> Option<Hover> { - let (range, targets) = Self::find_include(request)?; - for target in targets { - if let Some(document) = request.workspace().find(&target) { - let path = document.uri.to_file_path().ok()?; - return Some(Hover { - range: Some(range), - contents: HoverContents::Markup(MarkupContent { - kind: MarkupKind::PlainText, - value: path.to_string_lossy().into_owned(), - }), - }); - } - } - None - } -} - -impl LatexIncludeHoverProvider { - fn find_include( - request: &FeatureRequest<TextDocumentPositionParams>, - ) -> Option<(Range, &[Uri])> { - if let SyntaxTree::Latex(tree) = &request.document().tree { - for include in &tree.includes { - for (i, path) in include.paths().iter().enumerate() { - let range = path.range(); - if range.contains(request.params.position) { - return Some((range, &include.all_targets[i])); - } - } - } - } - None - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::range::RangeExt; - - #[test] - fn test_multiple_paths() { - let hover = test_feature( - LatexIncludeHoverProvider, - FeatureSpec { - files: vec![ - FeatureSpec::file("foo.tex", "\\include{bar, baz}"), - FeatureSpec::file("bar.tex", ""), - FeatureSpec::file("baz.tex", ""), - ], - main_file: "foo.tex", - position: Position::new(0, 16), - ..FeatureSpec::default() - }, - ); - - assert_eq!( - hover, - Some(Hover { - contents: HoverContents::Markup(MarkupContent { - kind: MarkupKind::PlainText, - value: FeatureSpec::uri("baz.tex") - .to_file_path() - .unwrap() - .to_string_lossy() - .into_owned(), - }), - range: Some(Range::new_simple(0, 14, 0, 17)), - }) - ); - } - - #[test] - fn test_empty() { - let hover = test_feature( - LatexIncludeHoverProvider, - FeatureSpec { - files: vec![FeatureSpec::file("foo.tex", "")], - main_file: "foo.tex", - position: Position::new(0, 0), - ..FeatureSpec::default() - }, - ); - assert_eq!(hover, None); - } -} diff --git a/support/texlab/src/hover/latex_label.rs b/support/texlab/src/hover/latex_label.rs deleted file mode 100644 index 2b3a8f51e7..0000000000 --- a/support/texlab/src/hover/latex_label.rs +++ /dev/null @@ -1,71 +0,0 @@ -use crate::range::RangeExt; -use crate::syntax::*; -use crate::workspace::*; -use futures_boxed::boxed; -use lsp_types::*; -use std::sync::Arc; - -pub struct LatexLabelHoverProvider; - -impl FeatureProvider for LatexLabelHoverProvider { - type Params = TextDocumentPositionParams; - type Output = Option<Hover>; - - #[boxed] - async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output { - if let SyntaxTree::Latex(tree) = &request.document().tree { - let reference = Self::find_reference(tree, request.params.position)?; - let (document, definition) = Self::find_definition(&request.view, reference)?; - - let workspace = Arc::clone(&request.view.workspace); - let view = DocumentView::new(workspace, document); - let outline = Outline::from(&view); - let outline_context = OutlineContext::parse(&view, &definition, &outline)?; - let markup = outline_context.documentation(); - Some(Hover { - contents: HoverContents::Markup(markup), - range: Some(reference.range()), - }) - } else { - None - } - } -} - -impl LatexLabelHoverProvider { - fn find_reference(tree: &LatexSyntaxTree, position: Position) -> Option<&LatexToken> { - for label in &tree.structure.labels { - let names = label.names(); - if names.len() == 1 && label.range().contains(position) { - return Some(&label.names()[0]); - } - - for name in &names { - if name.range().contains(position) { - return Some(name); - } - } - } - None - } - - fn find_definition<'a, 'b>( - view: &'a DocumentView, - reference: &'b LatexToken, - ) -> Option<(Arc<Document>, &'a LatexLabel)> { - for document in &view.related_documents { - if let SyntaxTree::Latex(tree) = &document.tree { - for label in &tree.structure.labels { - if label.kind == LatexLabelKind::Definition { - for name in label.names() { - if name.text() == reference.text() { - return Some((Arc::clone(&document), label)); - } - } - } - } - } - } - None - } -} diff --git a/support/texlab/src/hover/latex_preview.rs b/support/texlab/src/hover/latex_preview.rs deleted file mode 100644 index 7af66f6a52..0000000000 --- a/support/texlab/src/hover/latex_preview.rs +++ /dev/null @@ -1,357 +0,0 @@ -use crate::capabilities::ClientCapabilitiesExt; -use crate::completion::DATABASE; -use crate::range::RangeExt; -use crate::syntax::*; -use crate::workspace::*; -use futures_boxed::boxed; -use image::png::PNGEncoder; -use image::{DynamicImage, GenericImage, GenericImageView}; -use log::*; -use lsp_types::*; -use std::io; -use std::io::Cursor; -use std::process::Stdio; -use std::time::Duration; -use tempfile::TempDir; -use tokio_net::process::Command; - -const PREVIEW_ENVIRONMENTS: &[&str] = &[ - "align", - "alignat", - "aligned", - "alignedat", - "algorithmic", - "array", - "Bmatrix", - "bmatrix", - "cases", - "CD", - "eqnarray", - "equation", - "gather", - "gathered", - "matrix", - "multline", - "pmatrix", - "smallmatrix", - "split", - "subarray", - "Vmatrix", - "vmatrix", -]; - -const IGNORED_PACKAGES: &[&str] = &["biblatex", "pgf", "tikz"]; - -#[derive(Debug, PartialEq, Eq, Clone)] -enum MathElement<'a> { - Environment(&'a LatexEnvironment), - Equation(&'a LatexEquation), - Inline(&'a LatexInline), -} - -impl<'a> SyntaxNode for MathElement<'a> { - fn range(&self) -> Range { - match self { - MathElement::Environment(environment) => environment.range(), - MathElement::Equation(equation) => equation.range(), - MathElement::Inline(inline) => inline.range(), - } - } -} - -#[derive(Debug)] -enum RenderError { - IO(io::Error), - Compile(tex::CompileError), - DviNotFound, - DviPngNotInstalled, - DviPngFaulty, - DecodeImage, -} - -impl From<io::Error> for RenderError { - fn from(error: io::Error) -> Self { - RenderError::IO(error) - } -} - -impl From<tex::CompileError> for RenderError { - fn from(error: tex::CompileError) -> Self { - RenderError::Compile(error) - } -} - -pub struct LatexPreviewHoverProvider; - -impl LatexPreviewHoverProvider { - fn is_math_environment( - request: &FeatureRequest<TextDocumentPositionParams>, - environment: &LatexEnvironment, - ) -> bool { - let canonical_name = environment - .left - .name() - .map(LatexToken::text) - .unwrap_or_default() - .replace('*', ""); - - PREVIEW_ENVIRONMENTS.contains(&canonical_name.as_ref()) - || Self::theorem_environments(request).contains(&canonical_name.as_ref()) - } - - fn theorem_environments(request: &FeatureRequest<TextDocumentPositionParams>) -> Vec<&str> { - let mut names = Vec::new(); - for document in request.related_documents() { - if let SyntaxTree::Latex(tree) = &document.tree { - tree.math - .theorem_definitions - .iter() - .map(|thm| thm.name().text()) - .for_each(|thm| names.push(thm)); - } - } - names - } - - async fn render( - request: &FeatureRequest<TextDocumentPositionParams>, - range: Range, - ) -> Result<Hover, RenderError> { - let code = Self::generate_code(request, range); - let params = tex::CompileParams { - file_name: "preview.tex", - code: &code, - format: tex::Format::Latex, - timeout: Duration::from_secs(10), - }; - let directory = request.distribution.compile(params).await?.directory; - - if !directory.path().join("preview.dvi").exists() { - return Err(RenderError::DviNotFound); - } - - let image = Self::add_margin(Self::dvipng(&directory).await?); - let base64 = Self::encode_image(image); - let markdown = format!("![preview](data:image/png;base64,{})", base64); - directory.close()?; - Ok(Hover { - range: Some(range), - contents: HoverContents::Markup(MarkupContent { - kind: MarkupKind::Markdown, - value: markdown, - }), - }) - } - - fn generate_code(request: &FeatureRequest<TextDocumentPositionParams>, range: Range) -> String { - let mut code = String::new(); - code.push_str("\\documentclass{article}\n"); - code.push_str("\\thispagestyle{empty}\n"); - Self::generate_includes(request, &mut code); - Self::generate_command_definitions(request, &mut code); - Self::generate_math_operators(request, &mut code); - Self::generate_theorem_definitions(request, &mut code); - code.push_str("\\begin{document}\n"); - code.push_str(&CharStream::extract(&request.document().text, range)); - code.push('\n'); - code.push_str("\\end{document}\n"); - code - } - - fn generate_includes(request: &FeatureRequest<TextDocumentPositionParams>, code: &mut String) { - for document in request.related_documents() { - if let SyntaxTree::Latex(tree) = &document.tree { - let text = &request.document().text; - for include in &tree.includes { - if include.kind == LatexIncludeKind::Package { - if include - .paths() - .iter() - .all(|path| IGNORED_PACKAGES.contains(&path.text())) - { - continue; - } - - if include - .paths() - .iter() - .map(|path| format!("{}.sty", path.text())) - .any(|name| !DATABASE.exists(&name)) - { - continue; - } - - code.push_str(&CharStream::extract(&text, include.command.range)); - code.push('\n'); - } - } - } - } - } - - fn generate_command_definitions( - request: &FeatureRequest<TextDocumentPositionParams>, - code: &mut String, - ) { - for document in request.related_documents() { - if let SyntaxTree::Latex(tree) = &document.tree { - tree.command_definitions - .iter() - .map(|def| CharStream::extract(&document.text, def.range())) - .for_each(|def| { - code.push_str(&def); - code.push('\n'); - }); - } - } - } - - fn generate_math_operators( - request: &FeatureRequest<TextDocumentPositionParams>, - code: &mut String, - ) { - for document in request.related_documents() { - if let SyntaxTree::Latex(tree) = &document.tree { - tree.math - .operators - .iter() - .map(|op| CharStream::extract(&document.text, op.range())) - .for_each(|op| { - code.push_str(&op); - code.push('\n'); - }); - } - } - } - - fn generate_theorem_definitions( - request: &FeatureRequest<TextDocumentPositionParams>, - code: &mut String, - ) { - for document in request.related_documents() { - if let SyntaxTree::Latex(tree) = &document.tree { - tree.math - .theorem_definitions - .iter() - .map(|thm| CharStream::extract(&document.text, thm.range())) - .for_each(|thm| { - code.push_str(&thm); - code.push('\n'); - }) - } - } - } - - async fn dvipng(directory: &TempDir) -> Result<DynamicImage, RenderError> { - let process = Command::new("dvipng") - .args(&["-D", "175", "-T", "tight", "preview.dvi"]) - .current_dir(directory.path()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - .map_err(|_| RenderError::DviPngNotInstalled)?; - - process.await.map_err(|_| RenderError::DviPngFaulty)?; - - let png_file = directory.path().join("preview1.png"); - let png = image::open(png_file).map_err(|_| RenderError::DecodeImage)?; - Ok(png) - } - - fn add_margin(image: DynamicImage) -> DynamicImage { - let margin = 5; - let width = image.width() + 2 * margin; - let height = image.height() + 2 * margin; - let mut result = DynamicImage::new_rgb8(width, height); - for x in 0..result.width() { - for y in 0..result.height() { - result.put_pixel(x, y, image::Rgba([0xFF, 0xFF, 0xFF, 0xFF])) - } - } - - for x in 0..image.width() { - for y in 0..image.height() { - let pixel = image.get_pixel(x, y); - result.put_pixel(x + margin, y + margin, pixel); - } - } - result - } - - fn encode_image(image: DynamicImage) -> String { - let mut image_buf = Cursor::new(Vec::new()); - let png_encoder = PNGEncoder::new(&mut image_buf); - png_encoder - .encode( - &image.raw_pixels(), - image.width(), - image.height(), - image.color(), - ) - .unwrap(); - base64::encode(&image_buf.into_inner()) - } -} - -impl FeatureProvider for LatexPreviewHoverProvider { - type Params = TextDocumentPositionParams; - type Output = Option<Hover>; - - #[boxed] - async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output { - if !request.client_capabilities.has_hover_markdown_support() - || !request.distribution.supports_format(tex::Format::Latex) - || request.distribution.output_kind(tex::Format::Latex) != tex::OutputKind::Dvi - { - return None; - } - - if let SyntaxTree::Latex(tree) = &request.document().tree { - let mut elements = Vec::new(); - tree.math - .inlines - .iter() - .map(MathElement::Inline) - .for_each(|inline| elements.push(inline)); - - tree.math - .equations - .iter() - .map(MathElement::Equation) - .for_each(|eq| elements.push(eq)); - - tree.env - .environments - .iter() - .filter(|env| Self::is_math_environment(request, env)) - .map(MathElement::Environment) - .for_each(|env| elements.push(env)); - - let range = elements - .iter() - .find(|elem| elem.range().contains(request.params.position)) - .map(MathElement::range)?; - - return match Self::render(request, range).await { - Ok(hover) => Some(hover), - Err(why) => { - let message = match why { - RenderError::IO(why) => format!("I/O error: {}", why), - RenderError::Compile(why) => match why { - tex::CompileError::NotInstalled => "latex not installed".into(), - tex::CompileError::Timeout => "compilation timed out".into(), - tex::CompileError::IO(_) => "an I/O error occurred".into(), - }, - RenderError::DviNotFound => "compilation failed".to_owned(), - RenderError::DviPngNotInstalled => "dvipng is not installed".to_owned(), - RenderError::DviPngFaulty => "dvipng failed".to_owned(), - RenderError::DecodeImage => "failed to decode image".to_owned(), - }; - warn!("Preview failed: {}", message); - None - } - }; - } - None - } -} diff --git a/support/texlab/src/hover/mod.rs b/support/texlab/src/hover/mod.rs index ddb36a3354..e740657add 100644 --- a/support/texlab/src/hover/mod.rs +++ b/support/texlab/src/hover/mod.rs @@ -1,23 +1,24 @@ -mod bibtex_entry_type; -mod bibtex_field; -mod bibtex_string_reference; -mod latex_citation; -mod latex_component; -mod latex_include; -mod latex_label; -mod latex_preview; +mod bibtex; +mod latex; -use self::bibtex_entry_type::BibtexEntryTypeHoverProvider; -use self::bibtex_field::BibtexFieldHoverProvider; -use self::bibtex_string_reference::BibtexStringReferenceHoverProvider; -use self::latex_citation::LatexCitationHoverProvider; -use self::latex_component::LatexComponentHoverProvider; -use self::latex_include::LatexIncludeHoverProvider; -use self::latex_label::LatexLabelHoverProvider; -use self::latex_preview::LatexPreviewHoverProvider; -use crate::workspace::*; -use futures_boxed::boxed; -use lsp_types::{Hover, TextDocumentPositionParams}; +#[cfg(feature = "citation")] +use self::latex::citation::LatexCitationHoverProvider; + +use self::{ + bibtex::{ + entry_type::BibtexEntryTypeHoverProvider, field::BibtexFieldHoverProvider, + string_reference::BibtexStringReferenceHoverProvider, + }, + latex::{ + component::LatexComponentHoverProvider, label::LatexLabelHoverProvider, + preview::LatexPreviewHoverProvider, + }, +}; +use crate::{ + feature::{ChoiceProvider, FeatureProvider, FeatureRequest}, + protocol::{Hover, TextDocumentPositionParams}, +}; +use async_trait::async_trait; pub struct HoverProvider { provider: ChoiceProvider<TextDocumentPositionParams, Hover>, @@ -30,9 +31,9 @@ impl HoverProvider { Box::new(BibtexEntryTypeHoverProvider), Box::new(BibtexStringReferenceHoverProvider), Box::new(BibtexFieldHoverProvider), + #[cfg(feature = "citation")] Box::new(LatexCitationHoverProvider), Box::new(LatexComponentHoverProvider), - Box::new(LatexIncludeHoverProvider), Box::new(LatexLabelHoverProvider), Box::new(LatexPreviewHoverProvider), ]), @@ -46,15 +47,12 @@ impl Default for HoverProvider { } } +#[async_trait] impl FeatureProvider for HoverProvider { type Params = TextDocumentPositionParams; type Output = Option<Hover>; - #[boxed] - async fn execute<'a>( - &'a self, - request: &'a FeatureRequest<TextDocumentPositionParams>, - ) -> Option<Hover> { - self.provider.execute(request).await + async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { + self.provider.execute(req).await } } |