MobDev.Plugin.ManagedBlock (mob_dev v0.6.22)

Copy Markdown View Source

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).

Summary

Functions

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 region as whole lines immediately before the line containing byte index idx.

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.

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

Types

markers()

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

Functions

insert_before(content, anchor, region)

@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(content, idx, region)

@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(content, markers)

@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(content, markers, body, place)

@spec upsert(String.t(), markers(), String.t(), (String.t(), String.t() -> 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).