From 7506c79b63169d2fbe43f34fee0c1dfdd3c61805 Mon Sep 17 00:00:00 2001 From: Abdelilah El Aissaoui Date: Mon, 15 Aug 2022 03:52:36 +0200 Subject: [PATCH] Simplified split hunk generation & added to file history diff --- .../kotlin/app/extensions/ArrayExtensions.kt | 13 +++ .../kotlin/app/extensions/ListExtensions.kt | 3 +- src/main/kotlin/app/git/diff/Hunk.kt | 2 +- src/main/kotlin/app/ui/diff/Diff.kt | 12 +- .../GenerateSplitHunkFromDiffResultUseCase.kt | 107 ++++++++++++++++++ .../kotlin/app/viewmodels/DiffViewModel.kt | 100 +--------------- .../kotlin/app/viewmodels/HistoryViewModel.kt | 56 +++++++-- 7 files changed, 183 insertions(+), 110 deletions(-) create mode 100644 src/main/kotlin/app/extensions/ArrayExtensions.kt create mode 100644 src/main/kotlin/app/usecase/GenerateSplitHunkFromDiffResultUseCase.kt diff --git a/src/main/kotlin/app/extensions/ArrayExtensions.kt b/src/main/kotlin/app/extensions/ArrayExtensions.kt new file mode 100644 index 0000000..d6e587a --- /dev/null +++ b/src/main/kotlin/app/extensions/ArrayExtensions.kt @@ -0,0 +1,13 @@ +package app.extensions + +fun Array.matchingIndexes(filter: (T) -> Boolean): List { + val matchingIndexes = mutableListOf() + + this.forEachIndexed { index, item -> + if (filter(item)) { + matchingIndexes.add(index) + } + } + + return matchingIndexes +} \ No newline at end of file diff --git a/src/main/kotlin/app/extensions/ListExtensions.kt b/src/main/kotlin/app/extensions/ListExtensions.kt index 899b863..f9e7bc0 100644 --- a/src/main/kotlin/app/extensions/ListExtensions.kt +++ b/src/main/kotlin/app/extensions/ListExtensions.kt @@ -12,4 +12,5 @@ fun flatListOf(vararg lists: List): List { } return flatList -} \ No newline at end of file +} + diff --git a/src/main/kotlin/app/git/diff/Hunk.kt b/src/main/kotlin/app/git/diff/Hunk.kt index 5472f20..58bd984 100644 --- a/src/main/kotlin/app/git/diff/Hunk.kt +++ b/src/main/kotlin/app/git/diff/Hunk.kt @@ -2,7 +2,7 @@ package app.git.diff data class Hunk(val header: String, val lines: List) -data class SplitHunk(val hunk: Hunk, val lines: List>) +data class SplitHunk(val sourceHunk: Hunk, val lines: List>) data class Line(val text: String, val oldLineNumber: Int, val newLineNumber: Int, val lineType: LineType) { // lines numbers are stored based on 0 being the first one but on a file the first line is the 1, so increment it! diff --git a/src/main/kotlin/app/ui/diff/Diff.kt b/src/main/kotlin/app/ui/diff/Diff.kt index 1d4bc4e..0a44b36 100644 --- a/src/main/kotlin/app/ui/diff/Diff.kt +++ b/src/main/kotlin/app/ui/diff/Diff.kt @@ -317,17 +317,17 @@ fun HunkSplitTextDiff( item { DisableSelection { HunkHeader( - header = splitHunk.hunk.header, + header = splitHunk.sourceHunk.header, diffEntryType = diffEntryType, - onUnstageHunk = { onUnstageHunk(diffResult.diffEntry, splitHunk.hunk) }, - onStageHunk = { onStageHunk(diffResult.diffEntry, splitHunk.hunk) }, - onResetHunk = { onResetHunk(diffResult.diffEntry, splitHunk.hunk) }, + onUnstageHunk = { onUnstageHunk(diffResult.diffEntry, splitHunk.sourceHunk) }, + onStageHunk = { onStageHunk(diffResult.diffEntry, splitHunk.sourceHunk) }, + onResetHunk = { onResetHunk(diffResult.diffEntry, splitHunk.sourceHunk) }, ) } } - val oldHighestLineNumber = splitHunk.hunk.lines.maxOf { it.displayOldLineNumber } - val newHighestLineNumber = splitHunk.hunk.lines.maxOf { it.displayNewLineNumber } + val oldHighestLineNumber = splitHunk.sourceHunk.lines.maxOf { it.displayOldLineNumber } + val newHighestLineNumber = splitHunk.sourceHunk.lines.maxOf { it.displayNewLineNumber } val highestLineNumber = max(oldHighestLineNumber, newHighestLineNumber) val highestLineNumberLength = highestLineNumber.toString().count() diff --git a/src/main/kotlin/app/usecase/GenerateSplitHunkFromDiffResultUseCase.kt b/src/main/kotlin/app/usecase/GenerateSplitHunkFromDiffResultUseCase.kt new file mode 100644 index 0000000..0ef0704 --- /dev/null +++ b/src/main/kotlin/app/usecase/GenerateSplitHunkFromDiffResultUseCase.kt @@ -0,0 +1,107 @@ +package app.usecase + +import app.extensions.matchingIndexes +import app.git.diff.DiffResult +import app.git.diff.Line +import app.git.diff.LineType +import app.git.diff.SplitHunk +import javax.inject.Inject + +class GenerateSplitHunkFromDiffResultUseCase @Inject constructor() { + operator fun invoke(diffFormat: DiffResult.Text): List { + val unifiedHunksList = diffFormat.hunks + val hunksList = mutableListOf() + + for (hunk in unifiedHunksList) { + val lines = hunk.lines + + val linesNewSideCount = + lines.count { it.lineType == LineType.ADDED || it.lineType == LineType.CONTEXT } + val linesOldSideCount = + lines.count { it.lineType == LineType.REMOVED || it.lineType == LineType.CONTEXT } + + val addedLines = lines.filter { it.lineType == LineType.ADDED } + val removedLines = lines.filter { it.lineType == LineType.REMOVED } + + val oldLinesArray: Array = if (linesNewSideCount > linesOldSideCount) + generateArrayWithContextLines( + hunkLines = lines, + linesCount = linesNewSideCount, + lineNumberCallback = { it.newLineNumber }, + ) + else + generateArrayWithContextLines( + hunkLines = lines, + linesCount = linesOldSideCount, + lineNumberCallback = { it.oldLineNumber }, + ) + + // Old lines array only contains context lines for now, so copy it to new lines array + val newLinesArray = oldLinesArray.copyOf() + + val arraysSize = newLinesArray.count() + + for (removedLine in removedLines) { + placeLine(oldLinesArray, lines, removedLine) + } + + for (addedLine in addedLines) { + placeLine(newLinesArray, lines, addedLine) + } + + val newHunkLines = mutableListOf>() + + for (i in 0 until arraysSize) { + val old = oldLinesArray[i] + val new = newLinesArray[i] + + newHunkLines.add(old to new) + } + + hunksList.add(SplitHunk(hunk, newHunkLines)) + } + + return hunksList + } + + private inline fun generateArrayWithContextLines( + hunkLines: List, + lineNumberCallback: (Line) -> Int, + linesCount: Int + ): Array { + val linesArray = arrayOfNulls(linesCount) + + val contextLines = hunkLines.filter { it.lineType == LineType.CONTEXT } + + val firstLine = hunkLines.firstOrNull() + val firstLineNumber = if (firstLine == null) { + 0 + } else + lineNumberCallback(firstLine) + + for (contextLine in contextLines) { + val lineNumber = lineNumberCallback(contextLine) + + linesArray[lineNumber - firstLineNumber] = contextLine + } + + return linesArray + } + + private fun placeLine(linesArray: Array, hunkLines: List, lineToPlace: Line) { + val previousLinesToCurrent = hunkLines.takeWhile { it != lineToPlace } + val previousContextLine = previousLinesToCurrent.lastOrNull { it.lineType == LineType.CONTEXT } + + val contextArrayPosition = if (previousContextLine != null) + linesArray.indexOf(previousContextLine) + else + -1 + + val availableIndexes = linesArray.matchingIndexes { it == null } + + // Get the position of the next available line after the previous context line + val nextAvailableLinePosition = availableIndexes.first { index -> index > contextArrayPosition } + + linesArray[nextAvailableLinePosition] = lineToPlace + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/viewmodels/DiffViewModel.kt b/src/main/kotlin/app/viewmodels/DiffViewModel.kt index f6cec03..89c379a 100644 --- a/src/main/kotlin/app/viewmodels/DiffViewModel.kt +++ b/src/main/kotlin/app/viewmodels/DiffViewModel.kt @@ -5,15 +5,16 @@ import androidx.compose.foundation.lazy.LazyListState import app.exceptions.MissingDiffEntryException import app.extensions.delayedStateChange import app.git.* -import app.git.diff.* +import app.git.diff.DiffResult +import app.git.diff.Hunk import app.preferences.AppSettings +import app.usecase.GenerateSplitHunkFromDiffResultUseCase import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import org.eclipse.jgit.diff.DiffEntry -import java.lang.Integer.max import javax.inject.Inject private const val DIFF_MIN_TIME_IN_MS_TO_SHOW_LOAD = 200L @@ -23,6 +24,7 @@ class DiffViewModel @Inject constructor( private val diffManager: DiffManager, private val statusManager: StatusManager, private val settings: AppSettings, + private val generateSplitHunkFromDiffResultUseCase: GenerateSplitHunkFromDiffResultUseCase, ) { private val _diffResult = MutableStateFlow(ViewDiffResult.Loading("")) val diffResult: StateFlow = _diffResult @@ -91,7 +93,7 @@ class DiffViewModel @Inject constructor( diffEntry.changeType != DiffEntry.ChangeType.ADD && diffEntry.changeType != DiffEntry.ChangeType.DELETE ) { - val splitHunkList = generateSplitDiffFormat(diffFormat) + val splitHunkList = generateSplitHunkFromDiffResultUseCase(diffFormat) _diffResult.value = ViewDiffResult.Loaded( diffEntryType, DiffResult.TextSplit(diffEntry, splitHunkList) @@ -110,98 +112,6 @@ class DiffViewModel @Inject constructor( } } - private fun generateSplitDiffFormat(diffFormat: DiffResult.Text): List { - val unifiedHunksList = diffFormat.hunks - val hunksList = mutableListOf() - - for (hunk in unifiedHunksList) { - val linesNewSideCount = - hunk.lines.count { it.lineType == LineType.ADDED || it.lineType == LineType.CONTEXT } - val linesOldSideCount = - hunk.lines.count { it.lineType == LineType.REMOVED || it.lineType == LineType.CONTEXT } - - val addedLines = hunk.lines.filter { it.lineType == LineType.ADDED } - val removedLines = hunk.lines.filter { it.lineType == LineType.REMOVED } - - val maxLinesCountOfBothParts = max(linesNewSideCount, linesOldSideCount) - - val oldLinesArray = arrayOfNulls(maxLinesCountOfBothParts) - val newLinesArray = arrayOfNulls(maxLinesCountOfBothParts) - - val lines = hunk.lines - val firstLineOldNumber = hunk.lines.first().oldLineNumber - val firstLineNewNumber = hunk.lines.first().newLineNumber - - val firstLine = if (maxLinesCountOfBothParts == linesOldSideCount) { - firstLineOldNumber - } else - firstLineNewNumber - - val contextLines = lines.filter { it.lineType == LineType.CONTEXT } - - for (contextLine in contextLines) { - - val lineNumber = if (maxLinesCountOfBothParts == linesOldSideCount) { - contextLine.oldLineNumber - } else - contextLine.newLineNumber - - oldLinesArray[lineNumber - firstLine] = contextLine - newLinesArray[lineNumber - firstLine] = contextLine - } - - for (removedLine in removedLines) { - val previousLinesToCurrent = lines.takeWhile { it != removedLine } - val previousContextLine = previousLinesToCurrent.lastOrNull { it.lineType == LineType.CONTEXT } - - val contextArrayPosition = if (previousContextLine != null) - oldLinesArray.indexOf(previousContextLine) - else - -1 - - // Get the position the list of null position of the array - val availableIndexes = - newLinesArray.mapIndexed { index, line -> - if (line != null) - null - else - index - }.filterNotNull() - val nextAvailableLinePosition = availableIndexes.first { index -> index > contextArrayPosition } - oldLinesArray[nextAvailableLinePosition] = removedLine - } - - for (addedLine in addedLines) { - val previousLinesToCurrent = lines.takeWhile { it != addedLine } - val previousContextLine = previousLinesToCurrent.lastOrNull { it.lineType == LineType.CONTEXT } - - val contextArrayPosition = if (previousContextLine != null) - newLinesArray.indexOf(previousContextLine) - else - -1 - - val availableIndexes = - newLinesArray.mapIndexed { index, line -> if (line != null) null else index }.filterNotNull() - val newLinePosition = availableIndexes.first { index -> index > contextArrayPosition } - - newLinesArray[newLinePosition] = addedLine - } - - val newHunkLines = mutableListOf>() - - for (i in 0 until maxLinesCountOfBothParts) { - val old = oldLinesArray[i] - val new = newLinesArray[i] - - newHunkLines.add(old to new) - } - - hunksList.add(SplitHunk(hunk, newHunkLines)) - } - - return hunksList - } - fun stageHunk(diffEntry: DiffEntry, hunk: Hunk) = tabState.runOperation( refreshType = RefreshType.UNCOMMITED_CHANGES, ) { git -> diff --git a/src/main/kotlin/app/viewmodels/HistoryViewModel.kt b/src/main/kotlin/app/viewmodels/HistoryViewModel.kt index 1c2e560..d1e0b66 100644 --- a/src/main/kotlin/app/viewmodels/HistoryViewModel.kt +++ b/src/main/kotlin/app/viewmodels/HistoryViewModel.kt @@ -3,13 +3,14 @@ package app.viewmodels import androidx.compose.foundation.lazy.LazyListState import app.exceptions.MissingDiffEntryException import app.extensions.filePath -import app.git.DiffEntryType -import app.git.DiffManager -import app.git.RefreshType -import app.git.TabState +import app.git.* +import app.git.diff.DiffResult import app.preferences.AppSettings +import app.usecase.GenerateSplitHunkFromDiffResultUseCase import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch import org.eclipse.jgit.revwalk.RevCommit import javax.inject.Inject @@ -17,6 +18,7 @@ class HistoryViewModel @Inject constructor( private val tabState: TabState, private val diffManager: DiffManager, private val settings: AppSettings, + private val generateSplitHunkFromDiffResultUseCase: GenerateSplitHunkFromDiffResultUseCase, ) { private val _historyState = MutableStateFlow(HistoryState.Loading("")) val historyState: StateFlow = _historyState @@ -32,6 +34,40 @@ class HistoryViewModel @Inject constructor( ) ) + + init { + tabState.managerScope.launch { + settings.textDiffTypeFlow.collect { diffType -> + if (filePath.isNotBlank()) { + updateDiffType(diffType) + } + } + } + } + + private fun updateDiffType(newDiffType: TextDiffType) { + val viewDiffResult = this.viewDiffResult.value + + if (viewDiffResult is ViewDiffResult.Loaded) { + val diffResult = viewDiffResult.diffResult + + if (diffResult is DiffResult.Text && newDiffType == TextDiffType.SPLIT) { // Current is unified and new is split + val hunksList = generateSplitHunkFromDiffResultUseCase(diffResult) + _viewDiffResult.value = ViewDiffResult.Loaded( + diffEntryType = viewDiffResult.diffEntryType, + diffResult = DiffResult.TextSplit(diffResult.diffEntry, hunksList) + ) + } else if (diffResult is DiffResult.TextSplit && newDiffType == TextDiffType.UNIFIED) { // Current is split and new is unified + val hunksList = diffResult.hunks.map { it.sourceHunk } + + _viewDiffResult.value = ViewDiffResult.Loaded( + diffEntryType = viewDiffResult.diffEntryType, + diffResult = DiffResult.Text(diffResult.diffEntry, hunksList) + ) + } + } + } + fun fileHistory(filePath: String) = tabState.safeProcessing( refreshType = RefreshType.NONE, ) { git -> @@ -52,7 +88,6 @@ class HistoryViewModel @Inject constructor( ) { git -> try { - val diffEntries = diffManager.commitDiffEntries(git, commit) val diffEntry = diffEntries.firstOrNull { entry -> entry.filePath == this.filePath @@ -62,11 +97,18 @@ class HistoryViewModel @Inject constructor( _viewDiffResult.value = ViewDiffResult.DiffNotFound return@runOperation } + val diffEntryType = DiffEntryType.CommitDiff(diffEntry) - val diffFormat = diffManager.diffFormat(git, diffEntryType) + val diffResult = diffManager.diffFormat(git, diffEntryType) + val textDiffType = settings.textDiffType - _viewDiffResult.value = ViewDiffResult.Loaded(diffEntryType, diffFormat) + val formattedDiffResult = if (textDiffType == TextDiffType.SPLIT && diffResult is DiffResult.Text) { + DiffResult.TextSplit(diffEntry, generateSplitHunkFromDiffResultUseCase(diffResult)) + } else + diffResult + + _viewDiffResult.value = ViewDiffResult.Loaded(diffEntryType, formattedDiffResult) } catch (ex: Exception) { if (ex is MissingDiffEntryException) { tabState.refreshData(refreshType = RefreshType.UNCOMMITED_CHANGES)