# `MingaEditor.RenderModel.Window.SourceOffsetMap`
[🔗](https://github.com/jsmestad/minga/blob/main/lib/minga_editor/render_model/window/source_offset_map.ex#L1)

Maps source UTF-8 byte boundaries to composed UTF-16 boundaries and back.

The map stores one span per composition edit plus the unchanged source spans
between edits. It does not allocate an entry per grapheme, so an unmodified
long line has one span.

# `affinity`

```elixir
@type affinity() :: :start | :end
```

# `insertion`

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

# `replacement`

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

# `span`

```elixir
@type span() ::
  {span_kind(), non_neg_integer(), non_neg_integer(), non_neg_integer(),
   non_neg_integer()}
```

# `span_kind`

```elixir
@type span_kind() :: :source | :insertion | :replacement
```

# `t`

```elixir
@type t() :: %MingaEditor.RenderModel.Window.SourceOffsetMap{
  composed_end_utf16: non_neg_integer(),
  composed_start_utf16: non_neg_integer(),
  insertions: [insertion()],
  replacements: [replacement()],
  source_end_byte: non_neg_integer(),
  source_start_byte: non_neg_integer(),
  source_text: String.t(),
  spans: [span()]
}
```

# `composed_utf16_to_source_byte`

```elixir
@spec composed_utf16_to_source_byte(t(), non_neg_integer(), affinity()) ::
  non_neg_integer()
```

Maps a composed UTF-16 boundary to a source UTF-8 byte boundary.

Inserted text resolves to its source anchor. Inside replacement text,
`:start` resolves to the replaced range start and `:end` resolves to its end.
Inside a UTF-16 surrogate pair, affinity chooses the preceding or following
source boundary. Offsets outside the line are clamped.

# `composed_utf16_to_source_bytes`

```elixir
@spec composed_utf16_to_source_bytes(t(), [{non_neg_integer(), affinity()}]) :: [
  non_neg_integer()
]
```

Resolves nondecreasing composed boundaries in one forward scan.

This is the wrapping path for long lines. It preserves the semantics of
`composed_utf16_to_source_byte/3` without rescanning an unchanged source
prefix for every visual row boundary.

# `new`

```elixir
@spec new(String.t(), String.t(), Minga.Core.Decorations.t(), non_neg_integer()) ::
  t()
```

Creates a map for composition without tab expansion.

# `new`

```elixir
@spec new(
  String.t(),
  String.t(),
  Minga.Core.Decorations.t(),
  non_neg_integer(),
  keyword()
) :: t()
```

Creates a map for one composed logical line.

`:tab_width` records source tabs as replacements expanded to the next tab
stop. Omit it when `composed_text` retains raw tabs.

# `slice`

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

# `source_to_composed_utf16`

```elixir
@spec source_to_composed_utf16(t(), non_neg_integer(), affinity()) ::
  non_neg_integer()
```

Maps a source byte boundary to a composed UTF-16 boundary.

At an insertion anchor, `:start` returns the boundary after inserted text and
`:end` returns the boundary before it. This keeps inserted presentation out
of source-backed accessibility ranges.

---

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