Added option to discard lines individually

Fixes #21
This commit is contained in:
Abdelilah El Aissaoui 2023-04-27 18:24:49 +02:00
parent 07bb331daf
commit 1453c6f356
No known key found for this signature in database
GPG Key ID: 7587FC860F594869
5 changed files with 311 additions and 91 deletions

View File

@ -0,0 +1,63 @@
package com.jetpackduba.gitnuro.git.workspace
import com.jetpackduba.gitnuro.extensions.lineDelimiter
import com.jetpackduba.gitnuro.git.diff.Hunk
import com.jetpackduba.gitnuro.git.diff.Line
import com.jetpackduba.gitnuro.git.diff.LineType
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.diff.DiffEntry
import java.io.File
import java.io.FileWriter
import javax.inject.Inject
class DiscardUnstagedHunkLineUseCase @Inject constructor(
private val getLinesFromTextUseCase: GetLinesFromTextUseCase,
) {
suspend operator fun invoke(git: Git, diffEntry: DiffEntry, hunk: Hunk, line: Line) =
withContext(Dispatchers.IO) {
val repository = git.repository
try {
val file = File(repository.directory.parent, diffEntry.oldPath)
val content = file.readText()
val textLines = getLinesFromTextUseCase(content, content.lineDelimiter).toMutableList()
if (line.lineType == LineType.ADDED) {
textLines.removeAt(line.newLineNumber)
} else if (line.lineType == LineType.REMOVED) {
val previousContextLine = hunk.lines
.takeWhile { it != line }
.lastOrNull { it.lineType == LineType.CONTEXT }
if (previousContextLine != null) {
textLines.add(previousContextLine.newLineNumber + 1, line.text)
} else {
val previousAddedLine = hunk.lines
.takeWhile { it != line }
.lastOrNull { it.lineType == LineType.ADDED }
if (previousAddedLine != null) {
textLines.add(previousAddedLine.newLineNumber + 1, line.text)
} else {
textLines.add(0, line.text)
}
}
}
val resultText = textLines.joinToString("")
FileWriter(file).use { fw ->
fw.write(resultText)
}
} catch (ex: Exception) {
throw Exception(
"Discard hunk line failed. Check if the file still exists and has the write permissions set",
ex
)
}
}
}

View File

@ -175,7 +175,8 @@ fun HistoryContentLoaded(
onUnstageHunk = { _, _ -> }, onUnstageHunk = { _, _ -> },
onStageHunk = { _, _ -> }, onStageHunk = { _, _ -> },
onResetHunk = { _, _ -> }, onResetHunk = { _, _ -> },
onActionTriggered = { _, _, _ -> } onUnStageLine = { _, _, _ -> },
onDiscardLine = { _, _, _ -> },
) )
} }
@ -187,7 +188,8 @@ fun HistoryContentLoaded(
onUnstageHunk = { _, _ -> }, onUnstageHunk = { _, _ -> },
onStageHunk = { _, _ -> }, onStageHunk = { _, _ -> },
onResetHunk = { _, _ -> }, onResetHunk = { _, _ -> },
onActionTriggered = { _, _, _ -> }, onUnStageLine = { _, _, _ -> },
onDiscardLine = { _, _, _ -> },
) )
} }

View File

@ -0,0 +1,26 @@
package com.jetpackduba.gitnuro.ui.context_menu
import androidx.compose.foundation.ContextMenuState
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.text.TextContextMenu
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.AnnotatedString
@OptIn(ExperimentalFoundationApi::class)
class CustomTextContextMenu(val onIsTextSelected: (AnnotatedString) -> Unit) : TextContextMenu {
@Composable
override fun Area(
textManager: TextContextMenu.TextManager,
state: ContextMenuState,
content: @Composable () -> Unit
) {
try {
// For some reason, compose crashes internally when calling selectedText the first time Area is composed
onIsTextSelected(textManager.selectedText)
} catch (ex: Exception) {
println("Selected text check failed " + ex.message)
}
content()
}
}

View File

