From de86e54cf8a3d3e6cc335a3ac789607ac3bce830 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 21:11:37 +0000 Subject: [PATCH] fix: trim edge blank lines and skip full scan in highlight windowing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01ApuEseGcFjUFqYoLhCuR91 --- .../model/highlights/HighlightQuote.kt | 25 ++++++++++++------- .../model/highlights/HighlightQuoteTest.kt | 11 ++++++++ 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/highlights/HighlightQuote.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/highlights/HighlightQuote.kt index f5653a632f..c54fcb2915 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/highlights/HighlightQuote.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/highlights/HighlightQuote.kt @@ -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) } diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/highlights/HighlightQuoteTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/highlights/HighlightQuoteTest.kt index 8f2858d989..076d7d9c48 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/highlights/HighlightQuoteTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/highlights/HighlightQuoteTest.kt @@ -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