Skip to content

Commit 855db60

Browse files
committed
fix(parser): handle indentation in code blocks #259
- Update regex to account for leading whitespace in code blocks. - Add support for indentation in code block parsing, ensuring proper handling of nested or indented code. - Add test case for code blocks within lists with varying indentation.
1 parent 2a96ef6 commit 855db60

File tree

4 files changed

+83
-22
lines changed

4 files changed

+83
-22
lines changed

core/src/main/kotlin/cc/unitmesh/devti/observer/TestAgentObserver.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package cc.unitmesh.devti.observer
22

3-
import cc.unitmesh.devti.gui.chat.message.ChatActionType
4-
import cc.unitmesh.devti.gui.sendToChatWindow
53
import cc.unitmesh.devti.provider.observer.AgentObserver
64
import cc.unitmesh.devti.util.relativePath
75
import com.intellij.execution.testframework.sm.runner.SMTRunnerEventsAdapter
@@ -27,8 +25,8 @@ class TestAgentObserver : AgentObserver, Disposable {
2725
}
2826

2927
private fun sendResult(test: SMTestProxy, project: Project, searchScope: GlobalSearchScope) {
30-
val sourceCode = test.getLocation(project, searchScope)
3128
runInEdt {
29+
val sourceCode = test.getLocation(project, searchScope)
3230
val psiElement = sourceCode?.psiElement
3331
val language = psiElement?.language?.displayName ?: ""
3432
val filepath = psiElement?.containingFile?.virtualFile?.relativePath(project) ?: ""

core/src/main/kotlin/cc/unitmesh/devti/util/parser/CodeFence.kt

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class CodeFence(
1616
val devinEndRegex = Regex("</devin>")
1717

1818
fun parse(content: String): CodeFence {
19-
val languageRegex = Regex("```([\\w#+ ]*)")
19+
val languageRegex = Regex("\\s*```([\\w#+ ]*)")
2020
val lines = content.lines()
2121

2222
val startMatch = devinStartRegex.find(content)
@@ -38,21 +38,28 @@ class CodeFence(
3838
var codeStarted = false
3939
var codeClosed = false
4040
var languageId: String? = null
41+
var codeIndentation = ""
4142
val codeBuilder = StringBuilder()
4243

4344
for (line in lines) {
4445
if (!codeStarted) {
45-
val matchResult: MatchResult? = languageRegex.find(line.trimStart())
46+
val trimmedLine = line.trimStart()
47+
val matchResult: MatchResult? = languageRegex.find(trimmedLine)
4648
if (matchResult != null) {
47-
val substring = matchResult.groups[1]?.value
49+
// Store the indentation to match it when looking for the closing fence
50+
codeIndentation = line.substring(0, line.length - trimmedLine.length)
51+
val substring = matchResult.groups[1]?.value?.trim()
4852
languageId = substring
4953
codeStarted = true
5054
}
51-
} else if (line.startsWith("```")) {
52-
codeClosed = true
53-
break
5455
} else {
55-
codeBuilder.append(line).append("\n")
56+
val trimmedLine = line.trimStart()
57+
if (trimmedLine == "```") {
58+
codeClosed = true
59+
break
60+
} else {
61+
codeBuilder.append(line).append("\n")
62+
}
5663
}
5764
}
5865

@@ -112,7 +119,7 @@ class CodeFence(
112119
}
113120

114121
val devinRegexBlock = Regex("(?<=^|\\n)```devin\\n([\\s\\S]*?)\\n```\\n")
115-
val normalCodeBlock = Regex("```([\\w#+ ]*)\\n")
122+
val normalCodeBlock = Regex("\\s*```([\\w#+ ]*)\\n")
116123

117124
fun preProcessDevinBlock(content: String): String {
118125
var currentContent = content
@@ -135,17 +142,21 @@ class CodeFence(
135142
}
136143

137144
private fun parseMarkdownContent(content: String, codeFences: MutableList<CodeFence>) {
138-
val languageRegex = Regex("```([\\w#+ ]*)")
145+
val languageRegex = Regex("\\s*```([\\w#+ ]*)")
139146
val lines = content.lines()
140147

141148
var codeStarted = false
142149
var languageId: String? = null
143150
val codeBuilder = StringBuilder()
144151
val textBuilder = StringBuilder()
152+
var codeIndentation = ""
145153

146-
for (line in lines) {
154+
for (i in lines.indices) {
155+
val line = lines[i]
147156
if (!codeStarted) {
148-
val matchResult = languageRegex.find(line.trimStart())
157+
// Check for code block start with any indentation
158+
val trimmedLine = line.trimStart()
159+
val matchResult = languageRegex.find(trimmedLine)
149160
if (matchResult != null) {
150161
if (textBuilder.isNotEmpty()) {
151162
val textBlock = CodeFence(
@@ -156,13 +167,20 @@ class CodeFence(
156167
textBuilder.clear()
157168
}
158169

159-
languageId = matchResult.groups[1]?.value
170+
// Store the indentation to match it when looking for the closing fence
171+
codeIndentation = line.substring(0, line.length - trimmedLine.length)
172+
languageId = matchResult.groups[1]?.value?.trim()
160173
codeStarted = true
161174
} else {
162175
textBuilder.append(line).append("\n")
163176
}
164177
} else {
165-
if (line.startsWith("```")) {
178+
// Check if this line contains the closing fence with the same or similar indentation
179+
val trimmedLine = line.trimStart()
180+
181+
// Allow for some flexibility in indentation for the closing fence
182+
// This helps with numbered lists where indentation might vary slightly
183+
if (trimmedLine == "```") {
166184
val codeContent = codeBuilder.trim().toString()
167185
val codeFence = CodeFence(
168186
findLanguage(languageId ?: "markdown"),
@@ -176,6 +194,7 @@ class CodeFence(
176194
codeBuilder.clear()
177195
codeStarted = false
178196
languageId = null
197+
codeIndentation = ""
179198
} else {
180199
codeBuilder.append(line).append("\n")
181200
}
Lines changed: 7 additions & 6 deletions
Loading

core/src/test/kotlin/cc/unitmesh/devti/parser/CodeFenceTest.kt

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,4 +333,47 @@ npm run dev
333333
)
334334
assertTrue(codeFences[0].isComplete)
335335
}
336+
337+
fun testShouldHandleCodeUnderListWithErrorIndent() {
338+
val content = """
339+
1. **通过代码设置属性**:
340+
你可以在你的插件代码或项目中的某个地方直接使用这段代码来设置属性。例如:
341+
342+
```java
343+
PropertiesComponent.getInstance().setValue("matterhorn.junie.untestedIdeAccepted", "true");
344+
```
345+
346+
这会将 `matterhorn.junie.untestedIdeAccepted` 的值设置为 `"true"`。
347+
348+
2. **通过 IDEA 的配置文件手动设置**:
349+
如果你不想通过代码来设置这个属性,你可以手动编辑 IDEA 的配置文件。IDEA 的全局属性通常存储在 `idea.properties` 文件中,或者在某些情况下存储在 `options` 目录下的 XML 文件中。
350+
```
351+
""".trimMargin()
352+
353+
val codeFences = CodeFence.parseAll(content)
354+
assertEquals(codeFences.size, 3)
355+
assertEquals(
356+
codeFences[0].text, """
357+
|1. **通过代码设置属性**:
358+
| 你可以在你的插件代码或项目中的某个地方直接使用这段代码来设置属性。例如:
359+
""".trimMargin()
360+
)
361+
assertTrue(codeFences[0].isComplete)
362+
363+
assertEquals(
364+
codeFences[1].text, """
365+
|PropertiesComponent.getInstance().setValue("matterhorn.junie.untestedIdeAccepted", "true");
366+
""".trimMargin()
367+
)
368+
assertEquals(codeFences[1].originLanguage, "java")
369+
370+
assertEquals(
371+
codeFences[2].text, """
372+
|这会将 `matterhorn.junie.untestedIdeAccepted` 的值设置为 `"true"`。
373+
|
374+
|2. **通过 IDEA 的配置文件手动设置**:
375+
| 如果你不想通过代码来设置这个属性,你可以手动编辑 IDEA 的配置文件。IDEA 的全局属性通常存储在 `idea.properties` 文件中,或者在某些情况下存储在 `options` 目录下的 XML 文件中。
376+
""".trimMargin()
377+
)
378+
}
336379
}

0 commit comments

Comments
 (0)