fix: apply addStyle after all text mutations in OutputTransformation

TextFieldBuffer.addStyle() positions are not adjusted by subsequent
replace() calls. When multiple mentions had different-length display
names, styles for later mentions became misaligned. Split into two
phases: all replace() calls first, then all addStyle() calls with
cumulative-shift-corrected positions.

https://claude.ai/code/session_01SSsxsfJLbRiesBBhQFEVUd
This commit is contained in:
Claude
2026-04-01 19:15:47 +00:00
parent b2be345979
commit 09fb10220b
@@ -41,6 +41,11 @@ class UrlUserTagOutputTransformation(
val mentionRegex = Regex("(?:@|nostr:)(?:npub1[a-z0-9]{58}|nprofile1[a-z0-9]+)")
val matches = mentionRegex.findAll(text).toList().reversed()
// Phase 1: Replace all mentions (reverse order keeps indices valid for replace).
// Collect replacement info because addStyle must be called after all text mutations.
// (originalStart, originalMatchLength, displayNameLength)
val replacements = mutableListOf<Triple<Int, Int, Int>>()
for (match in matches) {
try {
val bech32 =
@@ -52,18 +57,22 @@ class UrlUserTagOutputTransformation(
val displayName = "@${user.toBestDisplayName()}"
replace(match.range.first, match.range.last + 1, displayName)
// Apply color styling to the replaced display name
addStyle(
SpanStyle(color = color, textDecoration = TextDecoration.None),
match.range.first,
match.range.first + displayName.length,
)
replacements.add(Triple(match.range.first, match.range.last + 1 - match.range.first, displayName.length))
} catch (e: Exception) {
if (e is CancellationException) throw e
}
}
// Phase 2: Apply styles after all text mutations are finalized.
// Iterate in forward document order, tracking cumulative shift from prior replacements.
val style = SpanStyle(color = color, textDecoration = TextDecoration.None)
var cumulativeShift = 0
for ((originalStart, originalLen, newLen) in replacements.reversed()) {
val adjustedStart = originalStart + cumulativeShift
addStyle(style, adjustedStart, adjustedStart + newLen)
cumulativeShift += newLen - originalLen
}
// Highlight URLs in remaining text
highlightUrls(color)
}