diff options
Diffstat (limited to 'support/texlab/src/hover/latex_preview.rs')
-rw-r--r-- | support/texlab/src/hover/latex_preview.rs | 357 |
1 files changed, 357 insertions, 0 deletions
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 + } +} |