Skip to content

About

Faster, Stronger LaTeX and Typst conceal for neovim with the power of Rust!

Resources

Stars

70 stars

Watchers

2 watching

Forks

Repository files navigation

math-conceal.nvim ☀️

中文版文档:zh-cn

Faster and More Precise LaTeX and typst conceal for Neovim.

Typst Equation Conceal

typst-showcase.mov

Markdown Equation Conceal (Compatible with Stream Output)

stream-showcase.mov

LaTeX Conceal

LaTeX ASCII/Unicode Conceal

Features

  • High performance conceal for LaTeX and typst files.
  • Fine grained conceal patterns:
    • Original neovim conceal patterns: expand all concealed nodes on the line where the cursor is located.
    • Fine grained conceal patterns: only expand the concealed node under the cursor.
  • Image overlay conceal: Using kitty graphic protocol to show the compiled equations instead of source code.
  • Support multiple conceal patterns, including greek letters, script letters, math symbols, font styles, delimiters, and physical units.
  • Multiple highlight groups for different conceal patterns, allowing you to customize the appearance of each pattern (all highlight groups can be found in highlights).

Installation

rocks.nvim

Command style

:Rocks install math-conceal.nvim

Declare style

~/.config/nvim/rocks.toml:

[plugins]
"math-conceal.nvim" = "scm"

Then

:Rocks sync

or:

$ luarocks --lua-version 5.1 --local --tree ~/.local/share/nvim/rocks install math-conceal.nvim
# ~/.local/share/nvim/rocks is the default rocks tree path
# you can change it according to your vim.g.rocks_nvim.rocks_path

Graphical equation conceal also needs the optional Rust service binary:

:Rocks install math-conceal-service

or:

$ luarocks --lua-version 5.1 --local --tree ~/.local/share/nvim/rocks install math-conceal-service

LuaRocks uses a prebuilt service rock when one is available for your platform; otherwise it builds the service from source and requires Rust/Cargo.

lazy.nvim

return {
  "pxwg/math-conceal.nvim",
  event = "VeryLazy",
  main = "math-conceal",
  build = "cargo build --release --manifest-path service/Cargo.toml", -- required for graphical equation conceal
  --- @type LaTeXConcealOptions
  opts = {
    conceal = {
      "greek",
      "script",
      "math",
      "font",
      "delim",
      "phy",
    },
    ft = { "plaintex", "tex", "context", "bibtex", "markdown", "typst" },
    opt = {
      conceallevel = 2,
      concealcursor = "n",
    },
    image = {
      enabled = true, -- set true to enable graphical equation conceal
    },
  },
}

The top-level opt table controls the Neovim window-local conceal options that math-conceal injects only into windows showing attached buffers. The default concealcursor = "n" keeps conceal active in Normal mode while revealing source on the cursor line during Insert mode; set it to "nci" to keep the previous always-concealed cursor-line behavior.

Buffer Attachment API

attach() is the common entry point for ASCII/Unicode and graphical conceal. Ordinary filetype buffers are attached automatically. Integrations for virtual or preview buffers can provide the logical source independently of the buffer's concrete filetype and name:

local conceal = require("math-conceal")
local attachment = conceal.attach(bufnr, {
  source = {
    kind = "markdown", -- "latex", "markdown", or "typst"
    filetype = "markdown",
    path = "/absolute/path/to/note.md",
  },
  surfaces = {
    unicode = true,
    image = true,
  },
  mode = "presentation",
})

attachment:detach()

path supplies the real source provenance for renderer roots, imports, and path filters when the attached buffer is anonymous. image = true still requires top-level image.enabled = true; graphical attachment is available for Typst and Markdown, while LaTeX uses ASCII/Unicode conceal only.

Calls return owner-qualified handles. Multiple integrations may attach the same logical source to one buffer, and detaching one handle leaves the other owners active. Re-attaching with the same explicit owner replaces that owner's request and makes its old handle stale. Use resolve_source(bufnr, source), get_attachment(bufnr), refresh(), or detach(bufnr) for custom integrations and lifecycle inspection.

The source descriptor determines the Tree-sitter root parser explicitly. math-conceal obtains that parser but does not start or stop Tree-sitter highlighting, so preview hosts remain responsible for their own highlighter lifecycle.

Snacks picker previews

The stock Snacks.picker.preview.file previewer is adapted automatically when Snacks is available. Scratch previews use presentation mode, while previews that reuse an already-loaded buffer preserve that buffer's existing mode and attachment owner. Typst, Markdown, and LaTeX previews receive Unicode conceal; Typst and Markdown also request image conceal when it is globally enabled.

require("math-conceal").setup({
  integrations = {
    snacks = {
      enabled = true, -- default
      unicode = true,
      image = true,
      mode = "presentation",
    },
  },
})

