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.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.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.rs60
9 files changed, 1035 insertions, 0 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..adf833b689
--- /dev/null
+++ b/support/texlab/src/hover/bibtex_entry_type.rs
@@ -0,0 +1,111 @@
+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
new file mode 100644
index 0000000000..cbe03078cf
--- /dev/null
+++ b/support/texlab/src/hover/bibtex_field.rs
@@ -0,0 +1,108 @@
+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
new file mode 100644
index 0000000000..28df138291
--- /dev/null
+++ b/support/texlab/src/hover/bibtex_string_reference.rs
@@ -0,0 +1,106 @@
+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..339dd1b35f
--- /dev/null
+++ b/support/texlab/src/hover/latex_citation.rs
@@ -0,0 +1,79 @@
+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
new file mode 100644
index 0000000000..4d262a6891
--- /dev/null
+++ b/support/texlab/src/hover/latex_component.rs
@@ -0,0 +1,39 @@
+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
new file mode 100644
index 0000000000..9073d99465
--- /dev/null
+++ b/support/texlab/src/hover/latex_include.rs
@@ -0,0 +1,104 @@
+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
new file mode 100644
index 0000000000..2b3a8f51e7
--- /dev/null
+++ b/support/texlab/src/hover/latex_label.rs
@@ -0,0 +1,71 @@
+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
new file mode 100644
index 0000000000..7af66f6a52
--- /dev/null
+++ b/support/texlab/src/hover/latex_preview.rs
@@ -0,0 +1,357 @@
+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
new file mode 100644
index 0000000000..ddb36a3354
--- /dev/null
+++ b/support/texlab/src/hover/mod.rs
@@ -0,0 +1,60 @@
+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;
+
+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};
+
+pub struct HoverProvider {
+ provider: ChoiceProvider<TextDocumentPositionParams, Hover>,
+}
+
+impl HoverProvider {
+ pub fn new() -> Self {
+ Self {
+ provider: ChoiceProvider::new(vec![
+ Box::new(BibtexEntryTypeHoverProvider),
+ Box::new(BibtexStringReferenceHoverProvider),
+ Box::new(BibtexFieldHoverProvider),
+ Box::new(LatexCitationHoverProvider),
+ Box::new(LatexComponentHoverProvider),
+ Box::new(LatexIncludeHoverProvider),
+ Box::new(LatexLabelHoverProvider),
+ Box::new(LatexPreviewHoverProvider),
+ ]),
+ }
+ }
+}
+
+impl Default for HoverProvider {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+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
+ }
+}