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

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

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

## Examples

    {:ok, pid} = Minga.Buffer.Server.start_link(file_path: "README.md")
    :ok = Minga.Buffer.Server.insert_char(pid, "x")
    true = Minga.Buffer.Server.dirty?(pid)
    :ok = Minga.Buffer.Server.save(pid)
    false = Minga.Buffer.Server.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.

# `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()}
  | {:filetype, atom()}
  | {:read_only, boolean()}
  | {:unlisted, boolean()}
  | {:persistent, boolean()}
```

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}`.

# `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_navigable_snapshot`

```elixir
@spec apply_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.

# `apply_snapshot`

```elixir
@spec apply_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.

# `apply_text_edit`

```elixir
@spec apply_text_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_text_edits`

```elixir
@spec apply_text_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.

# `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}`.

# `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_range`

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

Returns the text between two positions (from_pos inclusive, to_pos exclusive).

# `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.

# `flush_edits`

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

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

Returns and clears pending edit deltas accumulated since the last flush.

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

# `flush_edits`

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

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.

This avoids the data race where two consumers calling `flush_edits/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.

# `force_save`

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

Force-saves the buffer, skipping mtime conflict detection.

# `get_lines`

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

Returns a range of lines from the buffer.

# `get_lines_content`

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

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

# `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.

# `get_range`

```elixir
@spec get_range(
  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.

# `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.

# `line_count`

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

Returns the total line count.

# `local_options`

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

Returns all buffer-local option overrides (not the resolved values,
just the overrides set on this buffer).

# `move`

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

Moves the cursor in the given direction.

# `move_cursor`

```elixir
@spec move_cursor(GenServer.server(), :up | :down | :left | :right) :: :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 `apply_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.

# `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.Server.remap_face(buf, "default", fg: 0x000000, bg: 0xFFFFFF)
    Buffer.Server.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, get_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_force`

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

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

# `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.

# `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_cursor`

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

Sets the cursor to an absolute position. Clamped to buffer bounds.

# `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 `apply_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.

# `undo`

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

Undoes the last mutation, restoring the previous buffer state.

# `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*
