git.iko.soy/opencode-android
on e37d1bb help sign in
148 lines 6.1 kB history raw
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 {
32 val text: String
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")) {
45 when {
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))
53 }
54 }
55 return result
58/** Heuristic: does this string look like a unified diff? */
59fun looksLikeDiff(text: String): Boolean {
60 val lines = text.lineSequence()
61 var hunkCount = 0
62 var addCount = 0
63 var removeCount = 0
64 for (line in lines) {
65 when {
66 line.startsWith("@@") -> hunkCount++
67 line.startsWith("+") && !line.startsWith("+++") -> addCount++
68 line.startsWith("-") && !line.startsWith("---") -> removeCount++
69 }
70 if (hunkCount >= 1 && (addCount + removeCount) >= 1) return true
71 }
72 return false
75@Composable
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
84 Column(
85 modifier = modifier
86 .fillMaxWidth()
87 .padding(vertical = 4.dp)
88 .clip(RoundedCornerShape(8.dp))
89 .background(MaterialTheme.colorScheme.surfaceVariant)
90 .padding(top = 2.dp, end = 4.dp),
91 ) {
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)
95 }
96 }
97 Column(
98 modifier = Modifier
99 .horizontalScroll(rememberScrollState())
100 .verticalScroll(rememberScrollState())
101 .padding(horizontal = 10.dp)
102 .padding(bottom = 10.dp),
103 ) {
104 for (line in lines) {
105 when (line) {
106 is DiffLine.Hunk -> Text(
107 line.text,
108 style = MaterialTheme.typography.bodySmall,
109 fontFamily = FontFamily.Monospace,
110 color = MaterialTheme.colorScheme.tertiary,
111 modifier = Modifier.padding(vertical = 2.dp),
112 )
113 is DiffLine.FileHeader -> Text(
114 line.text,
115 style = MaterialTheme.typography.bodySmall,
116 fontFamily = FontFamily.Monospace,
117 color = MaterialTheme.colorScheme.onSurfaceVariant,
118 modifier = Modifier.padding(vertical = 1.dp),
119 )
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)
123 }
124 }
125 }
126 }
129@Composable
130private fun DiffRow(text: String, prefix: String, bg: Color, textColor: androidx.compose.ui.graphics.Color) {
131 Row(
132 modifier = Modifier.fillMaxWidth().background(bg),
133 ) {
134 Text(
135 prefix,
136 style = MaterialTheme.typography.bodySmall,
137 fontFamily = FontFamily.Monospace,
138 color = textColor,
139 modifier = Modifier.width(16.dp),
140 )
141 Text(
142 text,
143 style = MaterialTheme.typography.bodySmall,
144 fontFamily = FontFamily.Monospace,
145 color = textColor,
146 )
147 }