1package soy.iko.opencode.ui.components
3import androidx.compose.foundation.background
4import androidx.compose.foundation.horizontalScroll
5import androidx.compose.foundation.layout.Arrangement
6import androidx.compose.foundation.layout.Column
7import androidx.compose.foundation.layout.Row
8import androidx.compose.foundation.layout.fillMaxWidth
9import androidx.compose.foundation.layout.padding
10import androidx.compose.foundation.layout.width
11import androidx.compose.foundation.rememberScrollState
12import androidx.compose.foundation.shape.RoundedCornerShape
13import androidx.compose.foundation.verticalScroll
14import androidx.compose.material.icons.Icons
15import androidx.compose.material.icons.filled.ContentCopy
16import androidx.compose.material3.Icon
17import androidx.compose.material3.IconButton
18import androidx.compose.material3.MaterialTheme
19import androidx.compose.material3.Text
20import androidx.compose.runtime.Composable
21import androidx.compose.runtime.remember
22import androidx.compose.ui.Alignment
23import androidx.compose.ui.Modifier
24import androidx.compose.ui.draw.clip
25import androidx.compose.ui.graphics.Color
26import androidx.compose.ui.platform.LocalContext
27import androidx.compose.ui.text.font.FontFamily
28import androidx.compose.ui.unit.dp
30/** A single line of a parsed unified diff. */
31sealed interface DiffLine {
34 data class Hunk(override val text: String) : DiffLine // @@ ... @@
35 data class FileHeader(override val text: String) : DiffLine // --- / +++
36 data class Context(override val text: String) : DiffLine
37 data class Add(override val text: String) : DiffLine
38 data class Remove(override val text: String) : DiffLine
41/** Parse a unified diff string into typed [DiffLine]s. */
42fun parseDiff(diff: String): List<DiffLine> {
43 val result = mutableListOf<DiffLine>()
44 for (raw in diff.split("\n")) {
46 raw.startsWith("@@") -> result.add(DiffLine.Hunk(raw))
47 raw.startsWith("---") || raw.startsWith("+++") -> result.add(DiffLine.FileHeader(raw))
48 raw.startsWith("+") -> result.add(DiffLine.Add(raw.removePrefix("+")))
49 raw.startsWith("-") -> result.add(DiffLine.Remove(raw.removePrefix("-")))
50 raw.startsWith(" ") -> result.add(DiffLine.Context(raw.removePrefix(" ")))
51 raw.startsWith("diff ") || raw.startsWith("index ") -> result.add(DiffLine.FileHeader(raw))
52 raw.isNotBlank() -> result.add(DiffLine.Context(raw))
58/** Heuristic: does this string look like a unified diff? */
59fun looksLikeDiff(text: String): Boolean {
60 val lines = text.lineSequence()
66 line.startsWith("@@") -> hunkCount++
67 line.startsWith("+") && !line.startsWith("+++") -> addCount++
68 line.startsWith("-") && !line.startsWith("---") -> removeCount++
70 if (hunkCount >= 1 && (addCount + removeCount) >= 1) return true
76fun DiffView(diff: String, modifier: Modifier = Modifier) {
77 val lines = remember(diff) { parseDiff(diff) }
78 val addColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)
79 val removeColor = MaterialTheme.colorScheme.error.copy(alpha = 0.15f)
80 val addText = MaterialTheme.colorScheme.primary
81 val removeText = MaterialTheme.colorScheme.error
82 val context = LocalContext.current
87 .padding(vertical = 4.dp)
88 .clip(RoundedCornerShape(8.dp))
89 .background(MaterialTheme.colorScheme.surfaceVariant)
90 .padding(top = 2.dp, end = 4.dp),
92 Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
93 IconButton(onClick = { copyToClipboard(context, "diff", diff) }) {
94 Icon(Icons.Filled.ContentCopy, contentDescription = "Copy", tint = MaterialTheme.colorScheme.onSurfaceVariant)
99 .horizontalScroll(rememberScrollState())
100 .verticalScroll(rememberScrollState())
101 .padding(horizontal = 10.dp)
102 .padding(bottom = 10.dp),
104 for (line in lines) {
106 is DiffLine.Hunk -> Text(
108 style = MaterialTheme.typography.bodySmall,
109 fontFamily = FontFamily.Monospace,
110 color = MaterialTheme.colorScheme.tertiary,
111 modifier = Modifier.padding(vertical = 2.dp),
113 is DiffLine.FileHeader -> Text(
115 style = MaterialTheme.typography.bodySmall,
116 fontFamily = FontFamily.Monospace,
117 color = MaterialTheme.colorScheme.onSurfaceVariant,
118 modifier = Modifier.padding(vertical = 1.dp),
120 is DiffLine.Add -> DiffRow(line.text, "+", addColor, addText)
121 is DiffLine.Remove -> DiffRow(line.text, "-", removeColor, removeText)
122 is DiffLine.Context -> DiffRow(line.text, " ", Color.Transparent, MaterialTheme.colorScheme.onSurface)
130private fun DiffRow(text: String, prefix: String, bg: Color, textColor: androidx.compose.ui.graphics.Color) {
132 modifier = Modifier.fillMaxWidth().background(bg),
136 style = MaterialTheme.typography.bodySmall,
137 fontFamily = FontFamily.Monospace,
139 modifier = Modifier.width(16.dp),
143 style = MaterialTheme.typography.bodySmall,
144 fontFamily = FontFamily.Monospace,
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list