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

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

View File

@ -2,7 +2,7 @@ package app.git.diff
data class Hunk(val header: String, val lines: List<Line>) 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) { 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! // 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 { item {
DisableSelection { DisableSelection {
HunkHeader( HunkHeader(
header = splitHunk.hunk.header, header = splitHunk.sourceHunk.header,
diffEntryType = diffEntryType, diffEntryType = diffEntryType,
onUnstageHunk = { onUnstageHunk(diffResult.diffEntry, splitHunk.hunk) }, onUnstageHunk = { onUnstageHunk(diffResult.diffEntry, splitHunk.sourceHunk) },
onStageHunk = { onStageHunk(diffResult.diffEntry, splitHunk.hunk) }, onStageHunk = { onStageHunk(diffResult.diffEntry, splitHunk.sourceHunk) },
onResetHunk = { onResetHunk(diffResult.diffEntry, splitHunk.hunk) }, onResetHunk = { onResetHunk(diffResult.diffEntry, splitHunk.sourceHunk) },
) )
} }
} }
val oldHighestLineNumber = splitHunk.hunk.lines.maxOf { it.displayOldLineNumber } val oldHighestLineNumber = splitHunk.sourceHunk.lines.maxOf { it.displayOldLineNumber }
val newHighestLineNumber = splitHunk.hunk.lines.maxOf { it.displayNewLineNumber } val newHighestLineNumber = splitHunk.sourceHunk.lines.maxOf { it.displayNewLineNumber }
val highestLineNumber = max(oldHighestLineNumber, newHighestLineNumber) val highestLineNumber = max(oldHighestLineNumber, newHighestLineNumber)
val highestLineNumberLength = highestLineNumber.toString().count() 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.exceptions.MissingDiffEntryException
import app.extensions.delayedStateChange import app.extensions.delayedStateChange
import app.git.* import app.git.*
import app.git.diff.* import app.git.diff.DiffResult
import app.git.diff.Hunk
import app.preferences.AppSettings import app.preferences.AppSettings
import app.usecase.GenerateSplitHunkFromDiffResultUseCase
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.eclipse.jgit.diff.DiffEntry import org.eclipse.jgit.diff.DiffEntry
import java.lang.Integer.max
import javax.inject.Inject import javax.inject.Inject
private const val DIFF_MIN_TIME_IN_MS_TO_SHOW_LOAD = 200L 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 diffManager: DiffManager,
private val statusManager: StatusManager, private val statusManager: StatusManager,
private val settings: AppSettings, private val settings: AppSettings,
private val generateSplitHunkFromDiffResultUseCase: GenerateSplitHunkFromDiffResultUseCase,
) { ) {
private val _diffResult = MutableStateFlow<ViewDiffResult>(ViewDiffResult.Loading("")) private val _diffResult = MutableStateFlow<ViewDiffResult>(ViewDiffResult.Loading(""))
val diffResult: StateFlow<ViewDiffResult?> = _diffResult val diffResult: StateFlow<ViewDiffResult?> = _diffResult
@ -91,7 +93,7 @@ class DiffViewModel @Inject constructor(
diffEntry.changeType != DiffEntry.ChangeType.ADD && diffEntry.changeType != DiffEntry.ChangeType.ADD &&
diffEntry.changeType != DiffEntry.ChangeType.DELETE diffEntry.changeType != DiffEntry.ChangeType.DELETE
) { ) {
val splitHunkList = generateSplitDiffFormat(diffFormat) val splitHunkList = generateSplitHunkFromDiffResultUseCase(diffFormat)
_diffResult.value = ViewDiffResult.Loaded( _diffResult.value = ViewDiffResult.Loaded(
diffEntryType, diffEntryType,
DiffResult.TextSplit(diffEntry, splitHunkList) 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( fun stageHunk(diffEntry: DiffEntry, hunk: Hunk) = tabState.runOperation(
refreshType = RefreshType.UNCOMMITED_CHANGES, refreshType = RefreshType.UNCOMMITED_CHANGES,
) { git -> ) { git ->

View File

@ -3,13 +3,14 @@ package app.viewmodels
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import app.exceptions.MissingDiffEntryException import app.exceptions.MissingDiffEntryException
import app.extensions.filePath import app.extensions.filePath
import app.git.DiffEntryType import app.git.*
import app.git.DiffManager import app.git.diff.DiffResult
import app.git.RefreshType
import app.git.TabState
import app.preferences.AppSettings import app.preferences.AppSettings
import app.usecase.GenerateSplitHunkFromDiffResultUseCase
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import org.eclipse.jgit.revwalk.RevCommit import org.eclipse.jgit.revwalk.RevCommit
import javax.inject.Inject import javax.inject.Inject
@ -17,6 +18,7 @@ class HistoryViewModel @Inject constructor(
private val tabState: TabState, private val tabState: TabState,
private val diffManager: DiffManager, private val diffManager: DiffManager,
private val settings: AppSettings, private val settings: AppSettings,
private val generateSplitHunkFromDiffResultUseCase: GenerateSplitHunkFromDiffResultUseCase,
) { ) {
private val _historyState = MutableStateFlow<HistoryState>(HistoryState.Loading("")) private val _historyState = MutableStateFlow<HistoryState>(HistoryState.Loading(""))
val historyState: StateFlow<HistoryState> = _historyState 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( fun fileHistory(filePath: String) = tabState.safeProcessing(
refreshType = RefreshType.NONE, refreshType = RefreshType.NONE,
) { git -> ) { git ->
@ -52,7 +88,6 @@ class HistoryViewModel @Inject constructor(
) { git -> ) { git ->
try { try {
val diffEntries = diffManager.commitDiffEntries(git, commit) val diffEntries = diffManager.commitDiffEntries(git, commit)
val diffEntry = diffEntries.firstOrNull { entry -> val diffEntry = diffEntries.firstOrNull { entry ->
entry.filePath == this.filePath entry.filePath == this.filePath
@ -62,11 +97,18 @@ class HistoryViewModel @Inject constructor(
_viewDiffResult.value = ViewDiffResult.DiffNotFound _viewDiffResult.value = ViewDiffResult.DiffNotFound
return@runOperation return@runOperation
} }
val diffEntryType = DiffEntryType.CommitDiff(diffEntry) 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) { } catch (ex: Exception) {
if (ex is MissingDiffEntryException) { if (ex is MissingDiffEntryException) {
tabState.refreshData(refreshType = RefreshType.UNCOMMITED_CHANGES) tabState.refreshData(refreshType = RefreshType.UNCOMMITED_CHANGES)