Require native Git runtime and bundle full manpages

- Remove the app fallback path when native Git is unavailable and show a startup blocker instead
- Fix native level setup for staged file counting and cherry-pick parity
- Improve manpage loading/search and bundle full Git documentation assets
- Integrate Git cross-compilation into AndroidProjectTooling.sh and remove debug AAB support
This commit is contained in:
Joe Tretter
2026-05-08 17:43:46 -05:00
parent 64743ff4a9
commit 4fab442939
219 changed files with 58133 additions and 129 deletions

View File

@@ -27,6 +27,9 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@@ -38,6 +41,13 @@ data class ManPageState(
val content: String,
)
private data class ManPageMatch(
val start: Int,
val end: Int,
val lineIndex: Int,
val columnIndex: Int,
)
@Composable
fun ManPageDialog(
state: ManPageState,
@@ -52,21 +62,52 @@ fun ManPageDialog(
if (searchQuery.isBlank()) {
emptyList()
} else {
contentLines.mapIndexedNotNull { index, line ->
index.takeIf { line.contains(searchQuery, ignoreCase = true) }
val result = mutableListOf<ManPageMatch>()
var absoluteLineStart = 0
contentLines.forEachIndexed { lineIndex, line ->
var searchFrom = 0
while (searchFrom <= line.length) {
val columnIndex = line.indexOf(searchQuery, searchFrom, ignoreCase = true)
if (columnIndex == -1) break
val start = absoluteLineStart + columnIndex
result += ManPageMatch(
start = start,
end = start + searchQuery.length,
lineIndex = lineIndex,
columnIndex = columnIndex,
)
searchFrom = columnIndex + searchQuery.length.coerceAtLeast(1)
}
absoluteLineStart += line.length + 1
}
result
}
}
val matchCount = matches.size
val currentMatchNumber = if (matchCount == 0) 0 else selectedMatch + 1
val highlightedContent = remember(state.content, matches, selectedMatch) {
AnnotatedString.Builder(state.content).apply {
matches.forEachIndexed { index, match ->
addStyle(
SpanStyle(
color = if (index == selectedMatch) Color.White else Color.Black,
background = if (index == selectedMatch) Color(0xFFD32F2F) else Color(0xFFFFEB3B),
),
match.start,
match.end,
)
}
}.toAnnotatedString()
}
LaunchedEffect(searchQuery, matchCount) {
selectedMatch = selectedMatch.coerceIn(0, (matchCount - 1).coerceAtLeast(0))
}
LaunchedEffect(selectedMatch, matches) {
val lineIndex = matches.getOrNull(selectedMatch) ?: return@LaunchedEffect
verticalScrollState.animateScrollTo((lineIndex * 18).coerceAtMost(verticalScrollState.maxValue))
val match = matches.getOrNull(selectedMatch) ?: return@LaunchedEffect
verticalScrollState.animateScrollTo((match.lineIndex * 18).coerceAtMost(verticalScrollState.maxValue))
horizontalScrollState.animateScrollTo((match.columnIndex * 8).coerceAtMost(horizontalScrollState.maxValue))
}
Dialog(onDismissRequest = onClose) {
@@ -170,7 +211,7 @@ fun ManPageDialog(
) {
SelectionContainer {
Text(
text = state.content,
text = highlightedContent,
modifier = Modifier.widthIn(min = 1200.dp),
color = TextPrimary,
fontFamily = FontFamily.Monospace,