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 = { _, _ -> },
onStageHunk = { _, _ -> },
onResetHunk = { _, _ -> },
onActionTriggered = { _, _, _ -> }
onUnStageLine = { _, _, _ -> },
onDiscardLine = { _, _, _ -> },
)
}
@ -187,7 +188,8 @@ fun HistoryContentLoaded(
onUnstageHunk = { _, _ -> },
onStageHunk = { _, _ -> },
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.items
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.SelectionContainer
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.pointer.PointerEventType
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.painterResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
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.ui.components.ScrollableLazyColumn
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.TextDiffType
import com.jetpackduba.gitnuro.viewmodels.ViewDiffResult
@ -128,11 +137,14 @@ fun Diff(
onResetHunk = { entry, hunk ->
diffViewModel.resetHunk(entry, hunk)
},
onActionTriggered = { entry, hunk, line ->
onUnStageLine = { entry, hunk, line ->
if (diffEntryType is DiffEntryType.UnstagedDiff)
diffViewModel.stageHunkLine(entry, hunk, line)
else if (diffEntryType is DiffEntryType.StagedDiff)
diffViewModel.unstageHunkLine(entry, hunk, line)
},
onDiscardLine = { entry, hunk, line ->
diffViewModel.discardHunkLine(entry, hunk, line)
}
)
@ -149,16 +161,21 @@ fun Diff(
onResetHunk = { entry, hunk ->
diffViewModel.resetHunk(entry, hunk)
},
onActionTriggered = { entry, hunk, line ->
onUnStageLine = { entry, hunk, line ->
if (diffEntryType is DiffEntryType.UnstagedDiff)
diffViewModel.stageHunkLine(entry, hunk, line)
else if (diffEntryType is DiffEntryType.StagedDiff)
diffViewModel.unstageHunkLine(entry, hunk, line)
},
onDiscardLine = { entry, hunk, line ->
diffViewModel.discardHunkLine(entry, hunk, line)
}
)
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,
onOpenFileWithExternalApp = { onOpenFileWithExternalApp(entryContent.imagePath) }
)
else -> {
}
// 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
fun HunkUnifiedTextDiff(
diffEntryType: DiffEntryType,
@ -344,11 +363,20 @@ fun HunkUnifiedTextDiff(
diffResult: DiffResult.Text,
onUnstageHunk: (DiffEntry, Hunk) -> Unit,
onStageHunk: (DiffEntry, Hunk) -> Unit,
onActionTriggered: (DiffEntry, Hunk, Line) -> Unit,
onUnStageLine: (DiffEntry, Hunk, Line) -> Unit,
onResetHunk: (DiffEntry, Hunk) -> Unit,
onDiscardLine: (DiffEntry, Hunk, Line) -> Unit,
) {
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 {
ScrollableLazyColumn(
modifier = Modifier
@ -374,12 +402,20 @@ fun HunkUnifiedTextDiff(
val highestLineNumberLength = highestLineNumber.toString().count()
items(hunk.lines) { line ->
DiffContextMenu(
selectedText = selectedText,
localization = localization,
localClipboardManager = localClipboardManager,
diffEntryType = diffEntryType,
onDiscardLine = { onDiscardLine(diffResult.diffEntry, hunk, line) },
line = line,
) {
DiffLine(
highestLineNumberLength,
line,
diffEntryType = diffEntryType,
onActionTriggered = {
onActionTriggered(
onUnStageLine(
diffResult.diffEntry,
hunk,
line,
@ -390,9 +426,11 @@ fun HunkUnifiedTextDiff(
}
}
}
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun HunkSplitTextDiff(
diffEntryType: DiffEntryType,
@ -401,7 +439,8 @@ fun HunkSplitTextDiff(
onUnstageHunk: (DiffEntry, Hunk) -> Unit,
onStageHunk: (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
@ -410,6 +449,13 @@ fun HunkSplitTextDiff(
*/
var selectableSide by remember { mutableStateOf(SelectableSide.BOTH) }
var selectedText by remember { mutableStateOf(AnnotatedString("")) }
CompositionLocalProvider(
LocalTextContextMenu provides CustomTextContextMenu {
selectedText = it
}
) {
SelectionContainer {
ScrollableLazyColumn(
modifier = Modifier
@ -441,13 +487,17 @@ fun HunkSplitTextDiff(
newLine = linesPair.second,
selectableSide = selectableSide,
diffEntryType = diffEntryType,
selectedText = selectedText,
onActionTriggered = { line ->
onActionTriggered(diffResult.diffEntry, splitHunk.sourceHunk, line)
onUnStageLine(diffResult.diffEntry, splitHunk.sourceHunk, line)
},
onChangeSelectableSide = { newSelectableSide ->
if (newSelectableSide != selectableSide) {
selectableSide = newSelectableSide
}
},
onDiscardLine = { line ->
onDiscardLine(diffResult.diffEntry, splitHunk.sourceHunk, line)
}
)
}
@ -455,6 +505,7 @@ fun HunkSplitTextDiff(
}
}
}
}
@Composable
fun DynamicSelectionDisable(isDisabled: Boolean, content: @Composable () -> Unit) {
@ -471,8 +522,10 @@ fun SplitDiffLine(
newLine: Line?,
selectableSide: SelectableSide,
diffEntryType: DiffEntryType,
selectedText: AnnotatedString,
onChangeSelectableSide: (SelectableSide) -> Unit,
onActionTriggered: (Line) -> Unit,
onDiscardLine: (Line) -> Unit,
) {
Row(
modifier = Modifier
@ -489,7 +542,9 @@ fun SplitDiffLine(
lineSelectableSide = SelectableSide.OLD,
onChangeSelectableSide = onChangeSelectableSide,
diffEntryType = diffEntryType,
onActionTriggered = { if (oldLine != null) onActionTriggered(oldLine) }
onActionTriggered = { if (oldLine != null) onActionTriggered(oldLine) },
selectedText = selectedText,
onDiscardLine = onDiscardLine,
)
Box(
@ -509,7 +564,9 @@ fun SplitDiffLine(
lineSelectableSide = SelectableSide.NEW,
onChangeSelectableSide = onChangeSelectableSide,
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,
lineSelectableSide: SelectableSide,
diffEntryType: DiffEntryType,
selectedText: AnnotatedString,
onChangeSelectableSide: (SelectableSide) -> Unit,
onActionTriggered: () -> Unit,
onDiscardLine: (Line) -> Unit,
) {
var pressedAndMoved by remember(line) { mutableStateOf(Pair(false, false)) }
var movesCount by remember(line) { mutableStateOf(0) }
val localClipboardManager = LocalClipboardManager.current
val localization = LocalLocalization.current
Box(
modifier = modifier
.onPointerEvent(PointerEventType.Press) {
@ -563,6 +627,14 @@ fun SplitDiffLineSide(
DynamicSelectionDisable(
currentSelectableSide != lineSelectableSide &&
currentSelectableSide != SelectableSide.BOTH
) {
DiffContextMenu(
selectedText,
localization,
localClipboardManager,
line,
diffEntryType,
onDiscardLine = { onDiscardLine(line) },
) {
SplitDiffLine(
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 {
BOTH,

View File

@ -34,6 +34,7 @@ class DiffViewModel @Inject constructor(
private val openFileInExternalAppUseCase: OpenFileInExternalAppUseCase,
private val settings: AppSettings,
private val generateSplitHunkFromDiffResultUseCase: GenerateSplitHunkFromDiffResultUseCase,
private val discardUnstagedHunkLineUseCase: DiscardUnstagedHunkLineUseCase,
tabScope: CoroutineScope,
) {
private val _diffResult = MutableStateFlow<ViewDiffResult>(ViewDiffResult.Loading(""))
@ -192,6 +193,13 @@ class DiffViewModel @Inject constructor(
fun openFileWithExternalApp(path: String) {
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) {