Disable the default adapter with integrations.snacks = false. Custom Snacks previewers can reuse the same lifecycle:

local snacks_conceal = require("math-conceal.integrations.snacks")

opts.preview = snacks_conceal.wrap(my_synchronous_previewer, {
  source = function(ctx, bufnr, detected)
    return detected -- customize kind, filetype, or path here
  end,
})

For asynchronous previewers, call snacks_conceal.sync(ctx, opts) after the preview content has been committed and snacks_conceal.detach(ctx) during custom cleanup.

Equation Conceal

math-conceal.nvim can also render equations as terminal graphics using the renderer adapted from pxwg/typst-concealer, which is a fork of PartyWumpus/typst-concealer. This path uses kitty graphics protocol and works in terminals that support it, such as kitty and Ghostty.

Graphical equation conceal supports Typst and Markdown math through MiTeX. Markdown math supports $...$ and $$...$$ through Tree-sitter by default. Set image.renderers.markdown.backslash_math = true to also recognize \(...\) and \[...\]. This optional handwritten scanner scans the full buffer and is disabled by default to avoid its additional scanning cost. The graphical path requires Neovim 0.11 or newer; ASCII/Unicode conceal does not.

Enable it from the same setup table:

require("math-conceal").setup({
  image = {
    enabled = true,
    renderers = {
      typst = {
        filetypes = { "typst" },
      },
      markdown = {
        filetypes = { "markdown" },
        mitex_package = "@preview/mitex:0.2.7",
      },
    },
  },
})

Display math is centered by default. Set image.block_align = "source" to align Typst and Markdown display math with its source column instead. Inline math and Typst code placement are unchanged.

For rocks.nvim installs, install the optional service rock:

:Rocks install math-conceal-service

For source/lazy.nvim installs, build the bundled Rust service after installing or updating:

cargo build --release --manifest-path service/Cargo.toml

Check Neovim APIs, terminal support, adapters, and the render service with:

:checkhealth math-conceal

Renderer-specific options live under image.renderers.<name>, including filetypes, service_binary, live_debounce, root, inputs, preamble_file, header, render_paths, Typst's code_render.allow and code_render.exclude, and Markdown's mitex_package, mitex_preamble, and mitex_preamble_file.

Markdown MiTeX rendering can prepend reusable LaTeX macro definitions to every inline and block formula. File definitions are loaded before inline definitions:

require("math-conceal").setup({
  image = {
    enabled = true,
    renderers = {
      markdown = {
        mitex_preamble = [[
          \newcommand{\RR}{\mathbb{R}}
        ]],
        mitex_preamble_file = function(ctx)
          return vim.fs.dirname(ctx.path) .. "/mitex-preamble.tex"
        end,
      },
    },
  },
})

These options are MiTeX macro preludes, not full LaTeX document preambles: MiTeX-supported definitions such as \newcommand and \newenvironment work, but they do not load LaTeX packages with \usepackage. For example, a MiTeX-compatible \slashed approximation is:

\newcommand{\slashed}[1]{\not{#1}}

After changing an external preamble file, call require("math-conceal.image").rerender_buf() to refresh the current Markdown buffer.

Typst code rendering is intentionally allowlisted. math-conceal renders a built-in set of predictable Typst primitives by default. Add project-wide custom function names with code_render.allow, or remove names from the effective allowlist with code_render.exclude. Exclusions take precedence over both built-in and explicitly allowed names, and match the tracked code expression's head rather than recursively inspecting nested calls:

require("math-conceal").setup({
  image = {
    enabled = true,
    renderers = {
      typst = {
        code_render = {
          allow = { "theorem", "lemma", "remark" },
          exclude = { "image" },
        },
      },
    },
  },
})

To-do

  • Better support for typst files, and customizable conceal patterns.
    • LaTeX customizable conceal patterns.
    • Typst customizable conceal patterns.
  • Commutative diagram conceal for Typst files.
  • Table conceal for LaTeX and Typst files (inspired from render-markdown.nvim).
  • Automatically maintain the conceal query file and math_symbols.json file.
    • Typst
    • LaTeX

References

Acknowledgements

  • Freed-Wu: Instrumental in publishing this plugin to LuaRocks and refactoring the code structure to fit the best practices for Neovim plugins.
  • Dirichy: for helpful discussions about LaTeX conceal patterns and optimizations.
  • PartyWumpus: for the original typst-concealer plugin, which inspired Typst graphical conceal support.
  • latex.nvim for the idea of using customized conceal patterns.
  • latex_concealer.nvim for the idea of fine grained conceal patterns.
  • pxwg/typst-concealer as the fork whose renderer source was adapted into this plugin.

About

Faster, Stronger LaTeX and Typst conceal for neovim with the power of Rust!

Resources

Stars

70 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages