mirror of
https://github.com/carlrobertoh/ProxyAI.git
synced 2026-05-19 16:28:46 +00:00
fix: code block handling containing nested backticks (#1144)
Previously there was an issue where if a code block contained nested triple-backticks, it would end the code block, causing the message window to become horribly mangled. This fixes the issue by only considering a triple-backtick to be an end to the code block if the triple-backticks are at the same indention level which started the code block.
This commit is contained in:
parent
14937e8a2a
commit
debb573bc9
2 changed files with 71 additions and 19 deletions
|
|
@ -12,7 +12,7 @@ class SseMessageParser : MessageParser {
|
|||
const val NEWLINE = "\n"
|
||||
const val HEADER_DELIMITER = ":"
|
||||
const val HEADER_PARTS_LIMIT = 2
|
||||
|
||||
|
||||
val SEARCH_START_REGEX = Regex("""^\s*<{3,}(\s*SEARCH.*)?$""", RegexOption.IGNORE_CASE)
|
||||
val SEPARATOR_REGEX = Regex("""^\s*={3,}\s*$""")
|
||||
val REPLACE_END_REGEX = Regex("""^\s*>{3,}(\s*REPLACE.*)?$""", RegexOption.IGNORE_CASE)
|
||||
|
|
@ -62,9 +62,16 @@ class SseMessageParser : MessageParser {
|
|||
|
||||
return when {
|
||||
shouldProcessCodeFence(fenceIdx, thinkStartIdx) -> {
|
||||
extractTextBeforeIndex(fenceIdx)?.let { segments.add(it) }
|
||||
val textBeforeFence = buffer.substring(0, fenceIdx)
|
||||
val lineStartIdx = textBeforeFence.lastIndexOf(NEWLINE) + 1
|
||||
val indentation = textBeforeFence.substring(lineStartIdx)
|
||||
|
||||
// Emit text that comes before the line with the code fence.
|
||||
extractTextBeforeIndex(lineStartIdx)?.let { segments.add(it) }
|
||||
|
||||
consumeFromBuffer(fenceIdx + CODE_FENCE.length)
|
||||
parserState = ParserState.CodeHeaderWaiting()
|
||||
// Transition to CodeHeaderWaiting with the captured indentation.
|
||||
parserState = ParserState.CodeHeaderWaiting(indentation = indentation)
|
||||
true
|
||||
}
|
||||
|
||||
|
|
@ -94,11 +101,11 @@ class SseMessageParser : MessageParser {
|
|||
|
||||
return if (header != null) {
|
||||
segments.add(header)
|
||||
parserState = ParserState.InCode(header)
|
||||
parserState = ParserState.InCode(header, indentation = state.indentation)
|
||||
true
|
||||
} else {
|
||||
segments.add(CodeHeaderWaiting(updatedHeader))
|
||||
parserState = ParserState.CodeHeaderWaiting(updatedHeader)
|
||||
parserState = ParserState.CodeHeaderWaiting(updatedHeader, state.indentation)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
|
@ -114,7 +121,7 @@ class SseMessageParser : MessageParser {
|
|||
consumeFromBuffer(nlIdx + 1)
|
||||
|
||||
return when {
|
||||
line.trim() == CODE_FENCE -> {
|
||||
line.trimEnd() == state.indentation + CODE_FENCE -> {
|
||||
if (state.content.isNotEmpty()) {
|
||||
segments.add(Code(state.content, state.header.language, state.header.filePath))
|
||||
}
|
||||
|
|
@ -129,14 +136,14 @@ class SseMessageParser : MessageParser {
|
|||
segments.add(Code(state.content, state.header.language, state.header.filePath))
|
||||
}
|
||||
segments.add(SearchWaiting("", state.header.language, state.header.filePath))
|
||||
parserState = ParserState.InSearch(state.header, "")
|
||||
parserState = ParserState.InSearch(state.header, "", state.indentation)
|
||||
true
|
||||
}
|
||||
|
||||
else -> {
|
||||
val newContent =
|
||||
if (state.content.isEmpty()) line else state.content + NEWLINE + line
|
||||
parserState = ParserState.InCode(state.header, newContent)
|
||||
parserState = ParserState.InCode(state.header, newContent, state.indentation)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
@ -161,13 +168,14 @@ class SseMessageParser : MessageParser {
|
|||
state.header.filePath
|
||||
)
|
||||
)
|
||||
parserState = ParserState.InReplace(state.header, state.searchContent, "")
|
||||
parserState =
|
||||
ParserState.InReplace(state.header, state.searchContent, "", state.indentation)
|
||||
true
|
||||
} else {
|
||||
val newSearch =
|
||||
if (state.searchContent.isEmpty()) line else state.searchContent + NEWLINE + line
|
||||
segments.add(SearchWaiting(newSearch, state.header.language, state.header.filePath))
|
||||
parserState = ParserState.InSearch(state.header, newSearch)
|
||||
parserState = ParserState.InSearch(state.header, newSearch, state.indentation)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
|
@ -192,11 +200,11 @@ class SseMessageParser : MessageParser {
|
|||
filePath = state.header.filePath
|
||||
)
|
||||
)
|
||||
parserState = ParserState.InCode(state.header)
|
||||
parserState = ParserState.InCode(state.header, indentation = state.indentation)
|
||||
true
|
||||
}
|
||||
|
||||
line.trim() == CODE_FENCE -> {
|
||||
line.trimEnd() == state.indentation + CODE_FENCE -> {
|
||||
segments.add(CodeEnd(""))
|
||||
parserState = ParserState.Outside
|
||||
true
|
||||
|
|
@ -213,7 +221,12 @@ class SseMessageParser : MessageParser {
|
|||
state.header.filePath
|
||||
)
|
||||
)
|
||||
parserState = ParserState.InReplace(state.header, state.searchContent, newReplace)
|
||||
parserState = ParserState.InReplace(
|
||||
state.header,
|
||||
state.searchContent,
|
||||
newReplace,
|
||||
state.indentation
|
||||
)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
@ -257,7 +270,7 @@ class SseMessageParser : MessageParser {
|
|||
is ParserState.InCode -> {
|
||||
val segments = mutableListOf<Segment>()
|
||||
|
||||
if (buffer.toString().trim() == CODE_FENCE) {
|
||||
if (buffer.toString().trimEnd() == state.indentation + CODE_FENCE) {
|
||||
if (state.content.isNotBlank()) {
|
||||
segments.add(
|
||||
Code(state.content, state.header.language, state.header.filePath)
|
||||
|
|
@ -342,27 +355,32 @@ class SseMessageParser : MessageParser {
|
|||
return trimmed.startsWith(REPLACE_MARKER) || REPLACE_END_REGEX.matches(trimmed)
|
||||
}
|
||||
|
||||
// Step 1: Update parser states to hold indentation information.
|
||||
private sealed class ParserState {
|
||||
object Outside : ParserState()
|
||||
|
||||
data class CodeHeaderWaiting(
|
||||
val content: String = ""
|
||||
val content: String = "",
|
||||
val indentation: String = ""
|
||||
) : ParserState()
|
||||
|
||||
data class InCode(
|
||||
val header: CodeHeader,
|
||||
val content: String = ""
|
||||
val content: String = "",
|
||||
val indentation: String = ""
|
||||
) : ParserState()
|
||||
|
||||
data class InSearch(
|
||||
val header: CodeHeader,
|
||||
val searchContent: String = ""
|
||||
val searchContent: String = "",
|
||||
val indentation: String = ""
|
||||
) : ParserState()
|
||||
|
||||
data class InReplace(
|
||||
val header: CodeHeader,
|
||||
val searchContent: String,
|
||||
val replaceContent: String = ""
|
||||
val replaceContent: String = "",
|
||||
val indentation: String = ""
|
||||
) : ParserState()
|
||||
|
||||
data class InThinking(
|
||||
|
|
|
|||
|
|
@ -225,4 +225,38 @@ class SseMessageParserStreamTest {
|
|||
assertThat(finalCode).contains("println(\"Hello\")")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldHandleIndentedCodeBlockWithInnerBackticks() {
|
||||
val parser = SseMessageParser()
|
||||
val input = """
|
||||
Here is a code block:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val text = ""${'"'}
|
||||
```
|
||||
This should not end the block.
|
||||
```
|
||||
""${'"'}
|
||||
println(text)
|
||||
}
|
||||
```
|
||||
Done.
|
||||
""".trimIndent()
|
||||
|
||||
val segments = simulateStreaming(parser, input, seed = 42)
|
||||
|
||||
val codeSegments = segments.filterIsInstance<Code>()
|
||||
assertThat(codeSegments).isNotEmpty
|
||||
|
||||
val finalCodeContent = codeSegments.last().content
|
||||
assertThat(finalCodeContent).contains("This should not end the block.")
|
||||
assertThat(finalCodeContent).contains("println(text)")
|
||||
|
||||
val textSegments = segments.filterIsInstance<Text>()
|
||||
assertThat(textSegments.any { it.content.contains("Done.") }).isTrue
|
||||
|
||||
val codeEndSegments = segments.filterIsInstance<CodeEnd>()
|
||||
assertThat(codeEndSegments).hasSize(1)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue