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

A gap buffer implementation for text editing.

The gap buffer stores text as two binaries: `before` contains all text
before the cursor (in natural order), and `after` contains all text
after the cursor (in natural order). The "gap" is the conceptual space
between them where insertions happen in O(1).

Moving the cursor shifts characters between the two binaries. This gives
O(1) insertions and deletions at the cursor, and O(n) cursor movement
(where n is the distance moved). For an interactive editor where the
cursor moves incrementally, this is ideal.

## Byte-indexed positions

All positions are zero-indexed `{line, byte_col}` tuples, where `byte_col`
is the byte offset within the line. For ASCII content (the common case),
byte offset equals grapheme index. For multi-byte UTF-8 characters,
byte offset is larger (e.g., `é` = 2 bytes, emoji = 4+ bytes).

This representation enables O(1) `binary_part` slicing throughout the
editor and aligns with tree-sitter's byte-offset model.

Use `grapheme_col/2` to convert a byte-indexed position to a display
column for rendering.

## Examples

    iex> buf = Minga.Buffer.Document.new("hello\nworld")
    iex> Minga.Buffer.Document.cursor(buf)
    {0, 0}
    iex> buf = Minga.Buffer.Document.insert_char(buf, "H")
    iex> Minga.Buffer.Document.content(buf)
    "Hhello\nworld"

# `direction`

```elixir
@type direction() :: :left | :right | :up | :down
```

A direction for cursor movement.

# `line_offsets`

```elixir
@type line_offsets() :: tuple() | nil
```

Cached line offset tuple, or `nil` when stale.

# `position`

```elixir
@type position() :: {line :: non_neg_integer(), byte_col :: non_neg_integer()}
```

A zero-indexed `{line, byte_col}` position in the buffer.

`byte_col` is the byte offset within the line's UTF-8 binary.
For ASCII text, this equals the character/grapheme index.

# `t`

```elixir
@type t() :: %Minga.Buffer.Document{
  after: String.t(),
  before: String.t(),
  cursor_col: non_neg_integer(),
  cursor_line: non_neg_integer(),
  line_count: pos_integer(),
  line_offsets: line_offsets()
}
```

A gap buffer instance.

# `byte_col_for_grapheme`

```elixir
@spec byte_col_for_grapheme(String.t(), non_neg_integer()) :: non_neg_integer()
```

Converts a grapheme column to a byte column for the given line.

Walks graphemes until `grapheme_index` graphemes have been counted,
returning the byte offset at that point. Used by motions that need
to reason about character positions.

# `clear_line`

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

Clears all content on the given line, leaving an empty line.
Returns `{yanked_text, new_buffer}` where `yanked_text` is the text
that was on the line. The cursor is placed at column 0 of the line.

# `content`

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

Returns the full text content of the buffer.

# `content_and_cursor`

```elixir
@spec content_and_cursor(t()) :: {String.t(), position()}
```

Returns the content and cursor position in a single call,
avoiding separate content/1 + cursor/1 round-trips.

# `content_range`

```elixir
@spec content_range(t(), position(), position()) :: String.t()
```

Returns the text between two positions **inclusive** on both ends.
If the positions are reversed, they are normalised automatically.

# `cursor`

```elixir
@spec cursor(t()) :: position()
```

Returns the current cursor position as a `{line, byte_col}` tuple.

# `cursor_offset`

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

Returns the byte offset of the cursor in the full text.

# `delete_at`

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

Deletes the character at the cursor (delete forward).
Returns the buffer unchanged if the cursor is at the end.

# `delete_before`

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

Deletes the character before the cursor (backspace).
Returns the buffer unchanged if the cursor is at the beginning.

## Examples

    iex> buf = Minga.Buffer.Document.new("hello")
    iex> buf = Minga.Buffer.Document.move_to(buf, {0, 5})
    iex> buf = Minga.Buffer.Document.delete_before(buf)
    iex> Minga.Buffer.Document.content(buf)
    "hell"

# `delete_lines`

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

Deletes lines [start_line, end_line] inclusive from the buffer.

The cursor is placed at the beginning of the line that now occupies
the start position (or the last remaining line if fewer lines remain).

# `delete_range`

```elixir
@spec delete_range(t(), position(), position()) :: t()
```

Deletes the text between two positions **inclusive** on both ends.
If the positions are reversed, they are normalised automatically.
The cursor is placed at the earlier position.

# `empty?`

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

Returns true if the buffer contains no text.

