# `MingaEditor.Renderer.WindowCache`
[🔗](https://github.com/jsmestad/minga/blob/main/lib/minga_editor/renderer/window_cache.ex#L1)

Per-window render state for incremental rendering.

Tracks which buffer lines need re-rendering and stores last-frame comparison
values so the render pipeline can detect when a full redraw is needed. The
semantic `RenderModel.Window.Builder` reuses retained composed rows (see the
`retained_rows`/`retained_wrap_lines` fields) for unchanged lines.

## Dirty-line tracking

The dirty set uses two representations:

- `:all` means every line needs re-rendering (scroll, resize, theme
  change, highlight update, fold toggle, or any other wholesale
  invalidation)
- A map of specific buffer line numbers (`%{line => true}`) for targeted
  invalidation (edits that touch a few lines)

## Tracking fields

`last_viewport_top`, `last_viewport_cache_key`, `last_gutter_w`,
`last_line_count`, `last_cursor_line`, and `last_buf_version` store values
from the previous frame. The Scroll stage compares current values against
these to detect full-invalidation triggers. `last_context_fingerprint`
captures all per-frame render context inputs (visual selection, search
matches, syntax highlights, signs, etc.) so context changes trigger full
redraws.

# `context_fingerprint`

```elixir
@type context_fingerprint() :: term()
```

Context fingerprint: a term derived from the render context that
captures all per-frame inputs affecting every visible line. When
the fingerprint changes between frames, all lines are re-rendered.

Built from: visual selection, search matches, highlight version,
diagnostic signs, git signs, viewport left scroll, active status,
and theme color structs.

# `retained_row`

```elixir
@type retained_row() ::
  {input_hash :: non_neg_integer(), Minga.RenderModel.Window.Row.t()}
```

A retained composed row plus the cheap input fingerprint that produced it.

The Content stage (semantic Builder) keys these by `row_id`. On the next
frame it recomputes only the input fingerprint per row; when the fingerprint
matches, it reuses the cached `Row.t()` verbatim instead of recomposing the
text and spans. See `MingaEditor.RenderModel.Window.Builder` (#2287).

# `retained_wrap_line`

```elixir
@type retained_wrap_line() :: {input_hash :: non_neg_integer(), entries :: [map()]}
```

A retained wrapped logical line: the cheap input fingerprint that produced its
visual rows plus the full visual-row entry list (Rows and their wrap metadata).

The Content stage keys these by the first visual row's durable row id. On the next
frame, when the logical line's fingerprint (line text, highlight segments,
compose context, and content width) is unchanged, the Builder reuses the
entire visual-row set verbatim instead of recomposing the line and recomputing
its wrap points. See `MingaEditor.RenderModel.Window.Builder` (#2287).

# `t`

```elixir
@type t() :: %MingaEditor.Renderer.WindowCache{
  applied_change_sequence: non_neg_integer(),
  changed_snapshot: Minga.Buffer.RenderSnapshot.t() | nil,
  content_epoch: non_neg_integer(),
  dirty_lines: :all | %{optional(non_neg_integer()) =&gt; true},
  fetch_version: non_neg_integer() | nil,
  hydration_reason: atom() | nil,
  identity_buffer: pid() | nil,
  last_buf_version: integer(),
  last_context_fingerprint: context_fingerprint(),
  last_cursor_line: integer(),
  last_gutter_w: integer(),
  last_line_count: integer(),
  last_reset_fingerprint: term(),
  last_viewport_cache_key: integer(),
  last_viewport_top: integer(),
  line_identity: Minga.RenderModel.Window.LineIdentity.t() | nil,
  pending_edit_deltas: [Minga.Buffer.EditDelta.t()],
  reset_pending: boolean(),
  residence_armed: boolean(),
  resident: boolean(),
  resident_build: MingaEditor.RenderModel.Window.ResidentBuild.t() | nil,
  retained_rows: %{optional(non_neg_integer()) =&gt; retained_row()},
  retained_wrap_lines: %{optional(non_neg_integer()) =&gt; retained_wrap_line()},
  row_slot_allocator: Minga.RenderModel.Window.RowSlotAllocator.t(),
  scroll_seq: non_neg_integer(),
  scroll_seq_last_authoritative: non_neg_integer(),
  scroll_seq_last_top: integer() | nil,
  total_visual_rows_cache: {term(), non_neg_integer()} | nil
}
```

# `applied_change_sequence`

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

Returns the sequence through which the line identity has been applied.

# `apply_edit_deltas`

```elixir
@spec apply_edit_deltas(
  t(),
  pid(),
  [Minga.Buffer.EditDelta.t()],
  Minga.Buffer.RenderSnapshot.t() | nil
) :: t()
```

Applies renderer-consumed deltas and retains their atomic changed snapshot.

# `cached_total_visual_rows`

```elixir
@spec cached_total_visual_rows(t(), term()) :: non_neg_integer() | nil
```

Returns a cached wrapped visual row total when the key matches.

# `changed_snapshot`

```elixir
@spec changed_snapshot(t()) :: Minga.Buffer.RenderSnapshot.t() | nil
```

Returns the atomic bounded snapshot for pending resident deltas.

# `content_epoch`

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

Returns the durable content epoch.

# `detect_context_change`

```elixir
@spec detect_context_change(t(), context_fingerprint()) :: t()
```

Compares the current render context fingerprint against the last frame's.

If the fingerprint changed, marks all lines dirty. Catches changes to
visual selection, search matches, syntax highlights, diagnostic signs,
git signs, horizontal scroll, active/inactive status, and theme colors.

# `detect_invalidation`

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

Checks current frame parameters against last-frame tracking fields
and marks all lines dirty if anything requiring a full redraw has changed.

Structural redraw triggers: viewport scroll, gutter width, line count, buffer
version, first frame (sentinel values). Only first frame and gutter-width
geometry changes request a retained-GUI epoch reset; ordinary text edits and
line-count changes use row hashes without bumping the content epoch.

# `detect_invalidation`

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

# `detect_invalidation`

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

# `dirty?`

```elixir
@spec dirty?(t(), non_neg_integer()) :: boolean()
```

Returns true if the given buffer line needs re-rendering.

# `fetch_version`

```elixir
@spec fetch_version(t()) :: non_neg_integer() | nil
```

Returns the renderer-consumed version required by bounded fetches.

# `hydration_reason`

```elixir
@spec hydration_reason(t()) :: atom() | nil
```

Returns the explicit reason for the next full resident hydration.

# `line_identity`

```elixir
@spec line_identity(t()) :: Minga.RenderModel.Window.LineIdentity.t() | nil
```

Returns the durable logical-line identity sequence.

# `mark_dirty`

```elixir
@spec mark_dirty(t(), [non_neg_integer()] | :all) :: t()
```

Marks specific buffer lines as needing re-render.

Pass `:all` to force a complete redraw. Pass a list of buffer line
numbers for targeted invalidation. If already fully dirty, adding
specific lines is a no-op.

# `mark_identity_reset`

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

Forces the next hydration to establish a fresh durable content epoch.

# `mark_reset_pending`

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

Marks the next retained GUI frame as a frontend-state reset.

# `pending_edit_deltas`

```elixir
@spec pending_edit_deltas(t()) :: [Minga.Buffer.EditDelta.t()]
```

Returns renderer-consumed deltas pending resident composition.

# `prepare_epoch`

```elixir
@spec prepare_epoch(t(), term()) :: {t(), non_neg_integer(), boolean()}
```

Prepares the retained GUI content epoch for the current frame.

# `put_lineage`

```elixir
@spec put_lineage(
  t(),
  pid(),
  Minga.RenderModel.Window.LineIdentity.t(),
  non_neg_integer()
) :: t()
```

Overlays renderer-owned committed lineage onto a stale editor snapshot.

# `put_resident_build`

```elixir
@spec put_resident_build(t(), MingaEditor.RenderModel.Window.ResidentBuild.t() | nil) ::
  t()
```

Replaces the persistent residence build state captured by the last frame.

# `put_retained_rows`

```elixir
@spec put_retained_rows(t(), %{optional(non_neg_integer()) =&gt; retained_row()}) :: t()
```

Replaces the retained-row map with the current frame's composed rows.

Stored wholesale (not merged) so the map stays bounded to the visible row
set and never accumulates rows that scrolled out of view.

# `put_retained_wrap_lines`

```elixir
@spec put_retained_wrap_lines(t(), %{
  optional(non_neg_integer()) =&gt; retained_wrap_line()
}) :: t()
```

Replaces the retained wrapped-line map with the current frame's logical lines.

Stored wholesale (not merged) so the map stays bounded to the visible logical
lines and never accumulates lines that scrolled out of view.

# `put_row_slot_allocator`

```elixir
@spec put_row_slot_allocator(t(), Minga.RenderModel.Window.RowSlotAllocator.t()) ::
  t()
```

Stores the producer row-slot allocator after a successful content build.

# `put_total_visual_rows`

```elixir
@spec put_total_visual_rows(t(), term(), non_neg_integer()) :: t()
```

Stores the wrapped visual row total for the current cache key.

# `require_hydration`

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

Invalidates retained composition for full hydration while preserving residence promotion.

# `reset`

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

Returns a fresh cache with all lines dirty.

Use after any event that invalidates render state: buffer switch,
resize, theme change, etc.

# `reset`

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

Returns a fresh cache invalidation while preserving retained-render epoch state.

# `reset_content_identity`

```elixir
@spec reset_content_identity(t(), pid(), Minga.Buffer.RenderSnapshot.t()) :: t()
```

Explicitly rebuilds durable content identity in a fresh epoch.

# `residence_armed?`

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

Returns whether residence was armed by the previous eligible frame (#2679).

# `resident?`

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

Returns the residence flag captured by the last rendered frame.

# `resident_build`

```elixir
@spec resident_build(t()) :: MingaEditor.RenderModel.Window.ResidentBuild.t() | nil
```

Returns the persistent full-document residence build state, or `nil` when the
window has not built on the residence path since the last reset. Carries the
resident entry list and its incremental content digest across frames.

# `retained_rows`

```elixir
@spec retained_rows(t()) :: %{optional(non_neg_integer()) =&gt; retained_row()}
```

Returns the `{row_id => {input_hash, Row.t()}}` map captured by the last
semantic content build. The Builder consults it to skip recomposing rows
whose cheap input fingerprint is unchanged.

# `retained_wrap_lines`

```elixir
@spec retained_wrap_lines(t()) :: %{
  optional(non_neg_integer()) =&gt; retained_wrap_line()
}
```

Returns the `{durable_row_id => {input_hash, entries}}` map captured by the last
semantic content build for wrapped windows. The Builder consults it to skip
recomposing and re-wrapping logical lines whose input fingerprint is unchanged.

# `row_slot_allocator`

```elixir
@spec row_slot_allocator(t()) :: Minga.RenderModel.Window.RowSlotAllocator.t()
```

Returns the persistent producer row-slot allocator.

# `scroll_seq`

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

Returns the monotonic scroll-authority sequence (#2661).

# `set_residence_armed`

```elixir
@spec set_residence_armed(t(), boolean()) :: t()
```

Arms (or disarms) full-document residence for the next frame (#2679).

First-paint-then-promote: a resident-eligible window renders viewport-windowed
on its first frame after becoming eligible, arming promotion; the next frame
sees `residence_armed?/1` true and emits full residence. This keeps the
expensive O(document) first build off file-open first paint. The flag is reset
by `reset/1` so a layout_generation rebuild (resize, font, wrap) re-defers.

# `set_resident`

```elixir
@spec set_resident(t(), boolean()) :: t()
```

Records the render pipeline's full-document residence decision for the frame.

Lives in the render cache (not a top-level `Window` field) because the render
cache is the only per-window struct the async render writeback copies back to
the live window (`MingaEditor.State.merge_renderer_window/2`). The input layer
reads it via `resident?/1`.

# `settle_scroll_seq`

```elixir
@spec settle_scroll_seq(t(), integer(), integer() | nil, non_neg_integer()) :: t()
```

Advances the scroll-authority sequence when this frame made a genuine
BEAM-initiated scroll, and records the new baselines.

`scroll_seq` bumps when EITHER of two independent signals fires:

* **Authoritative marker (#2652):** `authoritative_seq` (the editor-owned
  `Window.authoritative_scroll_seq` request counter) moved past the last count
  this cache settled against. Command handlers increment it for the commands
  that must discard a frontend offset — at dispatch for always-authoritative
  viewport commands, and in the success branch of failable jumps (the
  `MingaEditor.Commands` `@authoritative_scroll_commands` comment is the
  source of truth for the set). This is the only signal that catches a jump
  landing exactly on the previous or echoed top (`zz` while already centered,
  a search hit already on screen), which the top comparison below cannot see.

* **Top comparison:** `top` differs from both the previous committed top
  (`scroll_seq_last_top`) and the frontend-reported free-scroll top
  (`echo_top`). A move to the echoed top is the frontend's own report reflected
  back (a wheel/trackpad free scroll, always viewport-only), so it does not
  advance the sequence. The first settle after a fresh cache (nil baseline) does
  not bump via this path (it only records the baseline).

The two signals are OR-combined into one bump, so a jump that both marks the
counter and moves the top advances `scroll_seq` exactly once, never twice.

The marker cannot latch: the render cache overwrites `scroll_seq_last_authoritative`
to the observed counter on every settle rather than the counter being cleared on
the live window, so a single increment causes at most one bump per rendered
lineage. Because the counter (`scroll_seq`) and both baselines live here and the
render cache is written back to the live window, the sequence is monotonic per
rendered lineage; overlapped in-flight frames can emit duplicate or regressing
values, which the frontend tolerates (strict greater-than check, with anchor-key
and reset_required covering discards).

# `snapshot`

```elixir
@spec snapshot(
  t(),
  non_neg_integer(),
  non_neg_integer(),
  non_neg_integer(),
  non_neg_integer(),
  non_neg_integer(),
  context_fingerprint()
) :: t()
```

Snapshots tracking fields after a successful render pass.

Clears the dirty set and records the current frame's parameters so the
next frame can detect what changed.

# `snapshot`

```elixir
@spec snapshot(
  t(),
  non_neg_integer(),
  non_neg_integer(),
  non_neg_integer(),
  non_neg_integer(),
  non_neg_integer(),
  non_neg_integer(),
  context_fingerprint()
) :: t()
```

# `sync_line_identity`

```elixir
@spec sync_line_identity(t(), pid(), Minga.Buffer.RenderSnapshot.t()) :: t()
```

Reconciles durable logical-line identity from one atomic buffer snapshot.

# `with_fetch_version`

```elixir
@spec with_fetch_version(t(), non_neg_integer() | nil) :: t()
```

Pins bounded line fetches to the version atomically consumed by the renderer.

---

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