Simplified split hunk generation & added to file history diff
This commit is contained in:
parent
e550a6289c
commit
7506c79b63
13
src/main/kotlin/app/extensions/ArrayExtensions.kt
Normal file
13
src/main/kotlin/app/extensions/ArrayExtensions.kt
Normal 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
|
||||
}
|
@ -13,3 +13,4 @@ fun <T> flatListOf(vararg lists: List<T>): List<T> {
|
||||
|
||||
return flatList
|
||||
}
|
||||
|
||||
|
@ -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!
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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 ->
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user