Skip to content

Commit 28371cf

Browse files
committed
feat(codereview): extract plan parsing and priority logic #463
Move PlanParser and priority adjustment to separate utilities with tests for better modularity and maintainability.
1 parent 21e22e6 commit 28371cf

File tree

5 files changed

+430
-122
lines changed

5 files changed

+430
-122
lines changed

mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/codereview/ModificationPlanSection.kt

Lines changed: 5 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -68,114 +68,6 @@ data class PlanItem(
6868
}
6969
}
7070

71-
/**
72-
* Parses Plan format markdown into structured plan items
73-
* Supports nested structure: ordered list for items, unordered list for steps
74-
*/
75-
object PlanParser {
76-
private val CODE_BLOCK_PATTERN = Regex("```plan\\s*\\n([\\s\\S]*?)\\n```", RegexOption.IGNORE_CASE)
77-
private val ORDERED_ITEM_PATTERN = Regex("^(\\d+)\\.\\s*(.+?)(?:\\s*-\\s*(.+))?$")
78-
private val UNORDERED_ITEM_PATTERN = Regex("^\\s*-\\s*\\[([\\s✓!*]?)\\]\\s*(.+)$")
79-
private val FILE_LINK_PATTERN = Regex("\\[([^\\]]+)\\]\\(([^)]+)\\)")
80-
81-
fun parse(planOutput: String): List<PlanItem> {
82-
val items = mutableListOf<PlanItem>()
83-
84-
// Extract plan code block if present
85-
val planContent = CODE_BLOCK_PATTERN.find(planOutput)?.groupValues?.get(1)
86-
?: planOutput
87-
88-
val lines = planContent.lines()
89-
var currentItem: PlanItem? = null
90-
var currentSteps = mutableListOf<PlanStep>()
91-
var currentNumber = 0
92-
93-
for (line in lines) {
94-
val trimmed = line.trim()
95-
if (trimmed.isEmpty()) continue
96-
97-
// Check for ordered list item (main plan item)
98-
val orderedMatch = ORDERED_ITEM_PATTERN.find(trimmed)
99-
if (orderedMatch != null) {
100-
// Save previous item
101-
currentItem?.let {
102-
items.add(it.copy(steps = currentSteps.toList()))
103-
}
104-
105-
// Start new item
106-
currentNumber = orderedMatch.groupValues[1].toIntOrNull() ?: 0
107-
val titleWithPriority = orderedMatch.groupValues[2].trim()
108-
109-
// Extract priority from title (format: "Title - PRIORITY")
110-
val titleParts = titleWithPriority.split(" - ", limit = 2)
111-
val title = titleParts[0].trim()
112-
val priority = titleParts.getOrNull(1)?.trim() ?: "MEDIUM"
113-
114-
currentItem = PlanItem(
115-
number = currentNumber,
116-
title = title,
117-
priority = priority
118-
)
119-
currentSteps = mutableListOf()
120-
continue
121-
}
122-
123-
// Check for unordered list item (plan step)
124-
val unorderedMatch = UNORDERED_ITEM_PATTERN.find(trimmed)
125-
if (unorderedMatch != null) {
126-
val statusMarker = unorderedMatch.groupValues[1].trim()
127-
val stepText = unorderedMatch.groupValues[2].trim()
128-
129-
val status = when (statusMarker) {
130-
"", "x", "X" -> StepStatus.COMPLETED
131-
"!" -> StepStatus.FAILED
132-
"*" -> StepStatus.IN_PROGRESS
133-
else -> StepStatus.TODO
134-
}
135-
136-
// Extract file links from step text
137-
val fileLinks = extractFileLinks(stepText)
138-
139-
currentSteps.add(
140-
PlanStep(
141-
text = stepText,
142-
status = status,
143-
fileLinks = fileLinks
144-
)
145-
)
146-
continue
147-
}
148-
149-
// If we're inside an item but not a step, append to last step
150-
if (currentItem != null && currentSteps.isNotEmpty() && trimmed.startsWith("-")) {
151-
val lastStep = currentSteps.last()
152-
currentSteps[currentSteps.size - 1] = lastStep.copy(
153-
text = "${lastStep.text} ${trimmed.removePrefix("-").trim()}"
154-
)
155-
}
156-
}
157-
158-
// Save last item
159-
currentItem?.let {
160-
items.add(it.copy(steps = currentSteps.toList()))
161-
}
162-
163-
return items
164-
}
165-
166-
private fun extractFileLinks(text: String): List<FileLink> {
167-
val links = mutableListOf<FileLink>()
168-
val matches = FILE_LINK_PATTERN.findAll(text)
169-
170-
matches.forEach { match ->
171-
val displayText = match.groupValues[1]
172-
val filePath = match.groupValues[2]
173-
links.add(FileLink(displayText, filePath))
174-
}
175-
176-
return links
177-
}
178-
}
17971

