Skip to content

Commit 54e36f8

Browse files
committed
refactor(plan-parser): simplify plan parsing logic and UI borders #331
- Replace XML `<THOUGHT>` tags with code-fence blocks for plan content. - Simplify UI borders in `ThoughtPlanSketchProvider` by using `JBUI.Borders.empty()`. - Refactor `MarkdownPlanParser` to handle flat ordered lists and improve section processing. - Add test case for parsing markdown with single section and tasks.
1 parent 5ccc75b commit 54e36f8

File tree

5 files changed

+107
-54
lines changed

5 files changed

+107
-54
lines changed

core/src/main/kotlin/cc/unitmesh/devti/sketch/ui/ThoughtPlanSketchProvider.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class PlanSketch(
5555
private fun createPlanUI() {
5656
planItems.forEachIndexed { index, planItem ->
5757
val titlePanel = JBPanel<JBPanel<*>>(FlowLayout(FlowLayout.LEFT)).apply {
58-
border = JBEmptyBorder(JBUI.insets(4, 0))
58+
border = JBUI.Borders.empty()
5959
}
6060

6161
val titleText = if (planItem.completed)
@@ -64,13 +64,13 @@ class PlanSketch(
6464
"<html><b>${index + 1}. ${planItem.title}</b></html>"
6565

6666
val sectionLabel = JLabel(titleText)
67-
sectionLabel.border = JBUI.Borders.empty(4, 0)
67+
sectionLabel.border = JBUI.Borders.empty()
6868
titlePanel.add(sectionLabel)
6969
contentPanel.add(titlePanel)
7070

7171
planItem.tasks.forEachIndexed { taskIndex, task ->
7272
val taskPanel = JBPanel<JBPanel<*>>(FlowLayout(FlowLayout.LEFT)).apply {
73-
border = JBEmptyBorder(JBUI.insets(2, 20, 2, 0))
73+
border = JBUI.Borders.empty()
7474
}
7575

7676
val checkbox = JBCheckBox(task).apply {

core/src/main/kotlin/cc/unitmesh/devti/sketch/ui/plan/MarkdownPlanParser.kt

Lines changed: 77 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,48 @@ object MarkdownPlanParser {
4646

4747
private fun parsePlanItems(node: ASTNode, content: String): List<PlanItem> {
4848
val planItems = mutableListOf<PlanItem>()
49+
val topLevelOrderedLists = findTopLevelOrderedLists(node)
50+
if (topLevelOrderedLists.isNotEmpty() && isFlatOrderedList(topLevelOrderedLists.first(), content)) {
51+
processFlatOrderedList(topLevelOrderedLists.first(), content, planItems)
52+
} else {
53+
processSectionedList(node, content, planItems)
54+
}
55+
56+
return planItems
57+
}
58+
59+
private fun findTopLevelOrderedLists(node: ASTNode): List<ASTNode> {
60+
val orderedLists = mutableListOf<ASTNode>()
61+
62+
node.children.forEach { child ->
63+
if (child.type == MarkdownElementTypes.ORDERED_LIST) {
64+
orderedLists.add(child)
65+
}
66+
}
67+
68+
return orderedLists
69+
}
70+
71+
private fun processFlatOrderedList(node: ASTNode, content: String, planItems: MutableList<PlanItem>) {
72+
node.children.forEach { listItemNode ->
73+
if (listItemNode.type == MarkdownElementTypes.LIST_ITEM) {
74+
val listItemText = listItemNode.getTextInNode(content).toString().trim()
75+
val titleMatch = "^(\\d+)\\.\\s*(.+?)(?:\\s*$CHECKMARK)?$".toRegex().find(listItemText)
76+
77+
if (titleMatch != null) {
78+
val title = titleMatch.groupValues[2].trim()
79+
val completed = listItemText.contains(CHECKMARK)
80+
planItems.add(PlanItem(title, emptyList(), completed))
81+
}
82+
}
83+
}
84+
}
85+
86+
private fun processSectionedList(node: ASTNode, content: String, planItems: MutableList<PlanItem>) {
4987
var currentSectionTitle = ""
5088
var currentSectionItems = mutableListOf<String>()
5189
var currentSectionCompleted = false
52-
90+
5391
node.accept(object : RecursiveVisitor() {
5492
override fun visitNode(node: ASTNode) {
5593
when (node.type) {
@@ -70,7 +108,7 @@ object MarkdownPlanParser {
70108

71109
if (titleMatch != null) {
72110
// Save previous section if exists
73-
if (currentSectionTitle.isNotEmpty() && currentSectionItems.isNotEmpty()) {
111+
if (currentSectionTitle.isNotEmpty()) {
74112
planItems.add(PlanItem(
75113
currentSectionTitle,
76114
currentSectionItems.toList(),
@@ -101,39 +139,53 @@ object MarkdownPlanParser {
101139

102140
super.visitNode(node)
103141
}
104-
105-
private fun processTaskItems(listNode: ASTNode, content: String, itemsList: MutableList<String>) {
106-
listNode.children.forEach { taskItemNode ->
107-
if (taskItemNode.type == MarkdownElementTypes.LIST_ITEM) {
108-
val taskText = taskItemNode.getTextInNode(content).toString().trim()
109-
// Extract just the first line for the task
110-
val firstLineEnd = taskText.indexOf('\n')
111-
val taskFirstLine = if (firstLineEnd > 0) {
112-
taskText.substring(0, firstLineEnd).trim()
113-
} else {
114-
taskText
115-
}
116-
117-
// Process task text and retain the checkmark in the text
118-
val cleanTaskText = taskFirstLine.replace(Regex("^[\\-\\*]\\s+"), "").trim()
119-
if (cleanTaskText.isNotEmpty()) {
120-
itemsList.add(cleanTaskText)
121-
}
122-
}
123-
}
124-
}
125142
})
126143

127144
// 添加最后一个章节(如果有)
128-
if (currentSectionTitle.isNotEmpty() && currentSectionItems.isNotEmpty()) {
145+
if (currentSectionTitle.isNotEmpty()) {
129146
planItems.add(PlanItem(
130147
currentSectionTitle,
131148
currentSectionItems.toList(),
132149
currentSectionCompleted
133150
))
134151
}
152+
}
135153

136-
return planItems
154+
private fun isFlatOrderedList(node: ASTNode, content: String): Boolean {
155+
var hasNestedLists = false
156+
157+
node.children.forEach { listItemNode ->
158+
if (listItemNode.type == MarkdownElementTypes.LIST_ITEM) {
159+
listItemNode.children.forEach { childNode ->
160+
if (childNode.type == MarkdownElementTypes.UNORDERED_LIST) {
161+
hasNestedLists = true
162+
}
163+
}
164+
}
165+
}
166+
167+
return !hasNestedLists
168+
}
169+
170+
private fun processTaskItems(listNode: ASTNode, content: String, itemsList: MutableList<String>) {
171+
listNode.children.forEach { taskItemNode ->
172+
if (taskItemNode.type == MarkdownElementTypes.LIST_ITEM) {
173+
val taskText = taskItemNode.getTextInNode(content).toString().trim()
174+
// Extract just the first line for the task
175+
val firstLineEnd = taskText.indexOf('\n')
176+
val taskFirstLine = if (firstLineEnd > 0) {
177+
taskText.substring(0, firstLineEnd).trim()
178+
} else {
179+
taskText
180+
}
181+
182+
// Process task text and retain the checkmark in the text
183+
val cleanTaskText = taskFirstLine.replace(Regex("^[\\-\\*]\\s+"), "").trim()
184+
if (cleanTaskText.isNotEmpty()) {
185+
itemsList.add(cleanTaskText)
186+
}
187+
}
188+
}
137189
}
138190

139191
// For debugging purposes

core/src/main/resources/genius/en/code/plan.devin

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Here is the rule you should follow:
4040

4141
1. Thoroughly review `<user.question>`. Create an initial plan that includes all the necessary steps to
4242
resolve `<user.question>`, using the recommended steps provided below, and incorporating any requirements from
43-
the `<user.question>`. Place your plan inside the XML tag `<THOUGHT>` within the code language `plan`.
43+
the `<user.question>`. Place your plan inside code-fence block which language is `plan`.
4444
2. Review the project’s codebase, examining not only its structure but also the specific implementation details, to
4545
identify all segments that may contribute to or help resolve the issue described in `<user.question>`.
4646
3. If `<user.question>` describes an error, create a script to reproduce it and run the script to confirm the error.
@@ -57,18 +57,6 @@ Here is the rule you should follow:
5757
If `<user.question>` directly contradicts any of these steps, follow the instructions from `<user.question>`
5858
first. Be thorough in your thinking process, so it's okay if it is lengthy.
5959

60-
For each step, document your reasoning process inside `<THOUGHT>` tags. Include the following information, enclosed within XML tags:
61-
62-
1. `plan`: An updated plan incorporating the outcomes from the previous step. Mark progress by adding `✓` after each task in the plan that was fully completed before this step during the **current session**. Use the symbol `!` for tasks that have a latest status as failed, and use `*` for tasks that are currently in progress. If there are sub-tasks, mark their progress statuses as well. Ensure all progress statuses are marked accurately and appropriately reflect the hierarchical relationships of statuses between tasks and sub-tasks. For example, if all sub-tasks are completed, the parent task should also be marked as completed.
63-
64-
For example:
65-
66-
<THOUGHT>
67-
```plan
68-
Some plan
69-
```plan
70-
</THOUGHT>
71-
7260
Here is user.question:
7361

7462
<user.question>user.question</user.question>

core/src/main/resources/genius/zh/code/plan.devin

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Here is the rule you should follow:
4040

4141
1. Thoroughly review `<user.question>`. Create an initial plan that includes all the necessary steps to
4242
resolve `<user.question>`, using the recommended steps provided below, and incorporating any requirements from
43-
the `<user.question>`. Place your plan inside the XML tag `<THOUGHT>` within the code language `plan`.
43+
the `<user.question>`. Place your plan inside code-fence block which language is `plan`.
4444
2. Review the project’s codebase, examining not only its structure but also the specific implementation details, to
4545
identify all segments that may contribute to or help resolve the issue described in `<user.question>`.
4646
3. If `<user.question>` describes an error, create a script to reproduce it and run the script to confirm the error.
@@ -57,18 +57,6 @@ Here is the rule you should follow:
5757
If `<user.question>` directly contradicts any of these steps, follow the instructions from `<user.question>`
5858
first. Be thorough in your thinking process, so it's okay if it is lengthy.
5959

60-
For each step, document your reasoning process inside `<THOUGHT>` tags. Include the following information, enclosed within XML tags:
61-
62-
1. `plan`: An updated plan incorporating the outcomes from the previous step. Mark progress by adding `✓` after each task in the plan that was fully completed before this step during the **current session**. Use the symbol `!` for tasks that have a latest status as failed, and use `*` for tasks that are currently in progress. If there are sub-tasks, mark their progress statuses as well. Ensure all progress statuses are marked accurately and appropriately reflect the hierarchical relationships of statuses between tasks and sub-tasks. For example, if all sub-tasks are completed, the parent task should also be marked as completed.
63-
64-
For example:
65-
66-
<THOUGHT>
67-
```plan
68-
Some plan
69-
```plan
70-
</THOUGHT>
71-
7260
Here is user.question:
7361

7462
<user.question>user.question</user.question>

core/src/test/kotlin/cc/unitmesh/devti/sketch/ui/plan/MarkdownPlanParserTest.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,31 @@ class MarkdownPlanParserTest {
2626
)
2727
}
2828

29+
@Test
30+
fun should_parse_markdown_with_single_section_and_tasks_no_subitem() {
31+
// Given
32+
val markdownContent = """
33+
1. 在 BlogRepository 中添加基于作者的删除方法
34+
2. 在 BlogService 中实现批量删除逻辑
35+
3. 在 BlogController 中添加 DELETE 端点
36+
4. 确保数据库表结构与实体类映射正确
37+
""".trimIndent()
38+
39+
// When
40+
val planItems = MarkdownPlanParser.parse(markdownContent)
41+
42+
// Then
43+
assertThat(planItems).hasSize(4)
44+
assertThat(planItems[0].title).isEqualTo("在 BlogRepository 中添加基于作者的删除方法")
45+
assertThat(planItems[0].tasks).isEmpty()
46+
assertThat(planItems[1].title).isEqualTo("在 BlogService 中实现批量删除逻辑")
47+
assertThat(planItems[1].tasks).isEmpty()
48+
assertThat(planItems[2].title).isEqualTo("在 BlogController 中添加 DELETE 端点")
49+
assertThat(planItems[2].tasks).isEmpty()
50+
assertThat(planItems[3].title).isEqualTo("确保数据库表结构与实体类映射正确")
51+
assertThat(planItems[3].tasks).isEmpty()
52+
}
53+
2954
@Test
3055
fun should_parse_markdown_with_multiple_sections_and_tasks() {
3156
// Given

0 commit comments

Comments
 (0)