summaryrefslogtreecommitdiff
path: root/support/texlab/src/hover/latex
diff options
context:
space:
mode:
Diffstat (limited to 'support/texlab/src/hover/latex')
-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
5 files changed, 736 insertions, 0 deletions
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
+ }
+}