## Examples

    iex> Minga.Buffer.Document.empty?(Minga.Buffer.Document.new(""))
    true
    iex> Minga.Buffer.Document.empty?(Minga.Buffer.Document.new("hi"))
    false

# `get_lines_content`

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

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

# `get_range`

```elixir
@spec get_range(t(), position(), position()) :: String.t()
```

Returns the text in the range [start_pos, end_pos] inclusive (characterwise).

Positions are clamped to valid buffer bounds. If start_pos is after end_pos,
the positions are swapped automatically.

# `grapheme_col`

```elixir
@spec grapheme_col(t(), position()) :: non_neg_integer()
```

Converts a `{line, byte_col}` position to a grapheme (display) column.

Counts graphemes in the line text from byte 0 to `byte_col`.
Used by the renderer to convert byte positions to screen columns.

# `insert_char`

```elixir
@spec insert_char(t(), String.t()) :: t()
```

Inserts a character (or string) at the cursor position.

## Examples

    iex> buf = Minga.Buffer.Document.new("world")
    iex> buf = Minga.Buffer.Document.insert_char(buf, "hello ")
    iex> Minga.Buffer.Document.content(buf)
    "hello world"

# `insert_text`

```elixir
@spec insert_text(t(), String.t()) :: t()
```

Inserts a multi-character string at the cursor position in a single
binary operation. Use this instead of decomposing into graphemes and
calling `insert_char/2` in a loop; that pattern is O(n²) on the gap
buffer's binary.

Functionally equivalent to `insert_char/2` (which already accepts
arbitrary strings), but exists as a separate entry point so the intent
is clear and `Buffer.Server` can route bulk inserts here directly.

## Examples

    iex> buf = Minga.Buffer.Document.new("world")
    iex> buf = Minga.Buffer.Document.insert_text(buf, "hello ")
    iex> Minga.Buffer.Document.content(buf)
    "hello world"

    iex> buf = Minga.Buffer.Document.new("end")
    iex> buf = Minga.Buffer.Document.insert_text(buf, "line1\nline2\n")
    iex> Minga.Buffer.Document.content(buf)
    "line1\nline2\nend"
    iex> Minga.Buffer.Document.cursor(buf)
    {2, 0}

# `last_grapheme_byte_offset`

```elixir
@spec last_grapheme_byte_offset(String.t()) :: non_neg_integer()
```

Returns the byte offset of the first byte of the last grapheme in `text`.
Returns 0 for empty strings.

# `line_at`

```elixir
@spec line_at(t(), non_neg_integer()) :: String.t() | nil
```

Returns the text of a specific line (zero-indexed), without the trailing newline.
Returns `nil` if the line number is out of range.

# `line_count`

```elixir
@spec line_count(t()) :: pos_integer()
```

Returns the total number of lines in the buffer.

An empty buffer counts as one line.

## Examples

    iex> buf = Minga.Buffer.Document.new("one\ntwo\nthree")
    iex> Minga.Buffer.Document.line_count(buf)
    3
    iex> Minga.Buffer.Document.line_count(Minga.Buffer.Document.new(""))
    1

# `lines`

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

Returns a range of lines (zero-indexed, inclusive start, exclusive end).

# `move`

```elixir
@spec move(t(), direction()) :: t()
```

Moves the cursor one step in the given direction.

# `move_to`

```elixir
@spec move_to(t(), position()) :: t()
```

Moves the cursor to an exact `{line, byte_col}` position.

Line and column are clamped to valid buffer bounds.

## Examples

    iex> buf = Minga.Buffer.Document.new("hello\nworld")
    iex> buf = Minga.Buffer.Document.move_to(buf, {1, 3})
    iex> Minga.Buffer.Document.cursor(buf)
    {1, 3}

# `new`

```elixir
@spec new(String.t()) :: t()
```

Creates a new gap buffer from a string. Cursor starts at `{0, 0}`.

## Examples

    iex> buf = Minga.Buffer.Document.new("hello")
    iex> Minga.Buffer.Document.content(buf)
    "hello"
    iex> Minga.Buffer.Document.cursor(buf)
    {0, 0}

# `offset_to_position`

```elixir
@spec offset_to_position(t(), non_neg_integer()) :: position()
```

Converts a byte offset in the buffer content to a `{line, byte_col}` position.
Clamps to valid bounds.

# `position_to_offset`

```elixir
@spec position_to_offset(t(), position()) :: non_neg_integer()
```

Returns the byte offset of a `{line, byte_col}` position in the buffer content.

---

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