fix: trim edge blank lines and skip full scan in highlight windowing

Audit follow-ups on the highlight context window:

- Blank lines sitting at the very start/end of a `context` tag were passed
  through untouched when a side was short enough not to be trimmed, so a
  highlight whose context began or ended with blank lines still rendered
  empty space above/below the quote. Trim the outer edges of the windowed
  passage (re-basing the marked range accordingly).
- `locate` enumerated every occurrence of the quote — a full context scan
  plus a list allocation — even in the common no-prefix case where only the
  first match is needed. Short-circuit to a single indexOf there.
- Clamp the returned marked range to the trimmed text length so it can never
  point past the end (e.g. a quote ending in trimmed whitespace).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApuEseGcFjUFqYoLhCuR91
This commit is contained in:
Claude
2026-07-28 21:11:37 +00:00
parent 89aea073ed
commit de86e54cf8
2 changed files with 27 additions and 9 deletions
@@ -84,17 +84,20 @@ data class HighlightQuote(
val trail = trimTrail(context.substring(endExclusive))
val quote = context.substring(start, endExclusive)
// Nothing to trim on either side: the original context is short enough to show whole.
if (!lead.trimmed && !trail.trimmed) {
return HighlightQuote(context, start until endExclusive)
}
val prefix = if (lead.trimmed) "$ELLIPSIS " else ""
val suffix = if (trail.trimmed) " $ELLIPSIS" else ""
val text = prefix + lead.text + quote + trail.text + suffix
val markStart = prefix.length + lead.text.length
return HighlightQuote(text, markStart until (markStart + quote.length))
// Drop any blank lines sitting at the very edges of the context — with the far text
// trimmed (or even when it isn't) they would otherwise render as empty space above or
// below the quote. Only the outer edges are touched; the whitespace framing the quote
// itself is preserved.
val raw = prefix + lead.text + quote + trail.text + suffix
val leadingBlank = raw.length - raw.trimStart().length
val text = raw.trim()
val markStart = (prefix.length + lead.text.length - leadingBlank).coerceAtLeast(0)
val markEnd = (markStart + quote.length).coerceAtMost(text.length)
return HighlightQuote(text, markStart until markEnd)
}
private class Side(
@@ -138,9 +141,13 @@ data class HighlightQuote(
highlight: String,
prefix: String?,
): Int? {
// The common case — no prefix to disambiguate with — needs only the first match, so
// don't scan the whole context enumerating every occurrence.
if (prefix.isNullOrBlank()) return context.indexOf(highlight).takeIf { it >= 0 }
val occurrences = occurrencesOf(context, highlight)
if (occurrences.isEmpty()) return null
if (occurrences.size == 1 || prefix.isNullOrBlank()) return occurrences.first()
if (occurrences.size == 1) return occurrences.first()
val tail = prefix.trimEnd()
return occurrences.firstOrNull { context.substring(0, it).trimEnd().endsWith(tail) }
@@ -114,6 +114,17 @@ class HighlightQuoteTest {
assertTrue(quote.text.length < context.length / 2, "expected windowing, got ${quote.text.length} of ${context.length}")
}
@Test
fun dropsBlankLinesAtTheEdgesOfAShortContext() {
// A context whose paragraph boundaries left blank lines at its very start and end would
// otherwise render as empty space above and below the quote.
val quote = HighlightQuote.of("Forward Secrecy", "\n\nForward Secrecy is nice.\n\n")
assertEquals("Forward Secrecy is nice.", quote.text)
assertEquals(0 until 15, quote.marked)
assertEquals("Forward Secrecy", quote.text.substring(quote.marked!!))
}
@Test
fun trimmingSnapsToWholeWordsSoNoWordIsCutInHalf() {
val lead = "alpha bravo charlie delta echo foxtrot ".repeat(20) // long, space-separated