# `Minga.Core.Decorations`
[🔗](https://github.com/jsmestad/minga/blob/main/lib/minga/core/decorations.ex#L1)

Buffer decoration storage and API.

Stores highlight ranges and virtual text (and later, fold regions and block
decorations) for a single buffer. Decorations are visual overlays that do
not modify the buffer's text content. They are stored per-buffer and
consumed by the render pipeline during the content rendering stage.

## Highlight ranges

Highlight ranges apply custom styling (fg, bg, bold, italic, underline,
strikethrough) to arbitrary spans of buffer text. They compose with
tree-sitter syntax highlighting: a highlight range that sets `bg` but
not `fg` preserves the syntax foreground color.

Multiple highlight ranges can overlap on the same character. When they
do, higher-priority ranges override lower-priority ranges per-property.

## Anchor adjustment

Decorations are anchor-based: their positions shift when the buffer is
edited. Insertions before a range shift it right. Insertions within a
range expand it. Deletions within a range shrink it. Deleting all text
in a range removes it.

## Performance

Ranges are backed by an interval tree (`Minga.Core.IntervalTree`)
providing O(log n + k) range queries. This handles 10,000+ decorations
per buffer (LSP diagnostics scale) without measurable frame-time impact.

## Batch updates

The `batch/2` function defers tree rebuilding until the batch is
committed, preventing frame stutter when replacing many decorations
at once (e.g., agent chat sync or LSP diagnostic refresh).

# `color`

```elixir
@type color() :: non_neg_integer()
```

A color value: 24-bit RGB integer.

# `highlight_range`

```elixir
@type highlight_range() :: Minga.Core.Decorations.HighlightRange.t()
```

A highlight range decoration.

- `id`: unique reference for removal
- `start`: inclusive start position `{line, col}`
- `end_`: exclusive end position `{line, col}`
- `style`: keyword list of style overrides (fg, bg, bold, italic, underline, strikethrough)
- `priority`: higher values win per-property on overlap (default 0)
- `group`: optional atom for bulk removal by group (e.g., `:search`, `:diagnostics`, `:agent`)

# `highlight_range_pos`

```elixir
@type highlight_range_pos() :: Minga.Core.IntervalTree.position()
```

A position used in highlight range start/end.

# `overlay`

```elixir
@type overlay() ::
  {start_col :: non_neg_integer(), end_col :: non_neg_integer() | :infinity,
   style :: Minga.Core.Face.t(), priority :: integer()}
```

A column-indexed style overlay: applies from start_col (inclusive) to end_col (exclusive).

# `style`

```elixir
@type style() :: Minga.Core.Face.t()
```

Style for a highlight range: a Face struct where nil fields
inherit from the underlying syntax style.

# `t`

```elixir
@type t() :: %Minga.Core.Decorations{
  ann_line_cache:
    %{
      required(non_neg_integer()) =&gt; [Minga.Core.Decorations.LineAnnotation.t()]
    }
    | nil,
  annotations: [Minga.Core.Decorations.LineAnnotation.t()],
  block_decorations: [Minga.Core.Decorations.BlockDecoration.t()],
  conceal_ranges: [Minga.Core.Decorations.ConcealRange.t()],
  fold_regions: [Minga.Core.Decorations.FoldRegion.t()],
  highlights: Minga.Core.IntervalTree.t(),
  pending:
    [add: highlight_range(), remove: reference(), remove_group: term()] | nil,
  version: non_neg_integer(),
  virtual_texts: [Minga.Core.Decorations.VirtualText.t()],
  vt_line_cache:
    %{required(non_neg_integer()) =&gt; [Minga.Core.Decorations.VirtualText.t()]}
    | nil
}
```

The decorations state for a buffer.

- `highlights`: interval tree of highlight ranges
- `virtual_texts`: list of virtual text decorations (queried by line, not range)
- `annotations`: list of line annotations (pill badges, inline text, gutter icons)
- `fold_regions`: list of buffer-level fold regions (per-buffer, not per-window)
- `block_decorations`: list of block decorations (custom-rendered lines between buffer lines)
- `conceal_ranges`: list of conceal ranges (hidden buffer text with optional replacement)
- `pending`: list of pending operations during a batch (nil when not batching)
- `version`: monotonically increasing version for change detection by the render pipeline

# `add_annotation`

```elixir
@spec add_annotation(t(), non_neg_integer(), String.t(), keyword()) ::
  {reference(), t()}
```

Adds a line annotation to the buffer. Returns `{id, updated_decorations}`.

## Options

- `:kind` (optional, default `:inline_pill`) - `:inline_pill`, `:inline_text`, or `:gutter_icon`
- `:fg` (optional, default `0xFFFFFF`) - foreground color (24-bit RGB)
- `:bg` (optional, default `0x6366F1`) - background color (24-bit RGB)
- `:group` (optional) - atom for bulk removal (e.g., `:org_tags`, `:agent`)
- `:priority` (optional, default 0) - ordering when multiple annotations share a line

## Examples

    {id, decs} = Decorations.add_annotation(decs, 5, "work",
      kind: :inline_pill, fg: 0xFFFFFF, bg: 0x6366F1, group: :org_tags)

    {id, decs} = Decorations.add_annotation(decs, 10, "J. Smith, 2d ago",
      kind: :inline_text, fg: 0x888888, group: :git_blame)

# `add_block_decoration`

```elixir
@spec add_block_decoration(t(), non_neg_integer(), keyword()) :: {reference(), t()}
```

Adds a block decoration to the buffer. Returns `{id, updated_decorations}`.

## Options

- `:placement` (required) - `:above` or `:below` the anchor line
- `:render` (required) - callback `(width -> [{text, style}] | [[{text, style}]])`
- `:height` (optional, default 1) - number of display lines, or `:dynamic`
- `:on_click` (optional) - callback `(row, col) -> :ok` for interactive blocks
- `:priority` (optional, default 0) - ordering when multiple blocks share an anchor

# `add_conceal`

```elixir
@spec add_conceal(
  t(),
  Minga.Core.IntervalTree.position(),
  Minga.Core.IntervalTree.position(),
  keyword()
) :: {reference(), t()}
```

Adds a conceal range. Returns `{id, updated_decorations}`.

Concealed text is hidden from the display without modifying the buffer.
When a replacement string is provided, the entire concealed range is
shown as that single replacement character.

## Options

- `:replacement` (optional) - string to show in place of concealed text (nil = invisible)
- `:replacement_style` (optional) - Face.t() struct for the replacement character
- `:priority` (optional, default 0) - higher values take precedence on overlap
- `:group` (optional) - atom for bulk removal (e.g., `:markdown`, `:agent`)

## Examples

    {id, decs} = Decorations.add_conceal(decs, {0, 0}, {0, 2})
    {id, decs} = Decorations.add_conceal(decs, {0, 0}, {0, 2},
      replacement: "·",
      group: :markdown
    )

# `add_fold_region`

```elixir
@spec add_fold_region(t(), non_neg_integer(), non_neg_integer(), keyword()) ::
  {reference(), t()}
```

Adds a buffer-level fold region. Returns `{id, updated_decorations}`.

## Options

- `:closed` (optional, default true) - initial fold state
- `:placeholder` (optional) - render callback `(start_line, end_line, width) -> [{text, style}]`

## Examples

    {id, decs} = Decorations.add_fold_region(decs, 10, 25,
      closed: true,
      placeholder: fn s, e, _w -> [{"💭 Thinking (#{e - s} lines)...", [fg: 0x555555]}] end
    )

# `add_highlight`

```elixir
@spec add_highlight(
  t(),
  Minga.Core.IntervalTree.position(),
  Minga.Core.IntervalTree.position(),
  keyword()
) :: {reference(), t()}
```

Adds a highlight range. Returns `{id, updated_decorations}`.

## Options

- `:style` (required) - a `Face.t()` struct with the properties to apply (e.g., `Face.new(bg: 0x3E4452, bold: true)`)
- `:priority` (optional, default 0) - higher values win per-property on overlap
- `:group` (optional) - atom for bulk removal (e.g., `:search`, `:diagnostics`)

## Examples

    {id, decs} = Decorations.add_highlight(decs, {0, 0}, {0, 10}, style: Face.new(bg: 0x3E4452))
    {id, decs} = Decorations.add_highlight(decs, {5, 0}, {10, 0},
      style: Face.new(underline: true, fg: 0xFF6C6B),
      priority: 10,
      group: :diagnostics
    )

# `add_virtual_text`

```elixir
@spec add_virtual_text(t(), Minga.Core.IntervalTree.position(), keyword()) ::
  {reference(), t()}
```

Adds a virtual text decoration to the buffer. Returns `{id, updated_decorations}`.

## Options

- `:segments` (required) - list of `{text, Face.t()}` tuples
- `:placement` (required) - `:inline`, `:eol`, `:above`, or `:below`
- `:priority` (optional, default 0) - determines ordering when multiple
  virtual texts share the same anchor

## Examples

    {id, decs} = Decorations.add_virtual_text(decs, {5, 10},
      segments: [{"← error here", Face.new(fg: 0xFF6C6B, italic: true)}],
      placement: :eol
    )

    {id, decs} = Decorations.add_virtual_text(decs, {0, 0},
      segments: [{"▎ Agent", Face.new(fg: 0x51AFEF, bold: true)}],
      placement: :above
    )

# `adjust_for_edit`

```elixir
@spec adjust_for_edit(
  t(),
  Minga.Core.IntervalTree.position(),
  Minga.Core.IntervalTree.position(),
  Minga.Core.IntervalTree.position()
) :: t()
```

Adjusts all decoration anchors after a buffer edit.

Handles the three cases:
1. **Insert before range**: shift range right
2. **Insert within range**: expand range
3. **Delete within range**: shrink range (remove if fully deleted)
4. **Delete spanning range**: remove range

`edit_start` and `edit_end` are the pre-edit positions of the changed region.
`new_end` is the post-edit position where the change ends (for insertions,
this is after the inserted text; for deletions, this equals `edit_start`).

This is called by `Buffer.Server` after each edit, passing the positions
from the `EditDelta`.

# `annotations_for_line`

```elixir
@spec annotations_for_line(t(), non_neg_integer()) :: [
  Minga.Core.Decorations.LineAnnotation.t()
]
```

Returns all annotations for a specific line, sorted by priority.

# `batch`

```elixir
@spec batch(t(), (t() -&gt; t())) :: t()
```

Executes a batch of operations, deferring tree rebuilding until the end.

The function receives the decorations struct and should call `add_highlight`,
`remove_highlight`, and `remove_group` as needed. All operations are
collected and applied at once, with a single tree rebuild.

## Example

    decs = Decorations.batch(decs, fn decs ->
      decs = Decorations.remove_group(decs, :search)
      {_id1, decs} = Decorations.add_highlight(decs, {0, 0}, {0, 5}, style: Face.new(bg: 0xECBE7B), group: :search)
      {_id2, decs} = Decorations.add_highlight(decs, {3, 0}, {3, 5}, style: Face.new(bg: 0xECBE7B), group: :search)
      decs
    end)

# `block_decoration_by_id`

```elixir
@spec block_decoration_by_id(t(), reference()) ::
  Minga.Core.Decorations.BlockDecoration.t() | nil
```

Returns the block decoration with the given ID, or nil.

# `blocks_for_line`

```elixir
@spec blocks_for_line(t(), non_neg_integer()) ::
  {above :: [Minga.Core.Decorations.BlockDecoration.t()],
   below :: [Minga.Core.Decorations.BlockDecoration.t()]}
```

Returns block decorations for a specific anchor line, sorted by priority.
Returns `{above, below}` tuple.

# `buf_col_to_display_col`

```elixir
@spec buf_col_to_display_col(t(), non_neg_integer(), non_neg_integer()) ::
  non_neg_integer()
```

Converts a buffer column to a display column on the given line,
accounting for inline virtual text that shifts content rightward
and conceal ranges that reduce display width.

Virtual texts anchored at or before `buf_col` add their display width
to the result. Conceal ranges before `buf_col` subtract their concealed
width and add replacement width (0 or 1). Virtual texts after `buf_col`
don't affect it.

# `build_ann_line_cache`

```elixir
@spec build_ann_line_cache(t()) :: t()
```

Builds the annotation line cache for O(1) per-line lookups.

Call once before a render pass. The cache is invalidated on any
annotation mutation.

# `build_vt_line_cache`

```elixir
@spec build_vt_line_cache(t()) :: t()
```

Builds the virtual text line cache.

Call this once before a render pass to get O(1) per-line lookups.
Returns an updated Decorations struct with the cache populated.
The cache is invalidated on any mutation (add, remove, adjust).

# `clear`

```elixir
@spec clear(t()) :: t()
```

Removes all decorations. Returns a fresh empty store with bumped version.

# `closed_fold_regions`

```elixir
@spec closed_fold_regions(t()) :: [Minga.Core.Decorations.FoldRegion.t()]
```

Returns all closed fold regions, sorted by start_line.

# `conceals_for_line`

```elixir
@spec conceals_for_line(t(), non_neg_integer()) :: [
  Minga.Core.Decorations.ConcealRange.t()
]
```

Returns conceal ranges that intersect the given line, sorted by start column.

Used by the rendering pipeline to know which graphemes to skip during
the line rendering walk.

# `display_col_to_buf_col`

```elixir
@spec display_col_to_buf_col(t(), non_neg_integer(), non_neg_integer()) ::
  non_neg_integer()
```

Converts a display column to a buffer column on the given line,
accounting for inline virtual text.

This is the inverse of `buf_col_to_display_col/3`. Used by mouse click
position mapping to find the correct buffer column when clicking on a
display column that may be offset by virtual text.

# `empty?`

```elixir
@spec empty?(t()) :: boolean()
```

Returns true if there are no decorations of any kind.

# `eol_virtual_texts_for_line`

```elixir
@spec eol_virtual_texts_for_line(t(), non_neg_integer()) :: [
  Minga.Core.Decorations.VirtualText.t()
]
```

Returns EOL virtual texts for a specific line, sorted by priority.

# `fold_region_at`

```elixir
@spec fold_region_at(t(), non_neg_integer()) ::
  Minga.Core.Decorations.FoldRegion.t() | nil
```

Returns the fold region containing the given line, or nil.

# `has_annotations?`

```elixir
@spec has_annotations?(t()) :: boolean()
```

Returns true if there are any annotations.

# `has_block_decorations?`

```elixir
@spec has_block_decorations?(t()) :: boolean()
```

Returns true if there are any block decorations.

# `has_conceal_ranges?`

```elixir
@spec has_conceal_ranges?(t()) :: boolean()
```

Returns true if there are any conceal ranges.

# `has_fold_regions?`

```elixir
@spec has_fold_regions?(t()) :: boolean()
```

Returns true if there are any fold regions.

# `has_virtual_texts?`

```elixir
@spec has_virtual_texts?(t()) :: boolean()
```

Returns true if there are any virtual texts of any placement.

# `highlight_count`

```elixir
@spec highlight_count(t()) :: non_neg_integer()
```

Returns the number of highlight ranges.

# `highlights_for_line`

```elixir
@spec highlights_for_line(t(), non_neg_integer()) :: [highlight_range()]
```

Returns all highlight ranges that intersect a specific line.

Convenience wrapper around `highlights_for_lines/3` for single-line queries.

# `highlights_for_lines`

```elixir
@spec highlights_for_lines(t(), non_neg_integer(), non_neg_integer()) :: [
  highlight_range()
]
```

Returns all highlight ranges that intersect the given line range.

This is the primary query for the render pipeline. Returns highlight
range structs (not raw intervals) sorted by priority (lowest first,
so higher priority ranges are applied last and win on overlap).

# `inline_virtual_texts_for_line`

```elixir
@spec inline_virtual_texts_for_line(t(), non_neg_integer()) :: [
  Minga.Core.Decorations.VirtualText.t()
]
```

Returns inline virtual texts for a specific line, sorted by column then priority.

# `merge_highlights`

```elixir
@spec merge_highlights(
  [{String.t(), Minga.Core.Face.t()}],
  [highlight_range()],
  non_neg_integer()
) :: [
  {String.t(), keyword()}
]
```

Merges highlight range styles onto syntax-highlighted segments for a line.

Takes the tree-sitter segments (list of `{text, style}` tuples) and the
highlight ranges intersecting this line, and produces a merged segment
list where decoration styles override syntax styles per-property.

This is the shared merge function used by both highlight range decorations
and (in the future) visual selection. It splits segments at range
boundaries and applies style overrides from highest-priority matching
ranges.

## Arguments

- `segments`: list of `{text, style_keyword}` from tree-sitter or plain rendering
- `ranges`: highlight ranges for this line, sorted by priority (lowest first)
- `line`: the buffer line number (0-indexed)

## Returns

A list of `{text, merged_style}` tuples with finer granularity where
ranges split syntax segments.

# `merge_style_props`

```elixir
@spec merge_style_props(Minga.Core.Face.t(), Minga.Core.Face.t()) ::
  Minga.Core.Face.t()
```

Merges overlay face properties onto a base face.

Only non-nil properties in the overlay override the base. This preserves
tree-sitter syntax colors when a decoration only specifies background.

# `new`

```elixir
@spec new() :: t()
```

Creates an empty decorations store.

# `remove_annotation`

```elixir
@spec remove_annotation(t(), reference()) :: t()
```

Removes a line annotation by ID.

# `remove_block_decoration`

```elixir
@spec remove_block_decoration(t(), reference()) :: t()
```

Removes a block decoration by ID.

# `remove_conceal`

```elixir
@spec remove_conceal(t(), reference()) :: t()
```

Removes a conceal range by ID.

# `remove_conceal_group`

```elixir
@spec remove_conceal_group(t(), atom()) :: t()
```

Removes all conceal ranges belonging to a group.

# `remove_fold_region`

```elixir
@spec remove_fold_region(t(), reference()) :: t()
```

Removes a fold region by ID.

# `remove_group`

```elixir
@spec remove_group(t(), term()) :: t()
```

Removes all decorations belonging to the given group across all types.

Clears highlight ranges, virtual texts, block decorations, fold regions,
and conceal ranges that have a matching `:group` field. This is the
correct way for a decoration consumer (e.g., agent chat, search, LSP)
to clear its own decorations without affecting other consumers.

The `group` parameter is typed as `term()` to support structured keys
like `{:lsp, server_id}` in the future.

# `remove_highlight`

```elixir
@spec remove_highlight(t(), reference()) :: t()
```

Removes a highlight range by ID. No-op if the ID doesn't exist.

# `remove_virtual_text`

```elixir
@spec remove_virtual_text(t(), reference()) :: t()
```

Removes a virtual text decoration by ID.

# `toggle_fold_region`

```elixir
@spec toggle_fold_region(t(), reference()) :: t()
```

Toggles a fold region's open/closed state by ID.

# `virtual_line_count`

```elixir
@spec virtual_line_count(t(), non_neg_integer(), non_neg_integer()) ::
  non_neg_integer()
```

Returns the count of virtual lines (`:above` and `:below`) in the given
line range. Used by viewport scroll calculations.

# `virtual_lines_for_line`

```elixir
@spec virtual_lines_for_line(t(), non_neg_integer()) ::
  {above :: [Minga.Core.Decorations.VirtualText.t()],
   below :: [Minga.Core.Decorations.VirtualText.t()]}
```

Returns virtual lines (`:above` or `:below`) anchored to a specific line.
Returns `{above, below}` tuple, each sorted by priority.

# `virtual_texts_for_line`

```elixir
@spec virtual_texts_for_line(t(), non_neg_integer()) :: [
  Minga.Core.Decorations.VirtualText.t()
]
```

Returns all virtual text decorations anchored to a specific line,
sorted by column then priority.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
