# `Sagents.TextLines`

Pure helper for line-number math on text content.

Shared by file system tools, document editing tools, and any other
surface that needs consistent line-numbered text operations.
All line numbers are **1-indexed**.

The line-numbered format uses right-aligned 6-char numbers with a tab separator:

    "     1	First line of content"
    "     2	"
    "     3	Third line"

# `count_occurrences`

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

Counts occurrences of `old_string` in `body`.

# `find`

```elixir
@spec find(String.t() | nil, String.t(), keyword()) ::
  {:ok, [map()], boolean()} | {:error, String.t()}
```

Finds matches of `pattern` in `body`, returning structured results
with line numbers and optional context.

Options:
  - `:regex` - treat pattern as regex (default false)
  - `:case_sensitive` - (default true)
  - `:context_lines` - lines of context before/after each match (default 2)
  - `:max_matches` - cap on returned matches (default 20)

Returns `{:ok, matches, truncated?}` or `{:error, reason}`.
Each match is `%{line_number: n, line: text, context_before: [...], context_after: [...]}`.

# `render`

```elixir
@spec render(
  String.t() | nil,
  keyword()
) :: {String.t(), pos_integer(), pos_integer(), pos_integer()}
```

Renders lines with right-aligned 6-char line numbers and a tab separator.

Options:
  - `:start_line` - 1-indexed start line (default 1)
  - `:limit`      - max lines to return (default: all)

Returns `{formatted_string, start_line, end_line, total_lines}`.

# `replace_range`

```elixir
@spec replace_range(String.t() | nil, pos_integer(), pos_integer(), String.t()) ::
  {:ok, String.t(), non_neg_integer()} | {:error, String.t()}
```

Replaces lines `start_line..end_line` (1-indexed, inclusive) with
`new_lines_text` and returns the rejoined body.

`new_lines_text` semantics:
  - `""` deletes the targeted range (zero lines inserted)
  - `"\n"` inserts exactly one blank line
  - A trailing `"\n"` on non-empty content is a line terminator, not an
    extra blank line: `"foo\n"` and `"foo"` both insert one line `"foo"`

Returns `{:ok, new_body, lines_replaced}` or `{:error, reason}`.

# `replace_text`

```elixir
@spec replace_text(String.t(), String.t(), String.t(), boolean()) ::
  {:ok, String.t(), pos_integer()} | {:error, String.t()}
```

Replaces `old_string` in `body`.

When `replace_all` is false, requires exactly one occurrence.
Returns `{:ok, new_body, replacement_count}` or `{:error, reason}`.

# `split`

```elixir
@spec split(String.t() | nil) :: {[String.t()], non_neg_integer()}
```

Splits `body` on newlines and returns the total line count.
An empty or nil body is treated as a single empty line.

---

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