@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.LocalTextContextMenu
import androidx.compose.foundation.text.selection.DisableSelection import androidx.compose.foundation.text.selection.DisableSelection
import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.* import androidx.compose.material.*
@ -27,8 +28,13 @@ import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type import androidx.compose.ui.input.key.type
import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.onPointerEvent import androidx.compose.ui.input.pointer.onPointerEvent
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalLocalization
import androidx.compose.ui.platform.PlatformLocalization
import androidx.compose.ui.res.loadImageBitmap import androidx.compose.ui.res.loadImageBitmap
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -49,6 +55,9 @@ import com.jetpackduba.gitnuro.keybindings.matchesBinding
import com.jetpackduba.gitnuro.theme.* import com.jetpackduba.gitnuro.theme.*
import com.jetpackduba.gitnuro.ui.components.ScrollableLazyColumn import com.jetpackduba.gitnuro.ui.components.ScrollableLazyColumn
import com.jetpackduba.gitnuro.ui.components.SecondaryButton import com.jetpackduba.gitnuro.ui.components.SecondaryButton
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenuElement
import com.jetpackduba.gitnuro.ui.context_menu.CustomTextContextMenu
import com.jetpackduba.gitnuro.viewmodels.DiffViewModel import com.jetpackduba.gitnuro.viewmodels.DiffViewModel
import com.jetpackduba.gitnuro.viewmodels.TextDiffType import com.jetpackduba.gitnuro.viewmodels.TextDiffType
import com.jetpackduba.gitnuro.viewmodels.ViewDiffResult import com.jetpackduba.gitnuro.viewmodels.ViewDiffResult
@ -128,11 +137,14 @@ fun Diff(
onResetHunk = { entry, hunk -> onResetHunk = { entry, hunk ->
diffViewModel.resetHunk(entry, hunk) diffViewModel.resetHunk(entry, hunk)
}, },
onActionTriggered = { entry, hunk, line -> onUnStageLine = { entry, hunk, line ->
if (diffEntryType is DiffEntryType.UnstagedDiff) if (diffEntryType is DiffEntryType.UnstagedDiff)
diffViewModel.stageHunkLine(entry, hunk, line) diffViewModel.stageHunkLine(entry, hunk, line)
else if (diffEntryType is DiffEntryType.StagedDiff) else if (diffEntryType is DiffEntryType.StagedDiff)
diffViewModel.unstageHunkLine(entry, hunk, line) diffViewModel.unstageHunkLine(entry, hunk, line)
},
onDiscardLine = { entry, hunk, line ->
diffViewModel.discardHunkLine(entry, hunk, line)
} }
) )
@ -149,16 +161,21 @@ fun Diff(
onResetHunk = { entry, hunk -> onResetHunk = { entry, hunk ->
diffViewModel.resetHunk(entry, hunk) diffViewModel.resetHunk(entry, hunk)
}, },
onActionTriggered = { entry, hunk, line -> onUnStageLine = { entry, hunk, line ->
if (diffEntryType is DiffEntryType.UnstagedDiff) if (diffEntryType is DiffEntryType.UnstagedDiff)
diffViewModel.stageHunkLine(entry, hunk, line) diffViewModel.stageHunkLine(entry, hunk, line)
else if (diffEntryType is DiffEntryType.StagedDiff) else if (diffEntryType is DiffEntryType.StagedDiff)
diffViewModel.unstageHunkLine(entry, hunk, line) diffViewModel.unstageHunkLine(entry, hunk, line)
},
onDiscardLine = { entry, hunk, line ->
diffViewModel.discardHunkLine(entry, hunk, line)
} }
) )
is DiffResult.NonText -> { is DiffResult.NonText -> {
NonTextDiff(diffResult, onOpenFileWithExternalApp = { path -> diffViewModel.openFileWithExternalApp(path) }) NonTextDiff(
diffResult,
onOpenFileWithExternalApp = { path -> diffViewModel.openFileWithExternalApp(path) })
} }
} }
} }
@ -256,6 +273,7 @@ fun SideDiff(entryContent: EntryContent, onOpenFileWithExternalApp: (String) ->
entryContent.contentType, entryContent.contentType,
onOpenFileWithExternalApp = { onOpenFileWithExternalApp(entryContent.imagePath) } onOpenFileWithExternalApp = { onOpenFileWithExternalApp(entryContent.imagePath) }
) )
else -> { else -> {
} }
// is EntryContent.Text -> //TODO maybe have a text view if the file was a binary before? // is EntryContent.Text -> //TODO maybe have a text view if the file was a binary before?
@ -337,6 +355,7 @@ fun BinaryDiff() {
} }
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun HunkUnifiedTextDiff( fun HunkUnifiedTextDiff(
diffEntryType: DiffEntryType, diffEntryType: DiffEntryType,
@ -344,11 +363,20 @@ fun HunkUnifiedTextDiff(
diffResult: DiffResult.Text, diffResult: DiffResult.Text,
onUnstageHunk: (DiffEntry, Hunk) -> Unit, onUnstageHunk: (DiffEntry, Hunk) -> Unit,
onStageHunk: (DiffEntry, Hunk) -> Unit, onStageHunk: (DiffEntry, Hunk) -> Unit,
onActionTriggered: (DiffEntry, Hunk, Line) -> Unit, onUnStageLine: (DiffEntry, Hunk, Line) -> Unit,
onResetHunk: (DiffEntry, Hunk) -> Unit, onResetHunk: (DiffEntry, Hunk) -> Unit,
onDiscardLine: (DiffEntry, Hunk, Line) -> Unit,
) { ) {
val hunks = diffResult.hunks val hunks = diffResult.hunks
var selectedText by remember { mutableStateOf(AnnotatedString("")) }
val localClipboardManager = LocalClipboardManager.current
val localization = LocalLocalization.current
CompositionLocalProvider(
LocalTextContextMenu provides CustomTextContextMenu {
selectedText = it
}
) {
SelectionContainer { SelectionContainer {
ScrollableLazyColumn( ScrollableLazyColumn(
modifier = Modifier modifier = Modifier
@ -374,12 +402,20 @@ fun HunkUnifiedTextDiff(
val highestLineNumberLength = highestLineNumber.toString().count() val highestLineNumberLength = highestLineNumber.toString().count()
items(hunk.lines) { line -> items(hunk.lines) { line ->
DiffContextMenu(
selectedText = selectedText,
localization = localization,
localClipboardManager = localClipboardManager,
diffEntryType = diffEntryType,
onDiscardLine = { onDiscardLine(diffResult.diffEntry, hunk, line) },
line = line,
) {
DiffLine( DiffLine(
highestLineNumberLength, highestLineNumberLength,
line, line,
diffEntryType = diffEntryType, diffEntryType = diffEntryType,
onActionTriggered = { onActionTriggered = {
onActionTriggered( onUnStageLine(
diffResult.diffEntry, diffResult.diffEntry,
hunk, hunk,
line, line,
@ -390,9 +426,11 @@ fun HunkUnifiedTextDiff(
} }
} }
} }
}
}
} }
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun HunkSplitTextDiff( fun HunkSplitTextDiff(
diffEntryType: DiffEntryType, diffEntryType: DiffEntryType,
@ -401,7 +439,8 @@ fun HunkSplitTextDiff(
onUnstageHunk: (DiffEntry, Hunk) -> Unit, onUnstageHunk: (DiffEntry, Hunk) -> Unit,
onStageHunk: (DiffEntry, Hunk) -> Unit, onStageHunk: (DiffEntry, Hunk) -> Unit,
onResetHunk: (DiffEntry, Hunk) -> Unit, onResetHunk: (DiffEntry, Hunk) -> Unit,
onActionTriggered: (DiffEntry, Hunk, Line) -> Unit, onUnStageLine: (DiffEntry, Hunk, Line) -> Unit,
onDiscardLine: (DiffEntry, Hunk, Line) -> Unit,
) { ) {
val hunks = diffResult.hunks val hunks = diffResult.hunks
@ -410,6 +449,13 @@ fun HunkSplitTextDiff(
*/ */
var selectableSide by remember { mutableStateOf(SelectableSide.BOTH) } var selectableSide by remember { mutableStateOf(SelectableSide.BOTH) }
var selectedText by remember { mutableStateOf(AnnotatedString("")) }
CompositionLocalProvider(
LocalTextContextMenu provides CustomTextContextMenu {
selectedText = it
}
) {
SelectionContainer { SelectionContainer {
ScrollableLazyColumn( ScrollableLazyColumn(
modifier = Modifier modifier = Modifier
@ -441,13 +487,17 @@ fun HunkSplitTextDiff(
newLine = linesPair.second, newLine = linesPair.second,
selectableSide = selectableSide, selectableSide = selectableSide,
diffEntryType = diffEntryType, diffEntryType = diffEntryType,
selectedText = selectedText,
onActionTriggered = { line -> onActionTriggered = { line ->
onActionTriggered(diffResult.diffEntry, splitHunk.sourceHunk, line) onUnStageLine(diffResult.diffEntry, splitHunk.sourceHunk, line)
}, },
onChangeSelectableSide = { newSelectableSide -> onChangeSelectableSide = { newSelectableSide ->
if (newSelectableSide != selectableSide) { if (newSelectableSide != selectableSide) {
selectableSide = newSelectableSide selectableSide = newSelectableSide
} }
},
onDiscardLine = { line ->
onDiscardLine(diffResult.diffEntry, splitHunk.sourceHunk, line)
} }
) )
} }
@ -455,6 +505,7 @@ fun HunkSplitTextDiff(
} }
} }
} }
}
@Composable @Composable
fun DynamicSelectionDisable(isDisabled: Boolean, content: @Composable () -> Unit) { fun DynamicSelectionDisable(isDisabled: Boolean, content: @Composable () -> Unit) {
@ -471,8 +522,10 @@ fun SplitDiffLine(
newLine: Line?, newLine: Line?,
selectableSide: SelectableSide, selectableSide: SelectableSide,
diffEntryType: DiffEntryType, diffEntryType: DiffEntryType,
selectedText: AnnotatedString,
onChangeSelectableSide: (SelectableSide) -> Unit, onChangeSelectableSide: (SelectableSide) -> Unit,
onActionTriggered: (Line) -> Unit, onActionTriggered: (Line) -> Unit,
onDiscardLine: (Line) -> Unit,
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@ -489,7 +542,9 @@ fun SplitDiffLine(
lineSelectableSide = SelectableSide.OLD, lineSelectableSide = SelectableSide.OLD,
onChangeSelectableSide = onChangeSelectableSide, onChangeSelectableSide = onChangeSelectableSide,
diffEntryType = diffEntryType, diffEntryType = diffEntryType,
onActionTriggered = { if (oldLine != null) onActionTriggered(oldLine) } onActionTriggered = { if (oldLine != null) onActionTriggered(oldLine) },
selectedText = selectedText,
onDiscardLine = onDiscardLine,
) )
Box( Box(
@ -509,7 +564,9 @@ fun SplitDiffLine(
lineSelectableSide = SelectableSide.NEW, lineSelectableSide = SelectableSide.NEW,
onChangeSelectableSide = onChangeSelectableSide, onChangeSelectableSide = onChangeSelectableSide,
diffEntryType = diffEntryType, diffEntryType = diffEntryType,
onActionTriggered = { if (newLine != null) onActionTriggered(newLine) } onActionTriggered = { if (newLine != null) onActionTriggered(newLine) },
selectedText = selectedText,
onDiscardLine = onDiscardLine,
) )
} }
@ -525,11 +582,18 @@ fun SplitDiffLineSide(
currentSelectableSide: SelectableSide, currentSelectableSide: SelectableSide,
lineSelectableSide: SelectableSide, lineSelectableSide: SelectableSide,
diffEntryType: DiffEntryType, diffEntryType: DiffEntryType,
selectedText: AnnotatedString,
onChangeSelectableSide: (SelectableSide) -> Unit, onChangeSelectableSide: (SelectableSide) -> Unit,
onActionTriggered: () -> Unit, onActionTriggered: () -> Unit,
onDiscardLine: (Line) -> Unit,
) { ) {
var pressedAndMoved by remember(line) { mutableStateOf(Pair(false, false)) } var pressedAndMoved by remember(line) { mutableStateOf(Pair(false, false)) }
var movesCount by remember(line) { mutableStateOf(0) } var movesCount by remember(line) { mutableStateOf(0) }
val localClipboardManager = LocalClipboardManager.current
val localization = LocalLocalization.current
Box( Box(
modifier = modifier modifier = modifier
.onPointerEvent(PointerEventType.Press) { .onPointerEvent(PointerEventType.Press) {
@ -563,6 +627,14 @@ fun SplitDiffLineSide(
DynamicSelectionDisable( DynamicSelectionDisable(
currentSelectableSide != lineSelectableSide && currentSelectableSide != lineSelectableSide &&
currentSelectableSide != SelectableSide.BOTH currentSelectableSide != SelectableSide.BOTH
) {
DiffContextMenu(
selectedText,
localization,
localClipboardManager,
line,
diffEntryType,
onDiscardLine = { onDiscardLine(line) },
) { ) {
SplitDiffLine( SplitDiffLine(
highestLineNumberLength = highestLineNumberLength, highestLineNumberLength = highestLineNumberLength,
@ -575,6 +647,55 @@ fun SplitDiffLineSide(
} }
} }
} }
}
@Composable
fun DiffContextMenu(
selectedText: AnnotatedString,
localization: PlatformLocalization,
localClipboardManager: ClipboardManager,
line: Line,
diffEntryType: DiffEntryType,
onDiscardLine: () -> Unit,
content: @Composable () -> Unit,
) {
ContextMenu(
items = {
val isTextSelected = selectedText.isNotEmpty()
if (isTextSelected) {
listOf(
ContextMenuElement.ContextTextEntry(
label = localization.copy,
icon = { painterResource(AppIcons.COPY) },
onClick = {
localClipboardManager.setText(selectedText)
}
)
)
} else {
if (
line.lineType != LineType.CONTEXT &&
diffEntryType is DiffEntryType.UnstagedDiff &&
diffEntryType.statusType == StatusType.MODIFIED
) {
listOf(
ContextMenuElement.ContextTextEntry(
label = "Discard line",
icon = { painterResource(AppIcons.UNDO) },
onClick = {
onDiscardLine()
}
)
)
} else
emptyList()
}
},
) {
content()
}
}
enum class SelectableSide { enum class SelectableSide {
BOTH, BOTH,

View File

@ -34,6 +34,7 @@ class DiffViewModel @Inject constructor(
private val openFileInExternalAppUseCase: OpenFileInExternalAppUseCase, private val openFileInExternalAppUseCase: OpenFileInExternalAppUseCase,
private val settings: AppSettings, private val settings: AppSettings,
private val generateSplitHunkFromDiffResultUseCase: GenerateSplitHunkFromDiffResultUseCase, private val generateSplitHunkFromDiffResultUseCase: GenerateSplitHunkFromDiffResultUseCase,
private val discardUnstagedHunkLineUseCase: DiscardUnstagedHunkLineUseCase,
tabScope: CoroutineScope, tabScope: CoroutineScope,
) { ) {
private val _diffResult = MutableStateFlow<ViewDiffResult>(ViewDiffResult.Loading("")) private val _diffResult = MutableStateFlow<ViewDiffResult>(ViewDiffResult.Loading(""))
@ -192,6 +193,13 @@ class DiffViewModel @Inject constructor(
fun openFileWithExternalApp(path: String) { fun openFileWithExternalApp(path: String) {
openFileInExternalAppUseCase(path) openFileInExternalAppUseCase(path)
} }
fun discardHunkLine(entry: DiffEntry, hunk: Hunk, line: Line) = tabState.runOperation(
refreshType = RefreshType.UNCOMMITED_CHANGES,
showError = true,
) { git ->
discardUnstagedHunkLineUseCase(git, entry, hunk, line)
}
} }
enum class TextDiffType(val value: Int) { enum class TextDiffType(val value: Int) {