# Highlight diffs (/recipes/highlight-diffs)

The `diff` language colours added, removed and context lines, and highlights the
code inside each hunk as whatever language the diff header names. Nothing has to
be configured: the `+++` path decides, and Lumis loads that language from its
installed package during the same pass.

### JavaScript / TypeScript

```js
import diff from '@lumis-sh/wasm-diff'
import elixir from '@lumis-sh/wasm-elixir'

const hl = await createHighlighter({
  languages: [diff, elixir],
})
const output = hl.highlight(
  source,
  htmlInline({language: diff, theme: frappe}),
)
```

### Rust

```rust
let html = highlight(source, HtmlInlineBuilder::new()
    .language(Language::Diff)
    .theme(Some(themes::get("catppuccin_frappe").unwrap()))
    .build()
    .unwrap());
```

### Elixir

```elixir
Lumis.highlight!(source,
  formatter:
    {:html_inline,
     language: "diff", theme: "catppuccin_frappe"}
)
```

### CLI

```sh
git diff | lumis highlight -l diff
```


Given this input:

```diff
diff --git a/lib/varsel.ex b/lib/varsel.ex
--- a/lib/varsel.ex
+++ b/lib/varsel.ex
@@ -1,6 +1,6 @@
 defmodule Varsel do
   def run(json) do
-    File.write!(path, Jason.encode!(json))
+    File.write!(path, JSON.encode!(json))
   end
 end
```

`defmodule`, `def` and `end` are Elixir keywords, `File` and `JSON` are modules,
and the changed lines keep their `diff.plus` and `diff.minus` colours around
that.

![A git diff of an Elixir file, rendered with catppuccin_frappe: the unchanged lines
carry Elixir keyword, module and atom colours, and the removed and added lines
keep their red and green backgrounds underneath them.](/img/diff-hunk-injection.png)

## Which language a hunk gets

From the `+++` path, matched against the same file name globs that
`lumis languages list` reports, so `+++ b/lib/varsel.ex` selects Elixir. A path
Lumis has no language for, including `/dev/null`, leaves the hunk uncoloured but
still marks its added and removed lines.

In the browser, load the inner language up front along with `diff`. A browser
cannot download a parser in the middle of highlighting, so a language first
named by a diff header has to already be loaded on the page. Node, the CLI, Elixir and
Rust load it on demand.

## Mark the words that changed

A one-word edit to a long line is easy to miss when the whole line is green.
Turn on word diff and Lumis also marks the words that differ between the removed
lines and the added lines that replace them. It only works when the document is
highlighted with the `diff` language; any other language ignores the option:

### JavaScript / TypeScript

```js
const output = hl.highlight(
  source,
  htmlInline({language: diff, theme: frappe}),
  {wordDiff: true},
)
```

### Rust

```rust
let html = highlight_with_options(
    source,
    HtmlInlineBuilder::new()
        .language(Language::Diff)
        .theme(Some(themes::get("catppuccin_frappe").unwrap()))
        .build()
        .unwrap(),
    HighlightOptions::new().word_diff(true),
);
```

### Elixir

```elixir
Lumis.highlight!(source,
  word_diff: true,
  formatter:
    {:html_inline,
     language: "diff", theme: "catppuccin_frappe"}
)
```

### CLI

```sh
git diff | lumis highlight -l diff --word-diff
```


![A git diff of an Elixir file with word diff on: on the removed lines, Jason and
café have a stronger red background than the rest of the line, and on the added
lines JSON, crème and the appended pipe into Path.join have a stronger green
one. The added Logger.debug line has no counterpart and keeps the plain line
colour.](/img/diff-word-diff.png)

Input is ordinary diff text, the kind `git diff`, `git format-patch`, `diff -u`
and `diff` print.
Git's own `--word-diff` output doesn't work: its `[-…-]{+…+}` markers can't be
told apart from the same characters in code.

Each run of removed lines followed by added lines is compared line by line.
A removed line is paired with the added line it shares the most words with, in
order, and only when each keeps at least a third of its words. A line with no
such partner, like `Logger.debug` above, or one rewritten from scratch, marks
nothing beyond its line colour. A run over 1000 bytes on either side is left
unmarked, and comparing the rest counts against the highlight
[budget](/advanced/budget).

Changed words take the theme scopes `diff.minus.word` and `diff.plus.word`, and
every built-in formatter styles them like any other scope. HTML linked writes the
`l-diff-minus-word` and `l-diff-plus-word` classes, which the bundled theme
stylesheets style. BBCode writes the scope's tag, such as `[diff-minus-word-diff]`
and `[diff-plus-word-diff]` in a `diff` document.

Every bundled theme defines both scopes. Neovim colorschemes have no colour for
changed words, so theme extraction derives one from the theme's `diff.minus` and
`diff.plus`: the same hue, a step lighter on a dark theme and darker on a light
one. A custom theme sets them like any other scope; without them, changed words
fall back to their line's `diff.minus` or `diff.plus` style.