Skip to content

Commit ee06e6f

Browse files
committed
feat(plan): add Markdown plan parser and UI integration #331
- Introduced `MarkdownPlanParser` to parse markdown-formatted plans into structured `PlanItem` objects. - Added `PlanSketch` UI component to display parsed plans with interactive checkboxes. - Updated `ThoughtPlanSketchProvider` to use the new parser and UI. - Removed XML-based plan format in favor of markdown code-fence blocks. - Added comprehensive unit tests for the parser.
1 parent ad7919a commit ee06e6f

File tree

6 files changed

+432
-26
lines changed

6 files changed

+432
-26
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,133 @@
11
package cc.unitmesh.devti.sketch.ui
22

33
import cc.unitmesh.devti.sketch.ui.code.CodeHighlightSketch
4+
import cc.unitmesh.devti.sketch.ui.plan.MarkdownPlanParser
5+
import cc.unitmesh.devti.sketch.ui.plan.PlanItem
6+
import com.intellij.lang.Language
47
import com.intellij.openapi.project.Project
8+
import com.intellij.ui.components.JBCheckBox
9+
import com.intellij.ui.components.JBPanel
10+
import com.intellij.ui.components.JBScrollPane
11+
import com.intellij.util.ui.JBEmptyBorder
12+
import com.intellij.util.ui.JBUI
13+
import java.awt.BorderLayout
14+
import java.awt.FlowLayout
15+
import javax.swing.Box
16+
import javax.swing.BoxLayout
17+
import javax.swing.JComponent
18+
import javax.swing.JLabel
519

