1package soy.iko.opencode.ui.components
3import androidx.compose.foundation.background
4import androidx.compose.foundation.horizontalScroll
5import androidx.compose.foundation.layout.Column
6import androidx.compose.foundation.layout.Row
7import androidx.compose.foundation.layout.fillMaxWidth
8import androidx.compose.foundation.layout.padding
9import androidx.compose.foundation.layout.width
10import androidx.compose.foundation.layout.widthIn
11import androidx.compose.foundation.rememberScrollState
12import androidx.compose.foundation.shape.RoundedCornerShape
13import androidx.compose.foundation.verticalScroll
14import androidx.compose.material3.MaterialTheme
15import androidx.compose.material3.Text
16import androidx.compose.runtime.Composable
17import androidx.compose.runtime.remember
18import androidx.compose.ui.Alignment
19import androidx.compose.ui.Modifier
20import androidx.compose.ui.draw.clip
21import androidx.compose.ui.graphics.Color
22import androidx.compose.ui.text.font.FontFamily
23import androidx.compose.ui.unit.dp
25/** A single line of a parsed unified diff. */
26sealed interface DiffLine {
29 data class Hunk(override val text: String) : DiffLine // @@ ... @@
30 data class FileHeader(override val text: String) : DiffLine // --- / +++
31 data class Context(override val text: String) : DiffLine
32 data class Add(override val text: String) : DiffLine
33 data class Remove(override val text: String) : DiffLine
36/** Parse a unified diff string into typed [DiffLine]s. */
37fun parseDiff(diff: String): List<DiffLine> {
38 val result = mutableListOf<DiffLine>()
39 for (raw in diff.split("\n")) {
41 raw.startsWith("@@") -> result.add(DiffLine.Hunk(raw))
42 raw.startsWith("---") || raw.startsWith("+++") -> result.add(DiffLine.FileHeader(raw))
43 raw.startsWith("+") -> result.add(DiffLine.Add(raw.removePrefix("+")))
44 raw.startsWith("-") -> result.add(DiffLine.Remove(raw.removePrefix("-")))
45 raw.startsWith(" ") -> result.add(DiffLine.Context(raw.removePrefix(" ")))
46 raw.startsWith("diff ") || raw.startsWith("index ") -> result.add(DiffLine.FileHeader(raw))
47 raw.isNotBlank() -> result.add(DiffLine.Context(raw))
53/** Heuristic: does this string look like a unified diff? */
54fun looksLikeDiff(text: String): Boolean {
55 val lines = text.lineSequence()
61 line.startsWith("@@") -> hunkCount++
62 line.startsWith("+") && !line.startsWith("+++") -> addCount++
63 line.startsWith("-") && !line.startsWith("---") -> removeCount++
65 if (hunkCount >= 1 && (addCount + removeCount) >= 1) return true
71fun DiffView(diff: String, modifier: Modifier = Modifier) {
72 val lines = remember(diff) { parseDiff(diff) }
73 val addColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)
74 val removeColor = MaterialTheme.colorScheme.error.copy(alpha = 0.15f)
75 val addText = MaterialTheme.colorScheme.primary
76 val removeText = MaterialTheme.colorScheme.error
81 .padding(vertical = 4.dp)
82 .clip(RoundedCornerShape(8.dp))
83 .background(MaterialTheme.colorScheme.surfaceVariant)
84 .verticalScroll(rememberScrollState())
85 .horizontalScroll(rememberScrollState())
90 is DiffLine.Hunk -> Text(
92 style = MaterialTheme.typography.bodySmall,
93 fontFamily = FontFamily.Monospace,
94 color = MaterialTheme.colorScheme.tertiary,
95 modifier = Modifier.padding(vertical = 2.dp),
97 is DiffLine.FileHeader -> Text(
99 style = MaterialTheme.typography.bodySmall,
100 fontFamily = FontFamily.Monospace,
101 color = MaterialTheme.colorScheme.onSurfaceVariant,
102 modifier = Modifier.padding(vertical = 1.dp),
104 is DiffLine.Add -> DiffRow(line.text, "+", addColor, addText)
105 is DiffLine.Remove -> DiffRow(line.text, "-", removeColor, removeText)
106 is DiffLine.Context -> DiffRow(line.text, " ", Color.Transparent, MaterialTheme.colorScheme.onSurface)
113private fun DiffRow(text: String, prefix: String, bg: Color, textColor: androidx.compose.ui.graphics.Color) {
115 modifier = Modifier.fillMaxWidth().background(bg),
119 style = MaterialTheme.typography.bodySmall,
120 fontFamily = FontFamily.Monospace,
122 modifier = Modifier.width(16.dp),
126 style = MaterialTheme.typography.bodySmall,
127 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