@@ -18,8 +18,13 @@ import androidx.compose.material3.Icon
18import androidx.compose.material3.IconButton
18import androidx.compose.material3.IconButton
19import androidx.compose.material3.MaterialTheme
19import androidx.compose.material3.MaterialTheme
20import androidx.compose.material3.Text
20import androidx.compose.material3.Text
21+import androidx.compose.material3.TextButton
21import androidx.compose.runtime.Composable
22import androidx.compose.runtime.Composable
23+import androidx.compose.runtime.getValue
24+import androidx.compose.runtime.mutableStateOf
22import androidx.compose.runtime.remember
25import androidx.compose.runtime.remember
26+import androidx.compose.runtime.saveable.rememberSaveable
27+import androidx.compose.runtime.setValue
23import androidx.compose.ui.Alignment
28import androidx.compose.ui.Alignment
24import androidx.compose.ui.Modifier
29import androidx.compose.ui.Modifier
25import androidx.compose.ui.draw.clip
30import androidx.compose.ui.draw.clip
@@ -84,12 +89,18 @@ fun looksLikeDiff(text: String): Boolean {
84 return false
89 return false
92+private const val COLLAPSED_DIFF_LINES = 500
88 * Renders a parsed unified diff. Implemented as a plain (non-lazy) [Column] so it is
95 * Renders a parsed unified diff. Implemented as a plain (non-lazy) [Column] so it is
89 * safe to embed inside another vertically scrolling container (e.g. the chat message
96 * safe to embed inside another vertically scrolling container (e.g. the chat message
90 * list) — a nested `LazyColumn` would crash with an unbounded-height constraint.
97 * list) — a nested `LazyColumn` would crash with an unbounded-height constraint.
91 * Callers that want the view to scroll on its own (e.g. the file viewer) should pass a
98 * Callers that want the view to scroll on its own (e.g. the file viewer) should pass a
92 * [Modifier.verticalScroll] in [modifier].
99 * [Modifier.verticalScroll] in [modifier].
101+ * To avoid composing thousands of [Text] nodes at once (which can ANR/OOM on large
102+ * diffs), only the first [COLLAPSED_DIFF_LINES] lines are rendered unless the user
94@Composable
105@Composable
95fun DiffView(diff: String, modifier: Modifier = Modifier) {
106fun DiffView(diff: String, modifier: Modifier = Modifier) {
@@ -100,6 +111,7 @@ fun DiffView(diff: String, modifier: Modifier = Modifier) {
100 val removeText = MaterialTheme.colorScheme.error
111 val removeText = MaterialTheme.colorScheme.error
101 val context = LocalContext.current
112 val context = LocalContext.current
102 val hScrollState = rememberScrollState()
113 val hScrollState = rememberScrollState()
114+ var expanded by rememberSaveable(diff) { mutableStateOf(false) }
105 modifier = modifier
117 modifier = modifier
@@ -115,7 +127,9 @@ fun DiffView(diff: String, modifier: Modifier = Modifier) {
115 Icon(Icons.Filled.ContentCopy, contentDescription = stringResource(R.string.copy), tint = MaterialTheme.colorScheme.onSurfaceVariant)
127 Icon(Icons.Filled.ContentCopy, contentDescription = stringResource(R.string.copy), tint = MaterialTheme.colorScheme.onSurfaceVariant)
130+ val visibleLines = if (lines.size <= COLLAPSED_DIFF_LINES || expanded) lines
131+ else lines.subList(0, COLLAPSED_DIFF_LINES)
118− lines.forEach { line ->
132+ visibleLines.forEach { line ->
119 Row(modifier = Modifier.horizontalScroll(hScrollState).padding(horizontal = 10.dp)) {
133 Row(modifier = Modifier.horizontalScroll(hScrollState).padding(horizontal = 10.dp)) {
120 when (line) {
134 when (line) {
121 is DiffLine.Hunk -> Text(
135 is DiffLine.Hunk -> Text(
@@ -145,6 +159,14 @@ fun DiffView(diff: String, modifier: Modifier = Modifier) {
162+ if (lines.size > COLLAPSED_DIFF_LINES && !expanded) {
164+ onClick = { expanded = true },
165+ contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 10.dp),
167+ Text(stringResource(R.string.show_more))
148 Spacer(Modifier.height(10.dp))
170 Spacer(Modifier.height(10.dp))