LumisDocs
Usage

Highlight

Highlight code with one Lumis workflow across CLI, Rust, Elixir, JavaScript / TypeScript, and Java.

This is the core Lumis workflow: choose a language, choose a formatter, then render output.

Simple example

import {highlight} from '@lumis-sh/lumis';
import {htmlInline} from '@lumis-sh/lumis/formatters';
import javascript from '@lumis-sh/wasm-javascript';
import frappe from '@lumis-sh/themes/catppuccin_frappe';

const html = await highlight(
  'const x = 1',
  htmlInline({language: javascript, theme: frappe})
);

Options by runtime

NeedCLIRustElixirJavaScript / TypeScriptJava
Set language--language.language(...)language: "..."language: ...withLang(...)
Set theme--theme.theme(...)theme: "..."theme: ...withTheme(...)
Choose output--formatterbuilder typeformatter: ...formatter factoryruntime-specific
Bound query matches--budget-match-limit.budget(Budget::new().match_limit(...))budget: [match_limit: ...]budget: {matchLimit: ...}—
Bound render time--budget-time-limit.budget(Budget::new().time_limit(...))budget: [time_limit: ...]budget: {timeLimit: ...}—
Color nested brackets--rainbow-brackets.rainbow_brackets(true)rainbow_brackets: truerainbowBrackets: true—
Mark changed words (diff language only)--word-diff.word_diff(true)word_diff: truewordDiff: true—
Compose your own ranges—.annotations(&annotations)annotations: [...]annotations: [...]—

Both limits are one feature, so they sit under one budget option rather than two loose ones. See Budget for what happens when either runs out. A CLI has no nesting, so the group becomes a flag prefix.

budget, rainbow_brackets, word_diff and annotations belong to the highlight rather than to a formatter: they change which events the walk produces, and every formatter renders whatever it is handed. So they sit beside the formatter rather than inside it — HighlightOptions in Rust and JavaScript, the options list of Lumis.highlight/2 in Elixir. See Rainbow brackets, Highlight diffs and Annotations.

Language detection

If you omit the language, Lumis tries to detect it using:

  • file extensions (.rs, .js, .ex, etc.)
  • filenames (Makefile, Dockerfile, Caddyfile)
  • shebangs (#!/usr/bin/env python)
  • Emacs mode lines (-*- mode: ruby -*-)

If detection fails, it falls back to plaintext.

Match limit

Tree-sitter walks its whole pool of in-progress matches before it emits each capture, so a query pattern that stays open across a large subtree costs more the deeper the document nests. Lumis bounds the number of matches kept in progress at 8192, for the highlight and bracket queries alike. HTML and the languages that embed it do not depend on that bound to stay fast, because their layers replay captures from finished matches; the bound is what keeps other languages linear when a query pattern stays open across a large subtree.

Raise it when a document is large enough that tree-sitter drops matches and scopes go missing, and accept the extra time. Tree-sitter accepts 1 to 65536; every runtime rejects anything else.

When matches are dropped, HTML formatters mark the <pre> with data-lumis-budget="matches" so the missing scopes are detectable rather than silent. See Budget.

const html = await highlight(
  source,
  htmlInline({language: html_, theme: frappe}),
  {budget: {matchLimit: 16384}},
);

Incomplete code

Lumis handles incomplete or malformed code without crashing. Tree-sitter produces a partial syntax tree and Lumis highlights what it can. This is useful for streaming scenarios where code arrives in chunks (e.g., LLM output).

Next steps

On this page