Skip to content

Commit da39b5b

Browse files
committed
feat(mpp-idea): add file change tracking components
Introduce new classes for tracking, summarizing, and displaying file changes in the IDEA toolwindow, and update input area to integrate with these features.
1 parent 1f09174 commit da39b5b

File tree

5 files changed

+983
-0
lines changed

5 files changed

+983
-0
lines changed

mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaDevInInputArea.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import androidx.compose.ui.unit.dp
1414
import cc.unitmesh.agent.plan.AgentPlan
1515
import cc.unitmesh.devins.idea.editor.*
1616
import cc.unitmesh.devins.idea.toolwindow.plan.IdeaPlanSummaryBar
17+
import cc.unitmesh.devins.idea.toolwindow.changes.IdeaFileChangeSummary
1718
import cc.unitmesh.llm.NamedModelConfig
1819
import com.intellij.openapi.Disposable
1920
import com.intellij.openapi.application.ApplicationManager
@@ -111,6 +112,12 @@ fun IdeaDevInInputArea(
111112
modifier = Modifier.fillMaxWidth()
112113
)
113114

115+
// File change summary - shown when there are file changes
116+
IdeaFileChangeSummary(
117+
project = project,
118+
modifier = Modifier.fillMaxWidth()
119+
)
120+
114121
// Top toolbar with file selection (no individual border)
115122
IdeaTopToolbar(
116123
project = project,
Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
package cc.unitmesh.devins.idea.toolwindow.changes
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.layout.*
5+
import androidx.compose.foundation.rememberScrollState
6+
import androidx.compose.foundation.shape.RoundedCornerShape
7+
import androidx.compose.foundation.verticalScroll
8+
import androidx.compose.runtime.Composable
9+
import androidx.compose.ui.Alignment
10+
import androidx.compose.ui.Modifier
11+
import androidx.compose.ui.draw.clip
12+
import androidx.compose.ui.graphics.Color
13+
import androidx.compose.ui.text.font.FontFamily
14+
import androidx.compose.ui.text.font.FontWeight
15+
import androidx.compose.ui.unit.dp
16+
import androidx.compose.ui.unit.sp
17+
import cc.unitmesh.agent.diff.ChangeType
18+
import cc.unitmesh.agent.diff.DiffUtils
19+
import cc.unitmesh.agent.diff.FileChange
20+
import cc.unitmesh.devins.ui.compose.theme.AutoDevColors
21+
import com.intellij.openapi.project.Project
22+
import com.intellij.openapi.ui.DialogWrapper
23+
import com.intellij.util.ui.JBUI
24+
import org.jetbrains.jewel.bridge.compose
25+
import org.jetbrains.jewel.foundation.theme.JewelTheme
26+
import org.jetbrains.jewel.ui.component.DefaultButton
27+
import org.jetbrains.jewel.ui.component.Icon
28+
import org.jetbrains.jewel.ui.component.OutlinedButton
29+
import org.jetbrains.jewel.ui.component.Text
30+
import org.jetbrains.jewel.ui.icons.AllIconsKeys
31+
import java.awt.Dimension
32+
import javax.swing.JComponent
33+
34+
/**
35+
* Dialog for displaying file change diff using IntelliJ's DialogWrapper.
36+
* Uses Jewel Compose for the content rendering.
37+
*/
38+
@Composable
39+
fun IdeaFileChangeDiffDialog(
40+
project: Project,
41+
change: FileChange,
42+
onDismiss: () -> Unit,
43+
onUndo: () -> Unit,
44+
onKeep: () -> Unit
45+
) {
46+
// Show the dialog using DialogWrapper
47+
IdeaFileChangeDiffDialogWrapper.show(
48+
project = project,
49+
change = change,
50+
onUndo = onUndo,
51+
onKeep = onKeep,
52+
onDismiss = onDismiss
53+
)
54+
}
55+
56+
/**
57+
* DialogWrapper implementation for file change diff dialog.
58+
*/
59+
class IdeaFileChangeDiffDialogWrapper(
60+
private val project: Project,
61+
private val change: FileChange,
62+
private val onUndoCallback: () -> Unit,
63+
private val onKeepCallback: () -> Unit,
64+
private val onDismissCallback: () -> Unit
65+
) : DialogWrapper(project) {
66+
67+
init {
68+
title = "Diff: ${change.getFileName()}"
69+
init()
70+
contentPanel.border = JBUI.Borders.empty()
71+
rootPane.border = JBUI.Borders.empty()
72+
}
73+
74+
override fun createSouthPanel(): JComponent? = null
75+
76+
override fun createCenterPanel(): JComponent {
77+
val dialogPanel = compose {
78+
DiffDialogContent(
79+
change = change,
80+
onDismiss = {
81+
onDismissCallback()
82+
close(CANCEL_EXIT_CODE)
83+
},
84+
onUndo = {
85+
onUndoCallback()
86+
close(OK_EXIT_CODE)
87+
},
88+
onKeep = {
89+
onKeepCallback()
90+
close(OK_EXIT_CODE)
91+
}
92+
)
93+
}
94+
dialogPanel.preferredSize = Dimension(800, 600)
95+
return dialogPanel
96+
}
97+
98+
override fun doCancelAction() {
99+
onDismissCallback()
100+
super.doCancelAction()
101+
}
102+
103+
companion object {
104+
fun show(
105+
project: Project,
106+
change: FileChange,
107+
onUndo: () -> Unit,
108+
onKeep: () -> Unit,
109+
onDismiss: () -> Unit
110+
): Boolean {
111+
val dialog = IdeaFileChangeDiffDialogWrapper(
112+
project = project,
113+
change = change,
114+
onUndoCallback = onUndo,
115+
onKeepCallback = onKeep,
116+
onDismissCallback = onDismiss
117+
)
118+
return dialog.showAndGet()
119+
}
120+
}
121+
}
122+
123+
@Composable
124+
private fun DiffDialogContent(
125+
change: FileChange,
126+
onDismiss: () -> Unit,
127+
onUndo: () -> Unit,
128+
onKeep: () -> Unit
129+
) {
130+
val scrollState = rememberScrollState()
131+
val diffContent = DiffUtils.generateUnifiedDiff(
132+
oldContent = change.originalContent ?: "",
133+
newContent = change.newContent ?: "",
134+
filePath = change.filePath
135+
)
136+
137+
Column(
138+
modifier = Modifier
139+
.fillMaxSize()
140+
.background(JewelTheme.globalColors.panelBackground)
141+
.padding(16.dp)
142+
) {
143+
// Header
144+
DiffDialogHeader(change = change)
145+
146+
Spacer(modifier = Modifier.height(12.dp))
147+
148+
// Diff content
149+
Box(
150+
modifier = Modifier
151+
.weight(1f)
152+
.fillMaxWidth()
153+
.clip(RoundedCornerShape(4.dp))
154+
.background(AutoDevColors.Neutral.c900)
155+
.padding(8.dp)
156+
) {
157+
Column(
158+
modifier = Modifier
159+
.fillMaxSize()
160+
.verticalScroll(scrollState)
161+
) {
162+
diffContent.lines().forEach { line ->
163+
DiffLine(line = line)
164+
}
165+
}
166+
}
167+
168+
Spacer(modifier = Modifier.height(12.dp))
169+
170+
// Action buttons
171+
DiffDialogActions(
172+
onDismiss = onDismiss,
173+
onUndo = onUndo,
174+
onKeep = onKeep
175+
)
176+
}
177+
}
178+
179+
@Composable
180+
private fun DiffDialogHeader(change: FileChange) {
181+
val diffStats = change.getDiffStats()
182+
val iconKey = when (change.changeType) {
183+
ChangeType.CREATE -> AllIconsKeys.General.Add
184+
ChangeType.EDIT -> AllIconsKeys.Actions.Edit
185+
ChangeType.DELETE -> AllIconsKeys.General.Remove
186+
ChangeType.RENAME -> AllIconsKeys.Actions.Edit // Use Edit as fallback for Rename
187+
}
188+
val iconColor = when (change.changeType) {
189+
ChangeType.CREATE -> AutoDevColors.Green.c400
190+
ChangeType.EDIT -> AutoDevColors.Blue.c400
191+
ChangeType.DELETE -> AutoDevColors.Red.c400
192+
ChangeType.RENAME -> AutoDevColors.Indigo.c400 // Use Indigo instead of Purple
193+
}
194+
195+
Row(
196+
modifier = Modifier.fillMaxWidth(),
197+
horizontalArrangement = Arrangement.SpaceBetween,
198+
verticalAlignment = Alignment.CenterVertically
199+
) {
200+
Row(
201+
verticalAlignment = Alignment.CenterVertically,
202+
horizontalArrangement = Arrangement.spacedBy(8.dp)
203+
) {
204+
Icon(
205+
key = iconKey,
206+
contentDescription = change.changeType.name,
207+
modifier = Modifier.size(20.dp),
208+
tint = iconColor
209+
)
210+
Column {
211+
Text(
212+
text = change.getFileName(),
213+
style = JewelTheme.defaultTextStyle.copy(
214+
fontSize = 14.sp,
215+
fontWeight = FontWeight.Bold
216+
)
217+
)
218+
Text(
219+
text = change.filePath,
220+
style = JewelTheme.defaultTextStyle.copy(
221+
fontSize = 11.sp,
222+
color = AutoDevColors.Neutral.c400
223+
)
224+
)
225+
}
226+
}
227+
228+
// Diff stats
229+
Row(
230+
horizontalArrangement = Arrangement.spacedBy(8.dp)
231+
) {
232+
Text(
233+
text = "+${diffStats.addedLines}",
234+
style = JewelTheme.defaultTextStyle.copy(
235+
fontSize = 12.sp,
236+
color = AutoDevColors.Green.c400,
237+
fontWeight = FontWeight.Medium
238+
)
239+
)
240+
Text(
241+
text = "-${diffStats.deletedLines}",
242+
style = JewelTheme.defaultTextStyle.copy(
243+
fontSize = 12.sp,
244+
color = AutoDevColors.Red.c400,
245+
fontWeight = FontWeight.Medium
246+
)
247+
)
248+
}
249+
}
250+
}
251+
252+
@Composable
253+
private fun DiffLine(line: String) {
254+
val backgroundColor: Color
255+
val textColor: Color
256+
257+
when {
258+
line.startsWith("+") && !line.startsWith("+++") -> {
259+
backgroundColor = AutoDevColors.Green.c900.copy(alpha = 0.3f)
260+
textColor = AutoDevColors.Green.c300
261+
}
262+
line.startsWith("-") && !line.startsWith("---") -> {
263+
backgroundColor = AutoDevColors.Red.c900.copy(alpha = 0.3f)
264+
textColor = AutoDevColors.Red.c300
265+
}
266+
line.startsWith("@@") -> {
267+
backgroundColor = AutoDevColors.Blue.c900.copy(alpha = 0.3f)
268+
textColor = AutoDevColors.Blue.c300
269+
}
270+
else -> {
271+
backgroundColor = Color.Transparent
272+
textColor = AutoDevColors.Neutral.c300
273+
}
274+
}
275+
276+
Text(
277+
text = line,
278+
style = JewelTheme.defaultTextStyle.copy(
279+
fontSize = 12.sp,
280+
fontFamily = FontFamily.Monospace,
281+
color = textColor
282+
),
283+
modifier = Modifier
284+
.fillMaxWidth()
285+
.background(backgroundColor)
286+
.padding(horizontal = 4.dp, vertical = 1.dp)
287+
)
288+
}
289+
290+
@Composable
291+
private fun DiffDialogActions(
292+
onDismiss: () -> Unit,
293+
onUndo: () -> Unit,
294+
onKeep: () -> Unit
295+
) {
296+
Row(
297+
modifier = Modifier.fillMaxWidth(),
298+
horizontalArrangement = Arrangement.End,
299+
verticalAlignment = Alignment.CenterVertically
300+
) {
301+
OutlinedButton(
302+
onClick = onDismiss,
303+
modifier = Modifier.padding(end = 8.dp)
304+
) {
305+
Text("Close")
306+
}
307+
308+
OutlinedButton(
309+
onClick = onUndo,
310+
modifier = Modifier.padding(end = 8.dp)
311+
) {
312+
Icon(
313+
key = AllIconsKeys.Actions.Rollback,
314+
contentDescription = "Undo",
315+
modifier = Modifier.size(14.dp),
316+
tint = AutoDevColors.Red.c400
317+
)
318+
Spacer(modifier = Modifier.width(4.dp))
319+
Text("Undo", color = AutoDevColors.Red.c400)
320+
}
321+
322+
DefaultButton(
323+
onClick = onKeep
324+
) {
325+
Icon(
326+
key = AllIconsKeys.Actions.Checked,
327+
contentDescription = "Keep",
328+
modifier = Modifier.size(14.dp),
329+
tint = AutoDevColors.Green.c400
330+
)
331+
Spacer(modifier = Modifier.width(4.dp))
332+
Text("Keep")
333+
}
334+
}
335+
}

0 commit comments

Comments
 (0)