Minga.Core.Decorations (Minga v0.1.0)

Copy Markdown View Source

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.

t()

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

color()

@type color() :: non_neg_integer()

A color value: 24-bit RGB integer.

highlight_range()

@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()

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

A position used in highlight range start/end.

overlay()

@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()

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

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

t()

@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 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

Functions

add_annotation(decs, line, text, opts \\ [])

@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(decs, anchor_line, opts)

@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(decs, start_pos, end_pos, opts \\ [])

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(decs, start_line, end_line, opts \\ [])

@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(decs, start_pos, end_pos, opts)

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(decs, anchor, opts)

@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(decs, edit_start, edit_end, new_end)

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(decorations, line)

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

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

batch(decs, fun)

@spec batch(t(), (t() -> 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(decorations, id)

@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(decorations, line)

@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(decs, line, buf_col)

@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(decs)

@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(decs)

@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(decs)

@spec clear(t()) :: t()

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

closed_fold_regions(decorations)

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

Returns all closed fold regions, sorted by start_line.

conceals_for_line(decorations, 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.

display_col_to_buf_col(decs, line, display_col)

@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?(decorations)

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

Returns true if there are no decorations of any kind.

eol_virtual_texts_for_line(decs, line)

@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(decorations, line)

@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?(decorations)

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

Returns true if there are any annotations.

has_block_decorations?(decorations)

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

Returns true if there are any block decorations.

has_conceal_ranges?(decorations)

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

Returns true if there are any conceal ranges.

has_fold_regions?(decorations)

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

Returns true if there are any fold regions.

has_virtual_texts?(decorations)

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

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

highlight_count(decorations)

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

Returns the number of highlight ranges.

highlights_for_line(decs, line)

@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(decorations, start_line, end_line)

@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(decs, line)

@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(segments, ranges, line)

@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(base, overlay)

@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()

@spec new() :: t()

Creates an empty decorations store.

remove_annotation(decs, id)

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

Removes a line annotation by ID.

remove_block_decoration(decs, id)

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

Removes a block decoration by ID.

remove_conceal(decs, id)

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

Removes a conceal range by ID.

remove_conceal_group(decs, group)

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

Removes all conceal ranges belonging to a group.

remove_fold_region(decs, id)

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

Removes a fold region by ID.

remove_group(decs, group)

@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(decs, id)

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

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

remove_virtual_text(decs, id)

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

Removes a virtual text decoration by ID.

toggle_fold_region(decs, id)

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

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

virtual_line_count(decorations, start_line, end_line)

@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(decs, line)

@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(decorations, line)

@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.