This commit is contained in:
7 files changed
+118
-9
No files matched your search
@@ -111,6 +111,8 @@ fun MarkdownText(
|
||||
code = block.code,
|
||||
language = block.language,
|
||||
onCopy = { /* clipboard handled internally */ },
|
||||
searchQuery = searchQuery,
|
||||
isCurrentMatch = isCurrentMatch,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,10 +10,16 @@ import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.m57.hermescontrol.theme.CodeTerminalText
|
||||
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
||||
import com.m57.hermescontrol.theme.SearchHighlightColors
|
||||
import com.m57.hermescontrol.theme.searchHighlightColors
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@@ -27,7 +33,10 @@ fun CodeBlockCard(
|
||||
language: String?,
|
||||
onCopy: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
searchQuery: String = "",
|
||||
isCurrentMatch: Boolean = false,
|
||||
) {
|
||||
val highlights = searchHighlightColors(LocalHermesStatusColors.current)
|
||||
val highlighted by produceState(
|
||||
initialValue = remember(code) { AnnotatedString(code) },
|
||||
key1 = code,
|
||||
@@ -47,7 +56,10 @@ fun CodeBlockCard(
|
||||
copyContentDescription = "Copy code",
|
||||
) {
|
||||
Text(
|
||||
text = highlighted,
|
||||
text =
|
||||
remember(highlighted, searchQuery, isCurrentMatch, highlights) {
|
||||
highlighted.withSearchHighlights(searchQuery, isCurrentMatch, highlights)
|
||||
},
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 13.sp,
|
||||
color = CodeTerminalText,
|
||||
@@ -59,3 +71,28 @@ fun CodeBlockCard(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Overlay search-hit spans on an already syntax-highlighted string, keeping its existing styles. */
|
||||
private fun AnnotatedString.withSearchHighlights(
|
||||
query: String,
|
||||
isCurrent: Boolean,
|
||||
highlights: SearchHighlightColors,
|
||||
): AnnotatedString {
|
||||
if (query.isEmpty()) return this
|
||||
val (bg, fg) =
|
||||
if (isCurrent) {
|
||||
highlights.currentSearchBackground to highlights.currentSearchForeground
|
||||
} else {
|
||||
highlights.searchBackground to highlights.searchForeground
|
||||
}
|
||||
return buildAnnotatedString {
|
||||
append(this@withSearchHighlights)
|
||||
var from = 0
|
||||
while (true) {
|
||||
val hit = text.indexOf(query, from, ignoreCase = true)
|
||||
if (hit < 0) break
|
||||
addStyle(SpanStyle(background = bg, color = fg), hit, hit + query.length)
|
||||
from = hit + query.length
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,9 @@ internal fun FullBleedAgentMessage(
|
||||
message: ChatMessage,
|
||||
searchQuery: String = "",
|
||||
isCurrentMatch: Boolean = false,
|
||||
reasoningSearchQuery: String = "",
|
||||
isCurrentReasoningMatch: Boolean = false,
|
||||
reasoningSearchOffset: Int = 0,
|
||||
showReasoning: Boolean = true,
|
||||
onOpenAttachment: (Attachment) -> Unit = {},
|
||||
onSaveAttachment: (Attachment) -> Unit = {},
|
||||
@@ -91,6 +94,9 @@ internal fun FullBleedAgentMessage(
|
||||
ReasoningCard(
|
||||
reasoningText = message.reasoningText,
|
||||
isStreaming = message.isStreaming,
|
||||
searchQuery = reasoningSearchQuery,
|
||||
isCurrentMatch = isCurrentReasoningMatch,
|
||||
searchOffset = reasoningSearchOffset,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
}
|
||||
|
||||
@@ -426,6 +426,24 @@ fun FullBleedChatList(
|
||||
searchState.currentIndex,
|
||||
) ==
|
||||
SearchTarget.CONTENT,
|
||||
reasoningSearchQuery =
|
||||
if (searchState.isActive &&
|
||||
proseMessage.id in searchState.matchedIds
|
||||
) {
|
||||
searchState.query
|
||||
} else {
|
||||
""
|
||||
},
|
||||
isCurrentReasoningMatch =
|
||||
searchState.currentMatchId == proseMessage.id &&
|
||||
searchState.matchTargets.getOrNull(
|
||||
searchState.currentIndex,
|
||||
) ==
|
||||
SearchTarget.REASONING,
|
||||
reasoningSearchOffset =
|
||||
searchState.matchOffsets.getOrElse(
|
||||
searchState.currentIndex,
|
||||
) { 0 },
|
||||
showReasoning = !hoistedReasoning,
|
||||
onOpenAttachment = actions.onOpenAttachment,
|
||||
onSaveAttachment = actions.onSaveAttachment,
|
||||
|
||||
@@ -197,7 +197,14 @@ fun searchMatchToLazyIndex(
|
||||
SearchTarget.REASONING -> "reasoning"
|
||||
SearchTarget.TOOL -> "tool"
|
||||
}
|
||||
return fullBleedItemKeys(turns).indexOf("$prefix-${message.id}").takeIf { it >= 0 }
|
||||
val keys = fullBleedItemKeys(turns)
|
||||
keys.indexOf("$prefix-${message.id}").takeIf { it >= 0 }?.let { return it }
|
||||
// Non-hoisted reasoning renders inside its message's prose row, not as its own row.
|
||||
return if (match.target == SearchTarget.REASONING) {
|
||||
keys.indexOf("prose-${message.id}").takeIf { it >= 0 }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/** Lazy row identities, including hoisted reasoning and grouped tool/system entries. */
|
||||
|
||||
@@ -102,7 +102,7 @@ object MarkdownInlineStyler {
|
||||
background = textColor.copy(alpha = 0.08f),
|
||||
),
|
||||
) {
|
||||
append(content)
|
||||
appendSearchable(content, searchQuery, searchHighlightColor)
|
||||
}
|
||||
i = matchingEnd + runLength
|
||||
} else {
|
||||
@@ -118,7 +118,7 @@ object MarkdownInlineStyler {
|
||||
val raw = src.substring(i + 3, end)
|
||||
val content = if (isRtl) BidiUtils.wrapLtrIsolate(raw) else raw
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic)) {
|
||||
append(content)
|
||||
appendSearchable(content, searchQuery, searchHighlightColor)
|
||||
}
|
||||
i = end + 3
|
||||
} else {
|
||||
@@ -161,7 +161,7 @@ object MarkdownInlineStyler {
|
||||
val raw = src.substring(i + 2, end)
|
||||
val toAppend = if (isRtl) BidiUtils.wrapLtrIsolate(raw) else raw
|
||||
withStyle(SpanStyle(textDecoration = TextDecoration.LineThrough)) {
|
||||
append(toAppend)
|
||||
appendSearchable(toAppend, searchQuery, searchHighlightColor)
|
||||
}
|
||||
i = end + 2
|
||||
} else {
|
||||
@@ -201,7 +201,7 @@ object MarkdownInlineStyler {
|
||||
val raw = src.substring(i + 2, end)
|
||||
val toAppend = if (isRtl) BidiUtils.wrapLtrIsolate(raw) else raw
|
||||
withStyle(SpanStyle(background = highlights.markupBackground)) {
|
||||
append(toAppend)
|
||||
appendSearchable(toAppend, searchQuery, searchHighlightColor)
|
||||
}
|
||||
i = end + 2
|
||||
} else {
|
||||
@@ -215,7 +215,7 @@ object MarkdownInlineStyler {
|
||||
val end = src.indexOf('^', i + 1)
|
||||
if (end != -1 && end > i + 1) {
|
||||
withStyle(SpanStyle(baselineShift = BaselineShift.Superscript)) {
|
||||
append(src.substring(i + 1, end))
|
||||
appendSearchable(src.substring(i + 1, end), searchQuery, searchHighlightColor)
|
||||
}
|
||||
i = end + 1
|
||||
} else {
|
||||
@@ -229,7 +229,7 @@ object MarkdownInlineStyler {
|
||||
val end = src.indexOf('~', i + 1)
|
||||
if (end != -1 && end > i + 1) {
|
||||
withStyle(SpanStyle(baselineShift = BaselineShift.Subscript)) {
|
||||
append(src.substring(i + 1, end))
|
||||
appendSearchable(src.substring(i + 1, end), searchQuery, searchHighlightColor)
|
||||
}
|
||||
i = end + 1
|
||||
} else {
|
||||
@@ -250,7 +250,7 @@ object MarkdownInlineStyler {
|
||||
background = textColor.copy(alpha = 0.12f),
|
||||
),
|
||||
) {
|
||||
append(toAppend)
|
||||
appendSearchable(toAppend, searchQuery, searchHighlightColor)
|
||||
}
|
||||
i = end + 6
|
||||
} else {
|
||||
@@ -382,4 +382,27 @@ object MarkdownInlineStyler {
|
||||
}
|
||||
|
||||
private fun isRtr(isRtl: Boolean): Boolean = isRtl
|
||||
|
||||
/** Append [text], highlighting every [query] hit so styled spans (inline code, sub/sup, ...) stay searchable. */
|
||||
private fun AnnotatedString.Builder.appendSearchable(
|
||||
text: String,
|
||||
query: String,
|
||||
highlight: Pair<Color, Color>,
|
||||
) {
|
||||
if (query.isEmpty()) {
|
||||
append(text)
|
||||
return
|
||||
}
|
||||
var from = 0
|
||||
while (from < text.length) {
|
||||
val hit = text.indexOf(query, from, ignoreCase = true)
|
||||
if (hit < 0) break
|
||||
append(text.substring(from, hit))
|
||||
withStyle(SpanStyle(background = highlight.first, color = highlight.second)) {
|
||||
append(text.substring(hit, hit + query.length))
|
||||
}
|
||||
from = hit + query.length
|
||||
}
|
||||
append(text.substring(from))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user