# `MobDev.Plugin.ManagedBlock`
[🔗](https://github.com/genericjam/mob_dev/blob/master/lib/mob_dev/plugin/managed_block.ex#L1)

Reversible insertion of plugin-contributed fragments into host-owned build
files (`AndroidManifest.xml`, `build.gradle`).

The problem: plugin permissions, AndroidManifest `<application>` components,
and Gradle dependencies are spliced into files the host also hand-edits. A
plain "append if absent" merge can't be undone — when a plugin is removed its
lines linger (a dangling `<service>` whose class is gone, an orphan permission
/ dep). Unlike the copied artifacts (`bridge_kt`, `res_files`) there's no
ledger to prune, because the target file is shared and hand-authored.

The fix: fence each managed region with begin/end marker comments and
**regenerate the whole region every build** from the current plugin set. A
removed plugin simply isn't in the fresh region, so its lines disappear;
anything the host wrote outside the fence is never touched. Re-running with an
unchanged plugin set is idempotent.

`upsert/4` is pure: `(content, markers, body, place) -> content'`. `markers`
is `{begin_line, end_line}` (the exact comment lines, comment syntax chosen by
the caller so it works for both XML and Gradle). `place` is
`(stripped_content, region) -> content'` — it inserts the freshly built region
at the file-specific anchor (using `insert_before/3` or `insert_before_index/3`
so the region occupies whole lines and `strip/2` reverses it exactly). An
empty `body` removes the region entirely.

Idempotence rests on `insert_before*` placing the region as complete lines at
a line boundary and `strip/2` removing exactly those lines — so
`strip(place(x)) == x`, and re-running with an unchanged plugin set is a fixed
point (verified in the tests).

# `markers`

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

# `insert_before`

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

Insert `region` as whole lines immediately before the line containing the
first occurrence of `anchor` (returns `content` unchanged if `anchor` is
absent). Pairs with `strip/2` for an exact round-trip.

# `insert_before_index`

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

Insert `region` as whole lines immediately before the line containing byte
index `idx`.

# `strip`

```elixir
@spec strip(String.t(), markers()) :: String.t()
```

Remove every managed region delimited by `markers` (the full lines the begin
and end markers sit on, inclusive), or return `content` unchanged when no
well-formed region is present.

Each region is identified as **the last BEGIN before the first END**, never
"first BEGIN → first END". That distinction matters: with a stray/duplicate
BEGIN (an interrupted write, a hand-edit, a bad merge-conflict resolution),
"first BEGIN → first END" would delete everything from the orphan through the
real region's END — silently eating host-authored lines in between. Anchoring
on the last BEGIN before the first END guarantees the removed span contains no
other marker, so only the region itself is deleted. Loops until no well-formed
region remains, so duplicates are all cleared.

# `upsert`

```elixir
@spec upsert(String.t(), markers(), String.t(), (String.t(), String.t() -&gt; String.t())) ::
  String.t()
```

Replace (or remove) the managed region delimited by `markers` in `content`.

Strips any existing region first (so the build is the single source of truth
for it), then — if `body` is non-empty — rebuilds it as
`begin <> "\n" <> body <> "\n" <> end` and hands it to `place` to insert.
An empty `body` leaves the content with the region stripped (the removal case).

---

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