Simplified split hunk generation & added to file history diff

This commit is contained in:
Abdelilah El Aissaoui 2022-08-15 03:52:36 +02:00
parent e550a6289c
commit 7506c79b63
7 changed files with 183 additions and 110 deletions

View File

@ -0,0 +1,13 @@
package app.extensions
fun <T> Array<T>.matchingIndexes(filter: (T) -> Boolean): List<Int> {
val matchingIndexes = mutableListOf<Int>()
this.forEachIndexed { index, item ->
if (filter(item)) {
matchingIndexes.add(index)
}
}
return matchingIndexes
}

View File

@ -13,3 +13,4 @@ fun <T> flatListOf(vararg lists: List<T>): List<T> {
return flatList
}

View File

@ -2,7 +2,7 @@ package app.git.diff
data class Hunk(val header: String, val lines: List<Line>)
data class SplitHunk(val hunk: Hunk, val lines: List<Pair<Line?, Line?>>)
data class SplitHunk(val sourceHunk: Hunk, val lines: List<Pair<Line?, Line?>>)
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!

View File

@ -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()

View File

@ -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<SplitHunk> {
val unifiedHunksList = diffFormat.hunks
val hunksList = mutableListOf<SplitHunk>()
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<Line?> = 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<Pair<Line?, Line?>>()
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<Line>,
lineNumberCallback: (Line) -> Int,
linesCount: Int
): Array<Line?> {
val linesArray = arrayOfNulls<Line?>(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<Line?>, hunkLines: List<Line>, 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
}
}

View File

@ -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>(ViewDiffResult.Loading(""))
val diffResult: StateFlow<ViewDiffResult?> = _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<SplitHunk> {
val unifiedHunksList = diffFormat.hunks
val hunksList = mutableListOf<SplitHunk>()
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<Line?>(maxLinesCountOfBothParts)
val newLinesArray = arrayOfNulls<Line?>(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<Pair<Line?, Line?>>()
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 ->

View File

@ -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>(HistoryState.Loading(""))
val historyState: StateFlow<HistoryState> = _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)