Add app-native interactive Git dialogs

- Open an interactive staging dialog for `git add -i`, `git stage -i`, and
  `git add --interactive`, allowing paths to be selected before staging.
- Open an interactive rebase dialog for `git rebase -i` / `--interactive`, with
  pick, squash, drop, commit-message editing, and reorder controls.
- Keep the existing synthetic runtime behavior available for direct engine/test calls,
  while making app-entered interactive commands show real UI instead of auto-staging.
- Center manpage search navigation using actual text-layout highlight bounds instead
  of approximate line and column math.
- Make editor dialogs use a full-screen IME-padded outer scroll container so controls
  remain reachable when the keyboard pans or resizes the dialog.
This commit is contained in:
Joe Tretter
2026-05-09 15:41:55 -05:00
parent 0acf8c9ab0
commit f49e8a96b1
9 changed files with 708 additions and 120 deletions

View File

@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
@@ -33,6 +34,7 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.compose.foundation.text.KeyboardOptions
data class TextEditorState(
@@ -53,75 +55,86 @@ fun TextEditorDialog(
) {
val editorHorizontalScroll = rememberScrollState()
val editorVerticalScroll = rememberScrollState()
val dialogScroll = rememberScrollState()
Dialog(onDismissRequest = onClose) {
Surface(
Dialog(
onDismissRequest = onClose,
properties = DialogProperties(usePlatformDefaultWidth = false),
) {
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.86f)
.imePadding(),
color = PanelPrimary,
shape = RoundedCornerShape(8.dp),
.fillMaxSize()
.imePadding()
.verticalScroll(dialogScroll)
.padding(12.dp),
) {
Column(
modifier = Modifier.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
Surface(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 360.dp, max = 720.dp),
color = PanelPrimary,
shape = RoundedCornerShape(8.dp),
) {
Text(
text = "Edit File",
color = TextPrimary,
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
Column(
modifier = Modifier.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
EditorButton(label = "Close", onClick = onClose)
EditorButton(label = "Save", enabled = state.saveAsPath.isNotBlank(), onClick = onSave)
}
Text(
text = "${state.editor} ${state.path.ifBlank { "<new file>" }}",
color = TextPrimary,
fontWeight = FontWeight.Bold,
)
OutlinedTextField(
value = state.saveAsPath,
onValueChange = onSaveAsPathChange,
modifier = Modifier.fillMaxWidth(),
singleLine = true,
keyboardOptions = KeyboardOptions(
autoCorrect = false,
keyboardType = KeyboardType.Ascii,
imeAction = ImeAction.Done,
),
label = { Text("File name") },
)
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.heightIn(min = 260.dp)
.background(TerminalBackground, RoundedCornerShape(6.dp))
.padding(10.dp)
.horizontalScroll(editorHorizontalScroll)
.verticalScroll(editorVerticalScroll),
) {
BasicTextField(
value = state.content,
onValueChange = onContentChange,
modifier = Modifier.sizeIn(minWidth = 1200.dp, minHeight = 1200.dp),
textStyle = TextStyle(
color = TextPrimary,
fontFamily = FontFamily.Monospace,
fontSize = 14.sp,
),
cursorBrush = SolidColor(Accent),
Text(
text = "Edit File",
color = TextPrimary,
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
EditorButton(label = "Close", onClick = onClose)
EditorButton(label = "Save", enabled = state.saveAsPath.isNotBlank(), onClick = onSave)
}
Text(
text = "${state.editor} ${state.path.ifBlank { "<new file>" }}",
color = TextPrimary,
fontWeight = FontWeight.Bold,
)
OutlinedTextField(
value = state.saveAsPath,
onValueChange = onSaveAsPathChange,
modifier = Modifier.fillMaxWidth(),
singleLine = true,
keyboardOptions = KeyboardOptions(
autoCorrect = false,
keyboardType = KeyboardType.Ascii,
imeAction = ImeAction.Done,
),
label = { Text("File name") },
)
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.heightIn(min = 260.dp)
.background(TerminalBackground, RoundedCornerShape(6.dp))
.padding(10.dp)
.horizontalScroll(editorHorizontalScroll)
.verticalScroll(editorVerticalScroll),
) {
BasicTextField(
value = state.content,
onValueChange = onContentChange,
modifier = Modifier.sizeIn(minWidth = 1200.dp, minHeight = 1200.dp),
textStyle = TextStyle(
color = TextPrimary,
fontFamily = FontFamily.Monospace,
fontSize = 14.sp,
),
cursorBrush = SolidColor(Accent),
keyboardOptions = KeyboardOptions(
autoCorrect = false,
keyboardType = KeyboardType.Ascii,
),
)
}
}
}
}