package cc.unitmesh.devins.idea.renderer.sketch import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.hoverable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import cc.unitmesh.agent.diff.DiffLineType import cc.unitmesh.agent.diff.DiffParser import cc.unitmesh.devins.idea.renderer.sketch.actions.IdeaDiffActions import cc.unitmesh.devins.idea.toolwindow.IdeaComposeIcons import cc.unitmesh.devins.idea.theme.IdeaAutoDevColors import com.intellij.openapi.project.Project import org.jetbrains.jewel.foundation.theme.JewelTheme import org.jetbrains.jewel.ui.component.Icon import org.jetbrains.jewel.ui.component.Text import org.jetbrains.jewel.ui.component.Tooltip import org.jetbrains.jewel.ui.icons.AllIconsKeys /** * Diff renderer for IntelliJ IDEA with Jewel styling. * Renders unified diff format with syntax highlighting and action buttons. * * Related GitHub Issue: https://github.com/phodal/auto-dev/issues/25 */ @Composable fun IdeaDiffRenderer( diffContent: String, project: Project? = null, modifier: Modifier = Modifier ) { val fileDiffs = remember(diffContent) { DiffParser.parse(diffContent) } var isRepairing by remember { mutableStateOf(false) } var patchApplied by remember { mutableStateOf(false) } Column(modifier = modifier) { // Toolbar with action buttons if (project != null && fileDiffs.isNotEmpty()) { DiffToolbar( diffContent = diffContent, project = project, isRepairing = isRepairing, patchApplied = patchApplied, onAccept = { val success = IdeaDiffActions.acceptPatch(project, diffContent) if (success) patchApplied = true }, onReject = { IdeaDiffActions.rejectPatch(project) patchApplied = false }, onViewDiff = { IdeaDiffActions.viewDiff(project, diffContent) { val success = IdeaDiffActions.acceptPatch(project, diffContent) if (success) patchApplied = true } }, onRepair = { isRepairing = true IdeaDiffActions.repairPatch(project, diffContent) { isRepairing = false } } ) } if (fileDiffs.isEmpty()) { // Show error with repair option Row( modifier = Modifier.padding(8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Text( text = "Unable to parse diff content", style = JewelTheme.defaultTextStyle.copy( fontSize = 11.sp, color = IdeaAutoDevColors.Red.c400 ) ) if (project != null && !isRepairing) { DiffActionButton( tooltip = "Repair with AI", onClick = { isRepairing = true IdeaDiffActions.repairPatch(project, diffContent) { isRepairing = false } } ) { Icon( key = AllIconsKeys.Actions.IntentionBulb, contentDescription = "Repair", modifier = Modifier.size(14.dp) ) } } if (isRepairing) { Text( text = "Repairing...", style = JewelTheme.defaultTextStyle.copy( fontSize = 10.sp, color = IdeaAutoDevColors.Blue.c400 ) ) } } return@Column } fileDiffs.forEach { fileDiff -> // File header Box( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(topStart = 4.dp, topEnd = 4.dp)) .background(JewelTheme.globalColors.panelBackground) .padding(8.dp) ) { val displayPath = fileDiff.newPath?.takeIf { it.isNotBlank() } ?: fileDiff.oldPath ?: "unknown" Text( text = displayPath, style = JewelTheme.defaultTextStyle.copy( fontFamily = FontFamily.Monospace, fontSize = 11.sp, fontWeight = FontWeight.Bold, color = IdeaAutoDevColors.Blue.c400 ) ) } // Diff hunks Column( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(bottomStart = 4.dp, bottomEnd = 4.dp)) .background(JewelTheme.globalColors.panelBackground.copy(alpha = 0.3f)) ) { fileDiff.hunks.forEach { hunk -> // Hunk header Text( text = "@@ -${hunk.oldStartLine},${hunk.oldLineCount} +${hunk.newStartLine},${hunk.newLineCount} @@", style = JewelTheme.defaultTextStyle.copy( fontFamily = FontFamily.Monospace, fontSize = 10.sp, color = IdeaAutoDevColors.Blue.c300 ), modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) ) // Diff lines hunk.lines.forEach { diffLine -> val (bgColor, textColor) = when (diffLine.type) { DiffLineType.ADDED -> Pair( IdeaAutoDevColors.Diff.Dark.addedBg, IdeaAutoDevColors.Green.c400 ) DiffLineType.DELETED -> Pair( IdeaAutoDevColors.Diff.Dark.deletedBg, IdeaAutoDevColors.Red.c400 ) else -> Pair( Color.Transparent, JewelTheme.globalColors.text.normal ) } Text( text = diffLine.content, style = JewelTheme.defaultTextStyle.copy( fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = textColor ), modifier = Modifier .fillMaxWidth() .background(bgColor) .padding(horizontal = 8.dp, vertical = 1.dp) ) } } } Spacer(modifier = Modifier.height(8.dp)) } } } @Composable private fun DiffToolbar( diffContent: String, project: Project, isRepairing: Boolean, patchApplied: Boolean, onAccept: () -> Unit, onReject: () -> Unit, onViewDiff: () -> Unit, onRepair: () -> Unit ) { Row( modifier = Modifier .fillMaxWidth() .background(JewelTheme.globalColors.panelBackground) .padding(horizontal = 8.dp, vertical = 4.dp), horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically ) { // Accept button DiffActionButton( tooltip = "Accept and apply patch", onClick = onAccept, enabled = !patchApplied ) { Icon( key = AllIconsKeys.Actions.Commit, contentDescription = "Accept", modifier = Modifier.size(14.dp), tint = if (patchApplied) IdeaAutoDevColors.Neutral.c500 else IdeaAutoDevColors.Green.c400 ) } Spacer(modifier = Modifier.width(4.dp)) // Reject/Undo button DiffActionButton( tooltip = "Reject/Undo patch", onClick = onReject ) { Icon( key = AllIconsKeys.Actions.Rollback, contentDescription = "Reject", modifier = Modifier.size(14.dp) ) } Spacer(modifier = Modifier.width(4.dp)) // View Diff button DiffActionButton( tooltip = "View diff in dialog", onClick = onViewDiff ) { Icon( key = AllIconsKeys.Actions.ListChanges, contentDescription = "View Diff", modifier = Modifier.size(14.dp) ) } Spacer(modifier = Modifier.width(4.dp)) // Repair button DiffActionButton( tooltip = "Repair patch with AI", onClick = onRepair, enabled = !isRepairing ) { if (isRepairing) { Icon( imageVector = IdeaComposeIcons.Refresh, contentDescription = "Repairing...", modifier = Modifier.size(14.dp), tint = IdeaAutoDevColors.Blue.c400 ) } else { Icon( key = AllIconsKeys.Actions.IntentionBulb, contentDescription = "Repair", modifier = Modifier.size(14.dp) ) } } } } @Composable private fun DiffActionButton( tooltip: String, onClick: () -> Unit, enabled: Boolean = true, content: @Composable () -> Unit ) { val interactionSource = remember { MutableInteractionSource() } val isHovered by interactionSource.collectIsHoveredAsState() Tooltip(tooltip = { Text(tooltip) }) { Box( modifier = Modifier .size(24.dp) .clip(RoundedCornerShape(4.dp)) .hoverable(interactionSource = interactionSource) .background( if (isHovered && enabled) JewelTheme.globalColors.borders.normal.copy(alpha = 0.3f) else Color.Transparent ) .then(if (enabled) Modifier.clickable(onClick = onClick) else Modifier), contentAlignment = Alignment.Center ) { content() } } }