# `Minga.Editing.Search`
[🔗](https://github.com/jsmestad/minga/blob/main/lib/minga/editing/search.ex#L1)

Pure search functions for the Minga editor.

Provides substring search over buffer content. All functions are pure —
they take content strings or line lists and return positions. No buffer
or process state is mutated.

All positions use byte-indexed columns.

## Match representation

A match is a `{line, byte_col, byte_length}` tuple where `line` and
`byte_col` are zero-indexed.

# `direction`

```elixir
@type direction() :: :forward | :backward
```

Search direction.

# `match`

```elixir
@type match() :: Minga.Editing.Search.Match.t()
```

A search match with line, byte column, and byte length.

# `position`

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

A zero-indexed cursor position.

# `replacement_span`

```elixir
@type replacement_span() :: {non_neg_integer(), non_neg_integer()}
```

A replacement span: `{byte_col, byte_length}` in the substituted line.

# `substitute_result`

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

Result of a substitution: new content and count of replacements.

# `find_all_in_range`

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

Finds all occurrences of `pattern` in `lines` (a list of line strings).

`first_line` is the buffer line number of the first element in `lines`,
used to compute absolute line numbers in the returned matches.

Returns a list of `Search.Match` structs.

## Examples

    iex> Minga.Editing.Search.find_all_in_range(["foo bar foo", "baz foo"], "foo", 0)
    [%Minga.Editing.Search.Match{line: 0, col: 0, length: 3}, %Minga.Editing.Search.Match{line: 0, col: 8, length: 3}, %Minga.Editing.Search.Match{line: 1, col: 4, length: 3}]

# `find_next`

```elixir
@spec find_next(String.t(), String.t(), position(), direction()) :: position() | nil
```

Finds the next match for `pattern` starting from `cursor` in the given
`direction`. Wraps around the buffer if no match is found between cursor
and the end (or start for backward).

Returns `{line, byte_col}` of the match start, or `nil` if no match exists.

## Examples

    iex> Minga.Editing.Search.find_next("hello world\nhello again", "hello", {0, 1}, :forward)
    {1, 0}

    iex> Minga.Editing.Search.find_next("hello world\nhello again", "hello", {1, 0}, :backward)
    {0, 0}

    iex> Minga.Editing.Search.find_next("no match here", "xyz", {0, 0}, :forward)
    nil

# `substitute`

```elixir
@spec substitute(String.t(), String.t(), String.t(), boolean()) :: substitute_result()
```

Replaces occurrences of `pattern` with `replacement` in `content`.

When `global?` is `true`, replaces all occurrences. When `false`, replaces
only the first occurrence on each line (Vim `:s` default).

Returns `{new_content, replacement_count}`.

## Examples

    iex> Minga.Editing.Search.substitute("foo bar foo", "foo", "baz", true)
    {"baz bar baz", 2}

    iex> Minga.Editing.Search.substitute("foo bar foo", "foo", "baz", false)
    {"baz bar foo", 1}

    iex> Minga.Editing.Search.substitute("hello world", "xyz", "abc", true)
    {"hello world", 0}

# `substitute_line`

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

Substitutes occurrences of `pattern` with `replacement` in a single line.

When `global?` is `true`, replaces all occurrences. When `false`, replaces
only the first occurrence.

Returns `{new_line, replacement_count}`.

# `substitute_line_with_spans`

```elixir
@spec substitute_line_with_spans(String.t(), String.t(), String.t(), boolean()) ::
  {String.t(), non_neg_integer(), [replacement_span()]}
```

Like `substitute_line/4` but also returns the column spans of the
replacement text in the resulting line, for highlighting.

Returns `{new_line, replacement_count, spans}`.

## Examples

    iex> Minga.Editing.Search.substitute_line_with_spans("foo bar foo", "foo", "hello", true)
    {"hello bar hello", 2, [{0, 5}, {10, 5}]}

# `word_at_cursor`

```elixir
@spec word_at_cursor(Minga.Buffer.Document.t(), position()) :: String.t() | nil
```

Returns the word under the cursor in the gap buffer, or `nil` if the
cursor is not on a word character.

A word character is alphanumeric or underscore (matching Vim's `\<word\>`).

## Examples

    iex> buf = Minga.Buffer.Document.new("hello world")
    iex> Minga.Editing.Search.word_at_cursor(buf, {0, 0})
    "hello"

    iex> buf = Minga.Buffer.Document.new("hello world")
    iex> Minga.Editing.Search.word_at_cursor(buf, {0, 5})
    nil

---

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