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).
Summary
Types
A color value: 24-bit RGB integer.
A highlight range decoration.
A position used in highlight range start/end.
A column-indexed style overlay: applies from start_col (inclusive) to end_col (exclusive).
Style for a highlight range: a Face struct where nil fields inherit from the underlying syntax style.
The decorations state for a buffer.
Functions
Adds a line annotation to the buffer. Returns {id, updated_decorations}.
Adds a block decoration to the buffer. Returns {id, updated_decorations}.
Adds a conceal range. Returns {id, updated_decorations}.
Adds a buffer-level fold region. Returns {id, updated_decorations}.
Adds a highlight range. Returns {id, updated_decorations}.
Adds a virtual text decoration to the buffer. Returns {id, updated_decorations}.
Adjusts all decoration anchors after a buffer edit.
Returns all annotations for a specific line, sorted by priority.
Executes a batch of operations, deferring tree rebuilding until the end.
Returns the block decoration with the given ID, or nil.
Returns block decorations for a specific anchor line, sorted by priority.
Returns {above, below} tuple.
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.
Builds the annotation line cache for O(1) per-line lookups.
Builds the virtual text line cache.
Removes all decorations. Returns a fresh empty store with bumped version.
Returns all closed fold regions, sorted by start_line.
Returns conceal ranges that intersect the given line, sorted by start column.
Converts a display column to a buffer column on the given line, accounting for inline virtual text.
Returns true if there are no decorations of any kind.
Returns EOL virtual texts for a specific line, sorted by priority.
Returns the fold region containing the given line, or nil.
Returns true if there are any annotations.
Returns true if there are any block decorations.
Returns true if there are any conceal ranges.
Returns true if there are any fold regions.
Returns true if there are any virtual texts of any placement.
Returns the number of highlight ranges.
Returns all highlight ranges that intersect a specific line.
Returns all highlight ranges that intersect the given line range.
Returns inline virtual texts for a specific line, sorted by column then priority.
Merges highlight range styles onto syntax-highlighted segments for a line.
Merges overlay face properties onto a base face.
Creates an empty decorations store.
Removes a line annotation by ID.
Removes a block decoration by ID.
Removes a conceal range by ID.
Removes all conceal ranges belonging to a group.
Removes a fold region by ID.
Removes all decorations belonging to the given group across all types.
Removes a highlight range by ID. No-op if the ID doesn't exist.
Removes a virtual text decoration by ID.
Toggles a fold region's open/closed state by ID.
Returns the count of virtual lines (:above and :below) in the given
line range. Used by viewport scroll calculations.
Returns virtual lines (:above or :below) anchored to a specific line.
Returns {above, below} tuple, each sorted by priority.
Returns all virtual text decorations anchored to a specific line, sorted by column then priority.
Types
@type color() :: non_neg_integer()
A color value: 24-bit RGB integer.
@type highlight_range() :: Minga.Core.Decorations.HighlightRange.t()
A highlight range decoration.
id: unique reference for removalstart: 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)
@type highlight_range_pos() :: Minga.Core.IntervalTree.position()
A position used in highlight range start/end.
@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).
@type style() :: Minga.Core.Face.t()
Style for a highlight range: a Face struct where nil fields inherit from the underlying syntax style.
@type t() :: %Minga.Core.Decorations{ ann_line_cache: %{ required(non_neg_integer()) => [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()) => [Minga.Core.Decorations.VirtualText.t()]} | nil }
The decorations state for a buffer.
highlights: interval tree of highlight rangesvirtual_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
Functions
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, default0xFFFFFF) - foreground color (24-bit RGB):bg(optional, default0x6366F1) - 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)
@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) -:aboveor:belowthe 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) -> :okfor interactive blocks:priority(optional, default 0) - ordering when multiple blocks share an anchor
@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
)
@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
)
@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) - aFace.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
)
@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
)
@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:
- Insert before range: shift range right
- Insert within range: expand range
- Delete within range: shrink range (remove if fully deleted)
- 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.
@spec annotations_for_line(t(), non_neg_integer()) :: [ Minga.Core.Decorations.LineAnnotation.t() ]
Returns all annotations for a specific line, sorted by priority.
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)
@spec block_decoration_by_id(t(), reference()) :: Minga.Core.Decorations.BlockDecoration.t() | nil
Returns the block decoration with the given ID, or nil.
@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.
@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.
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.
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).
Removes all decorations. Returns a fresh empty store with bumped version.
@spec closed_fold_regions(t()) :: [Minga.Core.Decorations.FoldRegion.t()]
Returns all closed fold regions, sorted by start_line.
@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.
@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.
Returns true if there are no decorations of any kind.
@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.
@spec fold_region_at(t(), non_neg_integer()) :: Minga.Core.Decorations.FoldRegion.t() | nil
Returns the fold region containing the given line, or nil.
Returns true if there are any annotations.
Returns true if there are any block decorations.
Returns true if there are any conceal ranges.
Returns true if there are any fold regions.
Returns true if there are any virtual texts of any placement.
@spec highlight_count(t()) :: non_neg_integer()
Returns the number of highlight ranges.
@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.
@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).
@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.
@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 renderingranges: 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.
@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.
@spec new() :: t()
Creates an empty decorations store.
Removes a line annotation by ID.
Removes a block decoration by ID.
Removes a conceal range by ID.
Removes all conceal ranges belonging to a group.
Removes a fold region by ID.
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.
Removes a highlight range by ID. No-op if the ID doesn't exist.
Removes a virtual text decoration by ID.
Toggles a fold region's open/closed state by ID.
@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.
@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.
@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.