From b8d4bb76703bcb15578e2b23c5d256532180b894 Mon Sep 17 00:00:00 2001 From: Norbert Preining Date: Tue, 3 Dec 2019 03:01:24 +0000 Subject: CTAN sync 201912030301 --- support/texlab/src/hover/bibtex_entry_type.rs | 111 +++++++ support/texlab/src/hover/bibtex_field.rs | 108 +++++++ .../texlab/src/hover/bibtex_string_reference.rs | 106 ++++++ support/texlab/src/hover/latex_citation.rs | 79 +++++ support/texlab/src/hover/latex_component.rs | 39 +++ support/texlab/src/hover/latex_include.rs | 104 ++++++ support/texlab/src/hover/latex_label.rs | 71 ++++ support/texlab/src/hover/latex_preview.rs | 357 +++++++++++++++++++++ support/texlab/src/hover/mod.rs | 60 ++++ 9 files changed, 1035 insertions(+) create mode 100644 support/texlab/src/hover/bibtex_entry_type.rs create mode 100644 support/texlab/src/hover/bibtex_field.rs create mode 100644 support/texlab/src/hover/bibtex_string_reference.rs create mode 100644 support/texlab/src/hover/latex_citation.rs create mode 100644 support/texlab/src/hover/latex_component.rs create mode 100644 support/texlab/src/hover/latex_include.rs create mode 100644 support/texlab/src/hover/latex_label.rs create mode 100644 support/texlab/src/hover/latex_preview.rs create mode 100644 support/texlab/src/hover/mod.rs (limited to 'support/texlab/src/hover') 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; + + #[boxed] + async fn execute<'a>( + &'a self, + request: &'a FeatureRequest, + ) -> Option { + 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; + + #[boxed] + async fn execute<'a>( + &'a self, + request: &'a FeatureRequest, + ) -> Option { + 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; + + #[boxed] + async fn execute<'a>(&'a self, request: &'a FeatureRequest) -> 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 { + 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; + + #[boxed] + async fn execute<'a>( + &'a self, + request: &'a FeatureRequest, + ) -> Option { + 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, + ) -> 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) -> 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; + + #[boxed] + async fn execute<'a>( + &'a self, + request: &'a FeatureRequest, + ) -> Option { + 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; + + #[boxed] + async fn execute<'a>( + &'a self, + request: &'a FeatureRequest, + ) -> Option { + 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, + ) -> 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; + + #[boxed] + async fn execute<'a>(&'a self, request: &'a FeatureRequest) -> 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, &'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 for RenderError { + fn from(error: io::Error) -> Self { + RenderError::IO(error) + } +} + +impl From for RenderError { + fn from(error: tex::CompileError) -> Self { + RenderError::Compile(error) + } +} + +pub struct LatexPreviewHoverProvider; + +impl LatexPreviewHoverProvider { + fn is_math_environment( + request: &FeatureRequest, + 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) -> 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, + range: Range, + ) -> Result { + 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, 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, 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, + 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, + 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, + 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 { + 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; + + #[boxed] + async fn execute<'a>(&'a self, request: &'a FeatureRequest) -> 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, +} + +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; + + #[boxed] + async fn execute<'a>( + &'a self, + request: &'a FeatureRequest, + ) -> Option { + self.provider.execute(request).await + } +} -- cgit v1.2.3