summaryrefslogtreecommitdiff
path: root/support/texlab/src/hover
diff options
context:
space:
mode:
Diffstat (limited to 'support/texlab/src/hover')
-rw-r--r--support/texlab/src/hover/bibtex/entry_type.rs116
-rw-r--r--support/texlab/src/hover/bibtex/field.rs111
-rw-r--r--support/texlab/src/hover/bibtex/mod.rs3
-rw-r--r--support/texlab/src/hover/bibtex/string_reference.rs165
-rw-r--r--support/texlab/src/hover/bibtex_entry_type.rs111
-rw-r--r--support/texlab/src/hover/bibtex_field.rs108
-rw-r--r--support/texlab/src/hover/bibtex_string_reference.rs106
-rw-r--r--support/texlab/src/hover/latex/citation.rs179
-rw-r--r--support/texlab/src/hover/latex/component.rs95
-rw-r--r--support/texlab/src/hover/latex/label.rs118
-rw-r--r--support/texlab/src/hover/latex/mod.rs6
-rw-r--r--support/texlab/src/hover/latex/preview.rs338
-rw-r--r--support/texlab/src/hover/latex_citation.rs79
-rw-r--r--support/texlab/src/hover/latex_component.rs39
-rw-r--r--support/texlab/src/hover/latex_include.rs104
-rw-r--r--support/texlab/src/hover/latex_label.rs71
-rw-r--r--support/texlab/src/hover/latex_preview.rs357
-rw-r--r--support/texlab/src/hover/mod.rs50
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
}
}