# `Minga.Buffer.Process`
[🔗](https://github.com/jsmestad/minga/blob/main/lib/minga/buffer/process.ex#L1)

GenServer wrapping a `Document` with file I/O and dirty tracking.

Each open file gets its own `Buffer.Process` process, managed by
the `Buffer.Supervisor` (DynamicSupervisor). If a buffer process
crashes, only that buffer is lost, and all other buffers and the
editor continue running.

## Examples

    {:ok, pid} = Minga.Buffer.Process.start_link(file_path: "README.md")
    :ok = Minga.Buffer.Process.insert_char(pid, "x")
    true = Minga.Buffer.Process.dirty?(pid)
    :ok = Minga.Buffer.Process.save(pid)
    false = Minga.Buffer.Process.dirty?(pid)

# `boundary`

```elixir
@type boundary() :: {non_neg_integer(), non_neg_integer()} | nil
```

An edit boundary as `{start_line, end_line}` (both inclusive, 0-indexed), or nil for unbounded.

# `edit_delta_update`

```elixir
@type edit_delta_update() :: {:ok, [Minga.Buffer.EditDelta.t()]} | :reset_required
```

# `motion_fun`

```elixir
@type motion_fun() :: (Minga.Buffer.Document.t(), Minga.Buffer.Document.position() -&gt;
                   Minga.Buffer.Document.position())
```

# `replace_edit`

```elixir
@type replace_edit() :: {old_text :: String.t(), new_text :: String.t()}
```

A find-and-replace edit pair for batch operations.

# `replace_result`

```elixir
@type replace_result() :: {:ok, String.t()} | {:error, String.t()}
```

Result of a single edit within a batch.

# `start_opt`

```elixir
@type start_opt() ::
  {:file_path, String.t()}
  | {:content, String.t()}
  | {:name, GenServer.name()}
  | {:buffer_name, String.t()}
  | {:buffer_type, Minga.Buffer.State.buffer_type()}
  | {:storage, Minga.Buffer.State.storage()}
  | {:filetype, atom()}
  | {:options_server, Minga.Config.Options.server() | nil}
  | {:read_only, boolean()}
  | {:unlisted, boolean()}
  | {:persistent, boolean()}
  | {:events_registry, Minga.Events.registry()}
```

Options for starting a buffer server.

# `state`

```elixir
@type state() :: Minga.Buffer.State.t()
```

Internal state of the buffer server.

# `text_edit`

```elixir
@type text_edit() ::
  {{non_neg_integer(), non_neg_integer()},
   {non_neg_integer(), non_neg_integer()}, String.t()}
```

A single text edit: `{start_pos, end_pos, replacement_text}`.

# `accept_saved_content`

```elixir
@spec accept_saved_content(GenServer.server(), String.t()) :: :ok
```

Accepts content as the saved base revision and clears dirty state.

# `acknowledge_disk_change`

```elixir
@spec acknowledge_disk_change(GenServer.server()) :: :ok
```

Acknowledges the current disk metadata after the user chooses to keep local edits.

# `add_block_decoration`

```elixir
@spec add_block_decoration(GenServer.server(), non_neg_integer(), keyword()) ::
  reference()
```

Adds a block decoration to the buffer.

Returns the decoration ID for later removal.
See `Minga.Core.Decorations.add_block_decoration/3` for options.

# `add_highlight`

```elixir
@spec add_highlight(
  GenServer.server(),
  Minga.Core.Decorations.highlight_range_pos(),
  Minga.Core.Decorations.highlight_range_pos(),
  keyword()
) :: reference()
```

Adds a highlight range decoration to the buffer.

Returns the decoration ID (a reference) for later removal.
See `Minga.Core.Decorations.add_highlight/4` for options.

# `add_virtual_text`

```elixir
@spec add_virtual_text(
  GenServer.server(),
  Minga.Core.Decorations.highlight_range_pos(),
  keyword()
) ::
  reference()
```

Adds a virtual text decoration to the buffer.

Returns the decoration ID (a reference) for later removal.
See `Minga.Core.Decorations.add_virtual_text/3` for options.

# `append`

```elixir
@spec append(GenServer.server(), String.t()) :: :ok
```

Appends text to the end of the buffer, bypassing read-only. For programmatic writes.

# `apply_edit`

```elixir
@spec apply_edit(
  GenServer.server(),
  non_neg_integer(),
  non_neg_integer(),
  non_neg_integer(),
  non_neg_integer(),
  String.t(),
  Minga.Buffer.EditSource.t()
) :: :ok
```

Replaces a range of text with new text.

Moves to the start of the range, deletes the range, then inserts the
new text. Used by LSP text edits.

# `apply_edits`

```elixir
@spec apply_edits(GenServer.server(), [text_edit()], Minga.Buffer.EditSource.t()) ::
  :ok
```

Applies multiple text edits in a single GenServer call.

All edits are applied to the Document sequentially, but only one undo
entry is pushed and the version bumps once. Edits should be sorted in
reverse document order (last position first) so earlier offsets remain
valid as later text is replaced. If edits are not pre-sorted, this
function sorts them automatically.

Used by AI/LSP batch operations to avoid N round-trips and N undo entries.

Source defaults to `{:lsp, :unknown}` (not `:user`) because batch edits
are typically LSP code actions or agent tool calls, not interactive typing.

# `apply_motion`

```elixir
@spec apply_motion(GenServer.server(), motion_fun()) :: :ok
```

Applies a cursor motion inside the buffer process.

# `batch_decorations`

```elixir
@spec batch_decorations(GenServer.server(), (Minga.Core.Decorations.t() -&gt;
                                         Minga.Core.Decorations.t())) ::
  :ok
```

Executes a batch of decoration operations. The function receives and
returns a `Decorations` struct. All operations are applied with a
single tree rebuild.

# `break_undo_coalescing`

```elixir
@spec break_undo_coalescing(GenServer.server()) :: :ok
```

Resets the undo coalescing timer so the next mutation starts a fresh
undo entry. Call this at undo boundaries like mode transitions (e.g.,
leaving insert mode).

# `buffer_name`

```elixir
@spec buffer_name(GenServer.server()) :: String.t() | nil
```

Returns the buffer name (e.g. `*Messages*`), or `nil` for file buffers.

# `buffer_type`

```elixir
@spec buffer_type(GenServer.server()) :: Minga.Buffer.State.buffer_type()
```

Returns the buffer's type (`:file`, `:nofile`, `:nowrite`, `:prompt`, `:terminal`).

# `byte_offset_for_line`

```elixir
@spec byte_offset_for_line(GenServer.server(), non_neg_integer()) :: non_neg_integer()
```

Returns the byte offset for the start of a given line.

# `child_spec`

```elixir
@spec child_spec([start_opt()]) :: Supervisor.child_spec()
```

Returns a child spec with `restart: :temporary`.

Buffers run under a DynamicSupervisor. If a buffer crashes, it should
stay dead rather than restarting without its original init args (file
path, content). The Editor detects the dead buffer via `:DOWN` monitor
and shows a clear indicator to the user.

# `clear_face_override`

```elixir
@spec clear_face_override(GenServer.server(), String.t()) :: :ok
```

Clears a buffer-local face override, restoring the theme default.

# `clear_line`

```elixir
@spec clear_line(GenServer.server(), non_neg_integer()) :: {:ok, String.t()}
```

Clears all content on the given line. Returns `{:ok, yanked_text}`.

# `commit_navigable_snapshot`

```elixir
@spec commit_navigable_snapshot(
  GenServer.server(),
  Minga.Editing.NavigableContent.BufferSnapshot.t()
) ::
  Minga.Editing.Scroll.t()
```

Applies a `BufferSnapshot` back to the server, updating the document
and returning the updated scroll state.

Only writes the document back if content or cursor changed. Returns
the scroll state from the snapshot for the caller to store.

# `commit_snapshot`

```elixir
@spec commit_snapshot(GenServer.server(), Minga.Buffer.Document.t()) :: :ok
```

Replaces the internal gap buffer with a new one, pushing the old buffer
onto the undo stack and marking the buffer dirty.

Use this after performing a batch of pure `Document` operations on a
snapshot. Only call this when content has actually changed; for cursor-only
changes use `move_to/2` instead.

# `consume_edit_deltas`

> This function is deprecated. Use consume_edit_deltas/2 with a consumer_id instead.

```elixir
@spec consume_edit_deltas(GenServer.server()) :: [Minga.Buffer.EditDelta.t()]
```

Returns and clears pending edit deltas accumulated since the last legacy consumer read.

Deprecated: use `consume_edit_deltas/2` with a consumer_id for per-consumer cursors.
This legacy version destructively drains the shared pending changes list.

# `consume_edit_deltas`

```elixir
@spec consume_edit_deltas(GenServer.server(), atom()) :: edit_delta_update()
```

Returns edit deltas accumulated since the given consumer's last read.

Each consumer is identified by an atom (e.g., `:lsp`, `:highlight`).
The buffer tracks a per-consumer cursor (sequence number). On each call,
deltas since that cursor are returned and the cursor advances. The buffer
trims deltas that all registered consumers have read. Returns `:reset_required`
if older retained deltas were compacted before the consumer caught up.

This avoids the data race where two consumers calling `consume_edit_deltas/1`
would each miss the other's deltas.

Deprecated: prefer consuming deltas from `BufferChangedEvent` payloads
on the event bus. LSP SyncServer already accumulates deltas from events.
HighlightSync still uses this during the migration period since it runs
synchronously inside the Editor GenServer before the deferred broadcast fires.

# `content`

```elixir
@spec content(GenServer.server()) :: String.t()
```

Returns the full text content of the buffer.

# `content_and_cursor`

```elixir
@spec content_and_cursor(GenServer.server()) ::
  {String.t(), Minga.Buffer.Document.position()}
```

Returns the content and cursor position in a single GenServer call.

# `content_on_lines`

```elixir
@spec content_on_lines(GenServer.server(), non_neg_integer(), non_neg_integer()) ::
  String.t()
```

Returns the joined text of lines [start_line, end_line] inclusive (no trailing newline).

# `content_range_length`

```elixir
@spec content_range_length(
  GenServer.server(),
  Minga.Buffer.Document.position(),
  Minga.Buffer.Document.position()
) :: non_neg_integer()
```

Returns the grapheme count in the range [start_pos, end_pos] inclusive.
Positions are sorted automatically.

# `cursor`

```elixir
@spec cursor(GenServer.server()) :: Minga.Buffer.Document.position()
```

Returns the current cursor position.

# `decorations`

```elixir
@spec decorations(GenServer.server()) :: Minga.Core.Decorations.t()
```

Returns the decorations struct for read-only access (e.g., by the render pipeline).

# `decorations_version`

```elixir
@spec decorations_version(GenServer.server()) :: non_neg_integer()
```

Returns the decorations version for cheap change detection.

# `delete_at`

```elixir
@spec delete_at(GenServer.server()) :: :ok
```

Deletes the character at the cursor (delete forward).

# `delete_before`

```elixir
@spec delete_before(GenServer.server()) :: :ok
```

Deletes the character before the cursor (backspace).

# `delete_lines`

```elixir
@spec delete_lines(GenServer.server(), non_neg_integer(), non_neg_integer()) :: :ok
```

Deletes lines [start_line, end_line] inclusive. Cursor lands at the first remaining line.

# `delete_range`

```elixir
@spec delete_range(
  GenServer.server(),
  Minga.Buffer.Document.position(),
  Minga.Buffer.Document.position()
) :: :ok
```

Deletes the text between two positions (from_pos inclusive, to_pos exclusive), placing the cursor at the start of the range.

# `dirty?`

```elixir
@spec dirty?(GenServer.server()) :: boolean()
```

Returns whether the buffer has unsaved changes.

# `display_name`

```elixir
@spec display_name(GenServer.server()) :: String.t()
```

Returns the display name for use in the status bar and modeline.

For named buffers (e.g. `*Messages*`), returns the name directly with a
`[RO]` suffix when read-only. For file buffers, returns `Path.basename(file_path)`
with a `[RO]` suffix when read-only, or `"[no file]"` when no path is set.

Single GenServer round-trip; prefer over combining `buffer_name/1`,
`file_path/1`, and `read_only?/1` separately.

# `face_overrides`

```elixir
@spec face_overrides(GenServer.server()) :: %{required(String.t()) =&gt; keyword()}
```

Returns the buffer-local face overrides map.

Face overrides are `%{face_name => [attr: value, ...]}` pairs that
are merged on top of the theme's face registry when rendering this
buffer. Used for filetype-specific styling (e.g., Markdown uses a
different default font) and buffer-local customization.

# `file_path`

```elixir
@spec file_path(GenServer.server()) :: String.t() | nil
```

Returns the file path associated with this buffer, if any.

# `filetype`

```elixir
@spec filetype(GenServer.server()) :: atom()
```

Returns the detected filetype atom for this buffer.

# `find_and_replace`

```elixir
@spec find_and_replace(GenServer.server(), String.t(), String.t(), boundary()) ::
  {:ok, String.t()} | {:error, String.t()}
```

Atomically finds and replaces text in the buffer.

The search, ambiguity check, and replacement all happen inside a single
`handle_call`, so there is no TOCTOU race between reading content and
applying the edit. Returns `{:ok, message}` on success, or `{:error, reason}`
if the text is not found, is ambiguous (multiple matches), or the buffer
is read-only.

# `find_and_replace_batch`

```elixir
@spec find_and_replace_batch(GenServer.server(), [replace_edit()], boundary()) ::
  {:ok, [replace_result()]} | {:error, String.t()}
```

Atomically applies multiple find-and-replace edits in a single `handle_call`.

Edits are applied sequentially: earlier edits affect the content that later
edits search against. Failed edits (not found, ambiguous) are reported but
don't block subsequent edits. A single undo entry is pushed for the entire
batch.

When `boundary` is provided, each edit is checked against the boundary and
rejected if the match falls outside the allowed line range.

Returns `{:ok, results}` where results is a list of per-edit outcomes.

# `force_save`

```elixir
@spec force_save(GenServer.server()) :: :ok | {:error, term()}
```

Force-saves the buffer, skipping mtime conflict detection.

# `get_option`

```elixir
@spec get_option(GenServer.server(), atom()) :: term()
```

Returns a buffer-local option value using the resolution chain:
buffer-local → filetype override → global default.

Buffer-local options take highest priority. If no local override
exists, the filetype default from `Config.Options` is checked, then
the global default. This gives each buffer its own isolated option
state while inheriting sensible defaults.

# `insert_char`

```elixir
@spec insert_char(GenServer.server(), String.t(), Minga.Buffer.EditSource.t()) :: :ok
```

Inserts a character at the current cursor position.

# `insert_text`

```elixir
@spec insert_text(GenServer.server(), String.t(), Minga.Buffer.EditSource.t()) :: :ok
```

Inserts a string at the current cursor position.

Each character is inserted sequentially, advancing the cursor.

# `last_redo_source`

```elixir
@spec last_redo_source(GenServer.server()) :: Minga.Buffer.State.edit_source() | nil
```

Returns the edit source of the most recent redo entry, or `nil` if the redo stack is empty.

# `last_undo_source`

```elixir
@spec last_undo_source(GenServer.server()) :: Minga.Buffer.State.edit_source() | nil
```

Returns the edit source of the most recent undo entry, or `nil` if the undo stack is empty.

# `line_count`

```elixir
@spec line_count(GenServer.server()) :: pos_integer()
```

Returns the total line count.

# `lines`

```elixir
@spec lines(GenServer.server(), non_neg_integer(), non_neg_integer()) :: [String.t()]
```

Returns a range of lines from the buffer.

# `local_option_overrides`

```elixir
@spec local_option_overrides(GenServer.server()) :: %{required(atom()) =&gt; term()}
```

Returns only options explicitly overridden on this buffer.

# `local_options`

```elixir
@spec local_options(GenServer.server()) :: %{required(atom()) =&gt; term()}
```

Returns all buffer option values currently cached on this buffer.

# `move`

```elixir
@spec move(GenServer.server(), Minga.Buffer.Document.direction()) :: :ok
```

Moves the cursor in the given direction.

# `move_if_possible`

```elixir
@spec move_if_possible(GenServer.server(), :left | :right) ::
  {:ok, Minga.Buffer.Document.position()} | :at_boundary
```

Moves the cursor left or right if the move is valid, performing the boundary
check inside the buffer process. Returns `{:ok, new_position}` if the cursor
moved, or `:at_boundary` if the cursor was already at the boundary.

For `:left`, the boundary is column 0.
For `:right`, the boundary is the last grapheme position on the current line.

This avoids copying the entire `Document.t()` across the process boundary
just to check whether the cursor is at a line boundary.

# `move_to`

```elixir
@spec move_to(GenServer.server(), Minga.Buffer.Document.position()) :: :ok
```

Moves the cursor to an exact position.

# `navigable_snapshot`

```elixir
@spec navigable_snapshot(GenServer.server(), Minga.Editing.Scroll.t()) ::
  Minga.Editing.NavigableContent.BufferSnapshot.t()
```

Takes a snapshot wrapped in a `BufferSnapshot` struct for use with the
`NavigableContent` protocol. Includes the given scroll state so that
scroll operations can be composed with cursor and content changes.

After operating on the snapshot through `NavigableContent`, apply the
result back with `commit_navigable_snapshot/2`.

# `open`

```elixir
@spec open(GenServer.server(), String.t()) :: :ok | {:error, term()}
```

Opens a file, replacing the current buffer content.

# `persistent?`

```elixir
@spec persistent?(GenServer.server()) :: boolean()
```

Returns whether the buffer is persistent (auto-recreated on kill).

# `pid_for_path`

```elixir
@spec pid_for_path(String.t()) :: {:ok, pid()} | :not_found
```

Looks up the buffer pid for a file path via `Minga.Buffer.Registry`.

Returns `{:ok, pid}` if a buffer is registered for the given path,
or `:not_found` if no buffer has that path open. O(1) ETS lookup,
no GenServer calls. The path is expanded to an absolute path before lookup.

# `read_only?`

```elixir
@spec read_only?(GenServer.server()) :: boolean()
```

Returns whether the buffer is read-only.

# `redo`

```elixir
@spec redo(GenServer.server()) :: :ok
```

Redoes the last undone mutation.

# `reload`

```elixir
@spec reload(GenServer.server()) :: :ok | {:error, term()}
```

Reloads the buffer from disk, preserving cursor position (clamped). Clears undo/redo history.

# `remap_face`

```elixir
@spec remap_face(GenServer.server(), String.t(), keyword()) :: :ok
```

Sets a buffer-local face override.

Merges the given attributes on top of the named face for this buffer
only. Other buffers are unaffected. The override persists until
cleared with `clear_face_override/2`.

## Examples

    Buffer.Process.remap_face(buf, "default", fg: 0x000000, bg: 0xFFFFFF)
    Buffer.Process.remap_face(buf, "comment", italic: false)

# `remove_block_decoration`

```elixir
@spec remove_block_decoration(GenServer.server(), reference()) :: :ok
```

Removes a block decoration by ID.

# `remove_highlight`

```elixir
@spec remove_highlight(GenServer.server(), reference()) :: :ok
```

Removes a highlight range by ID.

# `remove_highlight_group`

```elixir
@spec remove_highlight_group(GenServer.server(), atom()) :: :ok
```

Removes all highlight ranges in a group.

# `remove_virtual_text`

```elixir
@spec remove_virtual_text(GenServer.server(), reference()) :: :ok
```

Removes a virtual text decoration by ID.

# `render_snapshot`

```elixir
@spec render_snapshot(GenServer.server(), non_neg_integer(), non_neg_integer()) ::
  Minga.Buffer.RenderSnapshot.t()
```

Returns all data needed to render a single frame in one GenServer call.

Fetches cursor position, total line count, the visible line range starting at
`first_line` (up to `count` lines), file path, and dirty flag atomically.
This replaces 5 individual calls (cursor, line_count, lines, file_path,
dirty?) with a single round-trip.

# `replace_content`

```elixir
@spec replace_content(
  GenServer.server(),
  String.t(),
  Minga.Buffer.State.edit_source()
) ::
  :ok | {:error, :read_only}
```

Replaces the entire buffer content, pushing the old content onto the undo stack.

# `replace_content_with_decorations`

```elixir
@spec replace_content_with_decorations(
  GenServer.server(),
  String.t(),
  (Minga.Core.Decorations.t() -&gt; Minga.Core.Decorations.t()),
  keyword()
) :: :ok
```

Atomically replaces buffer content and rebuilds decorations in a single GenServer call.

The `decoration_fn` receives a fresh `Decorations.new()` and returns
the new decorations. Optional `cursor` clamps the cursor position.
This prevents a render frame from seeing new content with zero decorations.

# `replace_generated_content`

```elixir
@spec replace_generated_content(GenServer.server(), String.t()) :: :ok
```

Replaces buffer content bypassing read-only. For programmatic panel updates.

# `retarget_path`

```elixir
@spec retarget_path(GenServer.server(), String.t()) :: :ok | {:error, term()}
```

Retargets the buffer to a new file path without writing content.

# `save`

```elixir
@spec save(GenServer.server()) :: :ok | {:error, term()}
```

Saves the buffer content to the associated file.

# `save_as`

```elixir
@spec save_as(GenServer.server(), String.t()) :: :ok | {:error, term()}
```

Saves the buffer content to a specific file path.

# `set_filetype`

```elixir
@spec set_filetype(GenServer.server(), atom()) :: :ok
```

Changes the buffer's filetype and re-seeds per-filetype options.

The buffer content is not modified; only metadata (filetype, tab_width,
indent_with, etc.) changes. The caller is responsible for triggering
a highlight reparse after this call.

# `set_option`

```elixir
@spec set_option(GenServer.server(), atom(), term()) ::
  {:ok, term()} | {:error, String.t()}
```

Sets a buffer-local option override. Only affects this buffer.

The value is validated against the same type rules as
`Config.Options.set/2`. Returns `{:ok, value}` on success or
`{:error, reason}` if the value is invalid.

# `snapshot`

```elixir
@spec snapshot(GenServer.server()) :: Minga.Buffer.Document.t()
```

Returns the underlying `Document.t()` struct for pure computation.

Use this to batch multiple reads into a single GenServer call. Perform
all calculations on the returned struct, then apply the result with
`move_to/2` (cursor-only changes) or `commit_snapshot/2` (content changes).

# `start_link`

```elixir
@spec start_link([start_opt()]) :: GenServer.on_start()
```

Starts a buffer server. Pass `file_path:` to open a file, or `content:` for an unnamed buffer.

# `storage`

```elixir
@spec storage(GenServer.server()) :: Minga.Buffer.State.storage()
```

Returns the buffer storage backend.

# `text_between_inclusive`

```elixir
@spec text_between_inclusive(
  GenServer.server(),
  Minga.Buffer.Document.position(),
  Minga.Buffer.Document.position()
) :: String.t()
```

Returns the text in the range [start_pos, end_pos] inclusive.
Positions are sorted automatically.

# `undo`

```elixir
@spec undo(GenServer.server()) :: :ok
```

Undoes the last mutation, restoring the previous buffer state.

# `undo_agent_session`

```elixir
@spec undo_agent_session(GenServer.server()) :: {:ok, non_neg_integer()} | :empty
```

Undoes all consecutive agent-sourced entries from the top of the undo stack.

# `unlisted?`

```elixir
@spec unlisted?(GenServer.server()) :: boolean()
```

Returns whether the buffer is unlisted (hidden from buffer picker).

# `version`

```elixir
@spec version(GenServer.server()) :: non_neg_integer()
```

Returns the buffer's mutation version counter (increments on every content change).

---

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