18072
/**
18173
* Displays AI-generated modification plan with modern Plan-like UI
@@ -349,21 +241,12 @@ private fun PlanItemCard(
349241
) {
350242
var isExpanded by remember { mutableStateOf(true) }
351243

352-
// Determine priority color and adjust priority based on issue category
353-
// Code style and formatting issues should always be MEDIUM priority
354-
val isCodeStyleIssue = item.title.contains("代码风格") || item.title.contains("Code Style") ||
355-
item.title.contains("字符串格式化") || item.title.contains("String Formatting") ||
356-
item.title.contains("格式化") || item.title.contains("Formatting") ||
357-
item.title.contains("风格") || item.title.contains("Style")
358-
359-
val adjustedPriority = if (isCodeStyleIssue &&
360-
(item.priority.contains("关键") || item.priority.contains("CRITICAL") ||
361-
item.priority.contains("") || item.priority.contains("HIGH"))) {
362-
"MEDIUM"
363-
} else {
364-
item.priority
244+
// Get adjusted priority using utility function
245+
val adjustedPriority = remember(item) {
246+
PlanPriority.getAdjustedPriority(item)
365247
}
366248

249+
// Get priority color based on adjusted priority
367250
val priorityColor = when {
368251
adjustedPriority.contains("关键") || adjustedPriority.contains("CRITICAL") -> AutoDevColors.Red.c600
369252
adjustedPriority.contains("") || adjustedPriority.contains("HIGH") -> AutoDevColors.Amber.c600
@@ -585,7 +468,7 @@ private fun PlanStepItem(
585468
androidx.compose.foundation.text.ClickableText(
586469
text = annotatedText,
587470
style = MaterialTheme.typography.bodySmall.copy(
588-
color = MaterialTheme.colorScheme.onSurfaceVariant,
471+
color = MaterialTheme.colorScheme.onSurfaceVariant,
589472
lineHeight = MaterialTheme.typography.bodySmall.lineHeight * 1.2
590473
),
591474
onClick = { offset ->
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package cc.unitmesh.devins.ui.compose.agent.codereview
2+
3+
/**
4+
* Parses Plan format markdown into structured plan items
5+
* Supports nested structure: ordered list for items, unordered list for steps
6+
*/
7+
object PlanParser {
8+
private val CODE_BLOCK_PATTERN = Regex("```plan\\s*\\n([\\s\\S]*?)\\n```", RegexOption.IGNORE_CASE)
9+
private val ORDERED_ITEM_PATTERN = Regex("^(\\d+)\\.\\s*(.+?)(?:\\s*-\\s*(.+))?$")
10+
private val UNORDERED_ITEM_PATTERN = Regex("^\\s*-\\s*\\[([\\s✓!*]?)\\]\\s*(.+)$")
11+
private val FILE_LINK_PATTERN = Regex("\\[([^\\]]+)\\]\\(([^)]+)\\)")
12+
13+
/**
14+
* Parse plan markdown output into structured plan items
15+
*/
16+
fun parse(planOutput: String): List<PlanItem> {
17+
val items = mutableListOf<PlanItem>()
18+
19+
// Extract plan code block if present
20+
val planContent = CODE_BLOCK_PATTERN.find(planOutput)?.groupValues?.get(1)
21+
?: planOutput
22+
23+
val lines = planContent.lines()
24+
var currentItem: PlanItem? = null
25+
var currentSteps = mutableListOf<PlanStep>()
26+
var currentNumber = 0
27+
28+
for (line in lines) {
29+
val trimmed = line.trim()
30+
if (trimmed.isEmpty()) continue
31+
32+
// Check for ordered list item (main plan item)
33+
val orderedMatch = ORDERED_ITEM_PATTERN.find(trimmed)
34+
if (orderedMatch != null) {
35+
// Save previous item
36+
currentItem?.let {
37+
items.add(it.copy(steps = currentSteps.toList()))
38+
}
39+
40+
// Start new item
41+
currentNumber = orderedMatch.groupValues[1].toIntOrNull() ?: 0
42+
val titleWithPriority = orderedMatch.groupValues[2].trim()
43+
44+
// Extract priority from title (format: "Title - PRIORITY")
45+
val titleParts = titleWithPriority.split(" - ", limit = 2)
46+
val title = titleParts[0].trim()
47+
val priority = titleParts.getOrNull(1)?.trim() ?: "MEDIUM"
48+
49+
currentItem = PlanItem(
50+
number = currentNumber,
51+
title = title,
52+
priority = priority
53+
)
54+
currentSteps = mutableListOf()
55+
continue
56+
}
57+
58+
// Check for unordered list item (plan step)
59+
val unorderedMatch = UNORDERED_ITEM_PATTERN.find(trimmed)
60+
if (unorderedMatch != null) {
61+
val statusMarker = unorderedMatch.groupValues[1].trim()
62+
val stepText = unorderedMatch.groupValues[2].trim()
63+
64+
val status = parseStepStatus(statusMarker)
65+
66+
// Extract file links from step text
67+
val fileLinks = extractFileLinks(stepText)
68+
69+
currentSteps.add(
70+
PlanStep(
71+
text = stepText,
72+
status = status,
73+
fileLinks = fileLinks
74+
)
75+
)
76+
continue
77+
}
78+
79+
// If we're inside an item but not a step, append to last step
80+
if (currentItem != null && currentSteps.isNotEmpty() && trimmed.startsWith("-")) {
81+
val lastStep = currentSteps.last()
82+
currentSteps[currentSteps.size - 1] = lastStep.copy(
83+
text = "${lastStep.text} ${trimmed.removePrefix("-").trim()}"
84+
)
85+
}
86+
}
87+
88+
// Save last item
89+
currentItem?.let {
90+
items.add(it.copy(steps = currentSteps.toList()))
91+
}
92+
93+
return items
94+
}
95+
96+
/**
97+
* Parse step status from markdown marker
98+
*/
99+
private fun parseStepStatus(marker: String): StepStatus {
100+
return when (marker) {
101+
"", "x", "X" -> StepStatus.COMPLETED
102+
"!" -> StepStatus.FAILED
103+
"*" -> StepStatus.IN_PROGRESS
104+
else -> StepStatus.TODO
105+
}
106+
}
107+
108+
/**
109+
* Extract file links from text using markdown link pattern
110+
*/
111+
private fun extractFileLinks(text: String): List<FileLink> {
112+
val links = mutableListOf<FileLink>()
113+
val matches = FILE_LINK_PATTERN.findAll(text)
114+
115+
matches.forEach { match ->
116+
val displayText = match.groupValues[1]
117+
val filePath = match.groupValues[2]
118+
links.add(FileLink(displayText, filePath))
119+
}
120+
121+
return links
122+
}
123+
}
124+
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package cc.unitmesh.devins.ui.compose.agent.codereview
2+
3+
/**
4+
* Priority-related utilities for plan items
5+
* Handles priority adjustment logic
6+
*
7+
* This is a pure function utility that can be easily tested without Compose dependencies
8+
*/
9+
object PlanPriority {
10+
/**
11+
* Keywords that indicate code style or formatting issues
12+
* These should always be treated as MEDIUM priority
13+
*/
14+
private val CODE_STYLE_KEYWORDS = setOf(
15+
"代码风格", "Code Style",
16+
"字符串格式化", "String Formatting",
17+
"格式化", "Formatting",
18+
"风格", "Style"
19+
)
20+
21+
/**
22+
* Check if a title indicates a code style issue
23+
*/
24+
fun isCodeStyleIssue(title: String): Boolean {
25+
return CODE_STYLE_KEYWORDS.any { keyword ->
26+
title.contains(keyword, ignoreCase = true)
27+
}
28+
}
29+
30+
/**
31+
* Adjust priority based on issue category
32+
* Code style and formatting issues are always downgraded to MEDIUM
33+
*/
34+
fun adjustPriority(title: String, priority: String): String {
35+
return if (isCodeStyleIssue(title) && isHighPriority(priority)) {
36+
"MEDIUM"
37+
} else {
38+
priority
39+
}
40+
}
41+
42+
/**
43+
* Check if priority is high (CRITICAL or HIGH)
44+
*/
45+
fun isHighPriority(priority: String): Boolean {
46+
return priority.contains("关键", ignoreCase = true) ||
47+
priority.contains("CRITICAL", ignoreCase = true) ||
48+
priority.contains("", ignoreCase = true) ||
49+
priority.contains("HIGH", ignoreCase = true)
50+
}
51+
52+
/**
53+
* Get priority info (adjusted priority) for a plan item
54+
* Color resolution is done in Compose context
55+
*/
56+
fun getAdjustedPriority(item: PlanItem): String {
57+
return adjustPriority(item.title, item.priority)
58+
}
59+
}
60+

0 commit comments

Comments
 (0)