620
class ThoughtPlanSketchProvider : LanguageSketchProvider {
721
override fun isSupported(lang: String): Boolean = lang == "plan"
822

923
override fun create(project: Project, content: String): ExtensionLangSketch {
24+
val planItems = MarkdownPlanParser.parse(content)
25+
if (planItems.isNotEmpty()) {
26+
return PlanSketch(project, content, planItems.toMutableList())
27+
}
28+
1029
return object : CodeHighlightSketch(project, content, null), ExtensionLangSketch {
1130
override fun getExtensionName(): String = "ThoughtPlan"
1231
}
1332
}
1433
}
34+
35+
class PlanSketch(
36+
private val project: Project,
37+
private var content: String,
38+
private val planItems: MutableList<PlanItem>
39+
) : JBPanel<PlanSketch>(BorderLayout()), ExtensionLangSketch {
40+
41+
private val panel = JBPanel<PlanSketch>(BorderLayout())
42+
private val contentPanel = JBPanel<PlanSketch>().apply {
43+
layout = BoxLayout(this, BoxLayout.Y_AXIS)
44+
border = JBEmptyBorder(JBUI.insets(8))
45+
}
46+
47+
init {
48+
createPlanUI()
49+
50+
val scrollPane = JBScrollPane(contentPanel)
51+
panel.add(scrollPane, BorderLayout.CENTER)
52+
add(panel, BorderLayout.CENTER)
53+
}
54+
55+
private fun createPlanUI() {
56+
planItems.forEachIndexed { index, planItem ->
57+
val titlePanel = JBPanel<JBPanel<*>>(FlowLayout(FlowLayout.LEFT)).apply {
58+
border = JBEmptyBorder(JBUI.insets(4, 0))
59+
}
60+
61+
val sectionLabel = JLabel("<html><b>${index + 1}. ${planItem.title}</b></html>")
62+
sectionLabel.border = JBUI.Borders.empty(4, 0)
63+
titlePanel.add(sectionLabel)
64+
contentPanel.add(titlePanel)
65+
66+
planItem.tasks.forEachIndexed { taskIndex, task ->
67+
val taskPanel = JBPanel<JBPanel<*>>(FlowLayout(FlowLayout.LEFT)).apply {
68+
border = JBEmptyBorder(JBUI.insets(2, 20, 2, 0))
69+
}
70+
71+
val checkbox = JBCheckBox(task).apply {
72+
isSelected = planItem.completed[taskIndex]
73+
addActionListener {
74+
planItem.completed[taskIndex] = isSelected
75+
}
76+
}
77+
78+
taskPanel.add(checkbox)
79+
contentPanel.add(taskPanel)
80+
}
81+
82+
if (index < planItems.size - 1) {
83+
contentPanel.add(Box.createVerticalStrut(8))
84+
}
85+
}
86+
}
87+
88+
override fun getExtensionName(): String = "ThoughtPlan"
89+
90+
override fun getViewText(): String = content
91+
92+
override fun updateViewText(text: String, complete: Boolean) {
93+
this.content = text
94+
updateUi(text)
95+
}
96+
97+
private fun updateUi(text: String) {
98+
val newPlanItems = MarkdownPlanParser.parse(text)
99+
if (newPlanItems.isNotEmpty()) {
100+
val completionState = mutableMapOf<String, Boolean>()
101+
planItems.forEach { planItem ->
102+
planItem.tasks.forEachIndexed { index, task ->
103+
completionState[task] = planItem.completed[index]
104+
}
105+
}
106+
107+
contentPanel.removeAll()
108+
planItems.clear()
109+
110+
newPlanItems.forEach { newItem ->
111+
val completedList = MutableList(newItem.tasks.size) { index ->
112+
completionState[newItem.tasks[index]] ?: false
113+
}
114+
planItems.add(PlanItem(newItem.title, newItem.tasks, completedList))
115+
}
116+
117+
createPlanUI()
118+
}
119+
120+
contentPanel.revalidate()
121+
contentPanel.repaint()
122+
}
123+
124+
override fun getComponent(): JComponent = this
125+
126+
override fun onDoneStream(allText: String) {
127+
updateUi(this.content)
128+
}
129+
130+
override fun updateLanguage(language: Language?, originLanguage: String?) {}
131+
132+
override fun dispose() {}
133+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package cc.unitmesh.devti.sketch.ui.plan
2+
3+
import com.intellij.openapi.diagnostic.logger
4+
import org.intellij.markdown.IElementType
5+
import org.intellij.markdown.MarkdownElementTypes
6+
import org.intellij.markdown.ast.ASTNode
7+
import org.intellij.markdown.ast.accept
8+
import org.intellij.markdown.ast.getTextInNode
9+
import org.intellij.markdown.ast.visitors.RecursiveVisitor
10+
import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
11+
import org.intellij.markdown.parser.MarkdownParser
12+
13+
/**
14+
* Markdown计划解析器,负责将markdown格式的计划文本解析为PlanItem对象列表
15+
*
16+
* 示例:
17+
*
18+
* ```markdown
19+
* 1. 领域模型重构:
20+
* - 将BlogPost实体合并到Blog聚合根,建立完整的领域对象
21+
* - 添加领域行为方法(发布、审核、评论等)
22+
*
23+
* 2. 分层结构调整:
24+
* - 清理entity层冗余对象
25+
* ```
26+
*/
27+
object MarkdownPlanParser {
28+
private val LOG = logger<MarkdownPlanParser>()
29+
private val ROOT_ELEMENT_TYPE = IElementType("ROOT")
30+
31+
/**
32+
* 解析markdown文本为计划项列表
33+
* @param content markdown格式的计划文本
34+
* @return 解析得到的计划项列表,若解析失败则返回空列表
35+
*/
36+
fun parse(content: String): List<PlanItem> {
37+
try {
38+
val flavour = GFMFlavourDescriptor()
39+
val parsedTree = MarkdownParser(flavour).parse(ROOT_ELEMENT_TYPE, content)
40+
return parsePlanItems(parsedTree, content)
41+
} catch (e: Exception) {
42+
LOG.warn("Failed to parse markdown plan content", e)
43+
return emptyList()
44+
}
45+
}
46+
47+
private fun parsePlanItems(node: ASTNode, content: String): List<PlanItem> {
48+
val planItems = mutableListOf<PlanItem>()
49+
var currentSectionTitle = ""
50+
var currentSectionItems = mutableListOf<String>()
51+
52+
node.accept(object : RecursiveVisitor() {
53+
override fun visitNode(node: ASTNode) {
54+
when (node.type) {
55+
MarkdownElementTypes.ORDERED_LIST -> {
56+
node.children.forEach { listItemNode ->
57+
if (listItemNode.type == MarkdownElementTypes.LIST_ITEM) {
58+
val listItemText = listItemNode.getTextInNode(content).toString().trim()
59+
val titleMatch = "^(\\d+)\\.(.*)$".toRegex().find(listItemText)
60+
61+
if (titleMatch != null) {
62+
if (currentSectionTitle.isNotEmpty() && currentSectionItems.isNotEmpty()) {
63+
planItems.add(PlanItem(currentSectionTitle, currentSectionItems.toList()))
64+
currentSectionItems = mutableListOf()
65+
}
66+
67+
currentSectionTitle = titleMatch.groupValues[2].trim()
68+
69+
listItemNode.children.forEach { childNode ->
70+
if (childNode.type == MarkdownElementTypes.UNORDERED_LIST) {
71+
processTaskItems(childNode, content, currentSectionItems)
72+
}
73+
}
74+
}
75+
}
76+
}
77+
}
78+
MarkdownElementTypes.UNORDERED_LIST -> {
79+
processTaskItems(node, content, currentSectionItems)
80+
}
81+
}
82+
83+
super.visitNode(node)
84+
}
85+
86+
private fun processTaskItems(listNode: ASTNode, content: String, itemsList: MutableList<String>) {
87+
listNode.children.forEach { taskItemNode ->
88+
if (taskItemNode.type == MarkdownElementTypes.LIST_ITEM) {
89+
val taskText = taskItemNode.getTextInNode(content).toString().trim()
90+
// 去除任务项前缀(- 或 *)
91+
val cleanTaskText = taskText.replace(Regex("^[\\-\\*]\\s+"), "").trim()
92+
if (cleanTaskText.isNotEmpty()) {
93+
itemsList.add(cleanTaskText)
94+
}
95+
}
96+
}
97+
}
98+
})
99+
100+
// 添加最后一个章节(如果有)
101+
if (currentSectionTitle.isNotEmpty() && currentSectionItems.isNotEmpty()) {
102+
planItems.add(PlanItem(currentSectionTitle, currentSectionItems.toList()))
103+
}
104+
105+
return planItems
106+
}
107+
}
108+
109+
data class PlanItem(
110+
val title: String,
111+
val tasks: List<String>,
112+
val completed: MutableList<Boolean> = MutableList(tasks.size) { false }
113+
)

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>

0 commit comments

Comments
 (0)