Started implementation of full file diff (instead of hunks)
This commit is contained in:
parent
8903e473a0
commit
6ddcd0c69d
@ -86,7 +86,7 @@ kotlin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<KotlinCompile> {
|
tasks.withType<KotlinCompile> {
|
||||||
kotlinOptions.allWarningsAsErrors = false
|
kotlinOptions.allWarningsAsErrors = true
|
||||||
kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
|
kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ object AppIcons {
|
|||||||
const val COPY = "copy.svg"
|
const val COPY = "copy.svg"
|
||||||
const val CUT = "cut.svg"
|
const val CUT = "cut.svg"
|
||||||
const val DELETE = "delete.svg"
|
const val DELETE = "delete.svg"
|
||||||
|
const val DESCRIPTION = "description.svg"
|
||||||
const val DONE = "done.svg"
|
const val DONE = "done.svg"
|
||||||
const val DOWNLOAD = "download.svg"
|
const val DOWNLOAD = "download.svg"
|
||||||
const val DROPDOWN = "dropdown.svg"
|
const val DROPDOWN = "dropdown.svg"
|
||||||
@ -24,6 +25,7 @@ object AppIcons {
|
|||||||
const val EXPAND_MORE = "expand_more.svg"
|
const val EXPAND_MORE = "expand_more.svg"
|
||||||
const val FETCH = "fetch.svg"
|
const val FETCH = "fetch.svg"
|
||||||
const val GRADE = "grade.svg"
|
const val GRADE = "grade.svg"
|
||||||
|
const val HORIZONTAL_SPLIT = "horizontal_split.svg"
|
||||||
const val HISTORY = "history.svg"
|
const val HISTORY = "history.svg"
|
||||||
const val INFO = "info.svg"
|
const val INFO = "info.svg"
|
||||||
const val KEY = "key.svg"
|
const val KEY = "key.svg"
|
||||||
@ -55,8 +57,10 @@ object AppIcons {
|
|||||||
const val TERMINAL = "terminal.svg"
|
const val TERMINAL = "terminal.svg"
|
||||||
const val TOPIC = "topic.svg"
|
const val TOPIC = "topic.svg"
|
||||||
const val UNDO = "undo.svg"
|
const val UNDO = "undo.svg"
|
||||||
|
const val UNIFIED = "unified.svg"
|
||||||
const val UPDATE = "update.svg"
|
const val UPDATE = "update.svg"
|
||||||
const val UPLOAD = "upload.svg"
|
const val UPLOAD = "upload.svg"
|
||||||
|
const val VERTICAL_SPLIT = "vertical_split.svg"
|
||||||
const val VISIBILITY = "visibility.svg"
|
const val VISIBILITY = "visibility.svg"
|
||||||
const val VISIBILITY_OFF = "visibility_off.svg"
|
const val VISIBILITY_OFF = "visibility_off.svg"
|
||||||
const val WARNING = "warning.svg"
|
const val WARNING = "warning.svg"
|
||||||
|
@ -34,12 +34,6 @@ val animatedImages = arrayOf(
|
|||||||
class RawFileManager @Inject constructor(
|
class RawFileManager @Inject constructor(
|
||||||
private val tempFilesManager: TempFilesManager,
|
private val tempFilesManager: TempFilesManager,
|
||||||
) {
|
) {
|
||||||
private fun source(iterator: AbstractTreeIterator, reader: ObjectReader): ContentSource {
|
|
||||||
return if (iterator is WorkingTreeIterator)
|
|
||||||
ContentSource.create(iterator)
|
|
||||||
else
|
|
||||||
ContentSource.create(reader)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getRawContent(
|
fun getRawContent(
|
||||||
repository: Repository,
|
repository: Repository,
|
||||||
@ -76,6 +70,13 @@ class RawFileManager @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun source(iterator: AbstractTreeIterator, reader: ObjectReader): ContentSource {
|
||||||
|
return if (iterator is WorkingTreeIterator)
|
||||||
|
ContentSource.create(iterator)
|
||||||
|
else
|
||||||
|
ContentSource.create(reader)
|
||||||
|
}
|
||||||
|
|
||||||
private fun generateImageBinary(
|
private fun generateImageBinary(
|
||||||
ldr: ObjectLoader,
|
ldr: ObjectLoader,
|
||||||
entry: DiffEntry,
|
entry: DiffEntry,
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
package com.jetpackduba.gitnuro.git.diff
|
||||||
|
|
||||||
|
import com.jetpackduba.gitnuro.git.EntryContent
|
||||||
|
import org.eclipse.jgit.diff.RawText
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class CanGenerateTextDiffUseCase @Inject constructor() {
|
||||||
|
operator fun invoke(
|
||||||
|
rawOld: EntryContent,
|
||||||
|
rawNew: EntryContent,
|
||||||
|
onText: (oldRawText: RawText, newRawText: RawText) -> Unit
|
||||||
|
): Boolean {
|
||||||
|
val rawOldText = when (rawOld) {
|
||||||
|
is EntryContent.Text -> rawOld.rawText
|
||||||
|
EntryContent.Missing -> RawText.EMPTY_TEXT
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
val newOldText = when (rawNew) {
|
||||||
|
is EntryContent.Text -> rawNew.rawText
|
||||||
|
EntryContent.Missing -> RawText.EMPTY_TEXT
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (rawOldText != null && newOldText != null) {
|
||||||
|
onText(rawOldText, newOldText)
|
||||||
|
true
|
||||||
|
} else
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package com.jetpackduba.gitnuro.git.diff
|
||||||
|
|
||||||
|
import com.jetpackduba.gitnuro.git.EntryContent
|
||||||
|
import org.eclipse.jgit.patch.FileHeader
|
||||||
|
|
||||||
|
data class DiffContent(
|
||||||
|
val fileHeader: FileHeader,
|
||||||
|
val rawOld: EntryContent,
|
||||||
|
val rawNew: EntryContent,
|
||||||
|
)
|
@ -0,0 +1,24 @@
|
|||||||
|
package com.jetpackduba.gitnuro.git.diff
|
||||||
|
|
||||||
|
import com.jetpackduba.gitnuro.git.EntryContent
|
||||||
|
import org.eclipse.jgit.diff.DiffEntry
|
||||||
|
|
||||||
|
sealed class DiffResult(
|
||||||
|
val diffEntry: DiffEntry,
|
||||||
|
) {
|
||||||
|
class Text(
|
||||||
|
diffEntry: DiffEntry,
|
||||||
|
val hunks: List<Hunk>
|
||||||
|
) : DiffResult(diffEntry)
|
||||||
|
|
||||||
|
class TextSplit(
|
||||||
|
diffEntry: DiffEntry,
|
||||||
|
val hunks: List<SplitHunk>
|
||||||
|
) : DiffResult(diffEntry)
|
||||||
|
|
||||||
|
class NonText(
|
||||||
|
diffEntry: DiffEntry,
|
||||||
|
val oldBinaryContent: EntryContent,
|
||||||
|
val newBinaryContent: EntryContent,
|
||||||
|
) : DiffResult(diffEntry)
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package com.jetpackduba.gitnuro.git.diff
|
package com.jetpackduba.gitnuro.git.diff
|
||||||
|
|
||||||
import com.jetpackduba.gitnuro.git.DiffEntryType
|
import com.jetpackduba.gitnuro.git.DiffEntryType
|
||||||
|
import com.jetpackduba.gitnuro.git.EntryContent
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
@ -9,13 +10,20 @@ import org.eclipse.jgit.diff.DiffFormatter
|
|||||||
import org.eclipse.jgit.dircache.DirCacheIterator
|
import org.eclipse.jgit.dircache.DirCacheIterator
|
||||||
import org.eclipse.jgit.treewalk.FileTreeIterator
|
import org.eclipse.jgit.treewalk.FileTreeIterator
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.InvalidObjectException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class FormatDiffUseCase @Inject constructor(
|
class FormatDiffUseCase @Inject constructor(
|
||||||
private val hunkDiffGenerator: HunkDiffGenerator,
|
|
||||||
private val getDiffEntryForUncommitedDiffUseCase: GetDiffEntryForUncommitedDiffUseCase,
|
private val getDiffEntryForUncommitedDiffUseCase: GetDiffEntryForUncommitedDiffUseCase,
|
||||||
|
private val canGenerateTextDiffUseCase: CanGenerateTextDiffUseCase,
|
||||||
|
private val getDiffContentUseCase: GetDiffContentUseCase,
|
||||||
|
private val formatHunksUseCase: FormatHunksUseCase,
|
||||||
) {
|
) {
|
||||||
suspend operator fun invoke(git: Git, diffEntryType: DiffEntryType): DiffResult = withContext(Dispatchers.IO) {
|
suspend operator fun invoke(
|
||||||
|
git: Git,
|
||||||
|
diffEntryType: DiffEntryType,
|
||||||
|
isDisplayFullFile: Boolean
|
||||||
|
): DiffResult = withContext(Dispatchers.IO) {
|
||||||
val byteArrayOutputStream = ByteArrayOutputStream()
|
val byteArrayOutputStream = ByteArrayOutputStream()
|
||||||
val repository = git.repository
|
val repository = git.repository
|
||||||
val diffEntry: DiffEntry
|
val diffEntry: DiffEntry
|
||||||
@ -54,11 +62,31 @@ class FormatDiffUseCase @Inject constructor(
|
|||||||
newTree = null
|
newTree = null
|
||||||
}
|
}
|
||||||
|
|
||||||
return@withContext hunkDiffGenerator.format(
|
val diffContent = getDiffContentUseCase(repository, diffEntry, oldTree, newTree)
|
||||||
repository,
|
val fileHeader = diffContent.fileHeader
|
||||||
diffEntry,
|
|
||||||
oldTree,
|
val rawOld = diffContent.rawOld
|
||||||
newTree,
|
val rawNew = diffContent.rawNew
|
||||||
)
|
|
||||||
|
if (rawOld == EntryContent.InvalidObjectBlob || rawNew == EntryContent.InvalidObjectBlob)
|
||||||
|
throw InvalidObjectException("Invalid object in diff format")
|
||||||
|
|
||||||
|
var diffResult: DiffResult = DiffResult.Text(diffEntry, emptyList())
|
||||||
|
|
||||||
|
// If we can, generate text diff (if one of the files has never been a binary file)
|
||||||
|
val hasGeneratedTextDiff = canGenerateTextDiffUseCase(rawOld, rawNew) { oldRawText, newRawText ->
|
||||||
|
if (isDisplayFullFile) {
|
||||||
|
TODO()
|
||||||
|
} else {
|
||||||
|
diffResult = DiffResult.Text(diffEntry, formatHunksUseCase(fileHeader, oldRawText, newRawText))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasGeneratedTextDiff) {
|
||||||
|
diffResult = DiffResult.NonText(diffEntry, rawOld, rawNew)
|
||||||
|
}
|
||||||
|
|
||||||
|
return@withContext diffResult
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,112 +1,27 @@
|
|||||||
package com.jetpackduba.gitnuro.git.diff
|
package com.jetpackduba.gitnuro.git.diff
|
||||||
|
|
||||||
import com.jetpackduba.gitnuro.extensions.lineAt
|
import com.jetpackduba.gitnuro.extensions.lineAt
|
||||||
import com.jetpackduba.gitnuro.git.EntryContent
|
|
||||||
import com.jetpackduba.gitnuro.git.RawFileManager
|
|
||||||
import org.eclipse.jgit.diff.*
|
import org.eclipse.jgit.diff.*
|
||||||
import org.eclipse.jgit.lib.Repository
|
|
||||||
import org.eclipse.jgit.patch.FileHeader
|
import org.eclipse.jgit.patch.FileHeader
|
||||||
import org.eclipse.jgit.patch.FileHeader.PatchType
|
import org.eclipse.jgit.patch.FileHeader.PatchType
|
||||||
import org.eclipse.jgit.treewalk.AbstractTreeIterator
|
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InvalidObjectException
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.contracts.ExperimentalContracts
|
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
private const val CONTEXT_LINES = 3
|
private const val CONTEXT_LINES = 2
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generator of [Hunk] lists from [DiffEntry]
|
* Generator of [Hunk] lists from [DiffEntry]
|
||||||
*/
|
*/
|
||||||
class HunkDiffGenerator @Inject constructor(
|
class FormatHunksUseCase @Inject constructor() {
|
||||||
private val rawFileManager: RawFileManager,
|
operator fun invoke(
|
||||||
) {
|
fileHeader: FileHeader,
|
||||||
fun format(
|
rawOld: RawText,
|
||||||
repository: Repository,
|
rawNew: RawText,
|
||||||
diffEntry: DiffEntry,
|
): List<Hunk> {
|
||||||
oldTreeIterator: AbstractTreeIterator?,
|
return if (fileHeader.patchType == PatchType.UNIFIED)
|
||||||
newTreeIterator: AbstractTreeIterator?,
|
format(fileHeader.toEditList(), rawOld, rawNew)
|
||||||
): DiffResult {
|
|
||||||
val outputStream = ByteArrayOutputStream() // Dummy output stream used for the diff formatter
|
|
||||||
return outputStream.use {
|
|
||||||
val diffFormatter = DiffFormatter(outputStream).apply {
|
|
||||||
setRepository(repository)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldTreeIterator != null && newTreeIterator != null) {
|
|
||||||
diffFormatter.scan(oldTreeIterator, newTreeIterator)
|
|
||||||
}
|
|
||||||
|
|
||||||
val fileHeader = diffFormatter.toFileHeader(diffEntry)
|
|
||||||
|
|
||||||
val rawOld = rawFileManager.getRawContent(
|
|
||||||
repository,
|
|
||||||
DiffEntry.Side.OLD,
|
|
||||||
diffEntry,
|
|
||||||
oldTreeIterator,
|
|
||||||
newTreeIterator
|
|
||||||
)
|
|
||||||
val rawNew = rawFileManager.getRawContent(
|
|
||||||
repository,
|
|
||||||
DiffEntry.Side.NEW,
|
|
||||||
diffEntry,
|
|
||||||
oldTreeIterator,
|
|
||||||
newTreeIterator
|
|
||||||
)
|
|
||||||
|
|
||||||
if (rawOld == EntryContent.InvalidObjectBlob || rawNew == EntryContent.InvalidObjectBlob)
|
|
||||||
throw InvalidObjectException("Invalid object in diff format")
|
|
||||||
|
|
||||||
var diffResult: DiffResult = DiffResult.Text(diffEntry, emptyList())
|
|
||||||
|
|
||||||
// If we can, generate text diff (if one of the files has never been a binary file)
|
|
||||||
val hasGeneratedTextDiff = canGenerateTextDiff(rawOld, rawNew) { oldRawText, newRawText ->
|
|
||||||
diffResult = DiffResult.Text(diffEntry, format(fileHeader, oldRawText, newRawText))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasGeneratedTextDiff) {
|
|
||||||
diffResult = DiffResult.NonText(diffEntry, rawOld, rawNew)
|
|
||||||
}
|
|
||||||
|
|
||||||
return@use diffResult
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalContracts::class)
|
|
||||||
private fun canGenerateTextDiff(
|
|
||||||
rawOld: EntryContent,
|
|
||||||
rawNew: EntryContent,
|
|
||||||
onText: (oldRawText: RawText, newRawText: RawText) -> Unit
|
|
||||||
): Boolean {
|
|
||||||
|
|
||||||
val rawOldText = when (rawOld) {
|
|
||||||
is EntryContent.Text -> rawOld.rawText
|
|
||||||
EntryContent.Missing -> RawText.EMPTY_TEXT
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
val newOldText = when (rawNew) {
|
|
||||||
is EntryContent.Text -> rawNew.rawText
|
|
||||||
EntryContent.Missing -> RawText.EMPTY_TEXT
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
return if (rawOldText != null && newOldText != null) {
|
|
||||||
onText(rawOldText, newOldText)
|
|
||||||
true
|
|
||||||
} else
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a [FileHeader] and the both [RawText], generate a [List] of [Hunk]
|
|
||||||
*/
|
|
||||||
private fun format(head: FileHeader, oldRawText: RawText, newRawText: RawText): List<Hunk> {
|
|
||||||
return if (head.patchType == PatchType.UNIFIED)
|
|
||||||
format(head.toEditList(), oldRawText, newRawText)
|
|
||||||
else
|
else
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
@ -169,7 +84,9 @@ class HunkDiffGenerator @Inject constructor(
|
|||||||
newCurrentLine++
|
newCurrentLine++
|
||||||
}
|
}
|
||||||
|
|
||||||
if (end(curEdit, oldCurrentLine, newCurrentLine) && ++curIdx < edits.size) curEdit = edits[curIdx]
|
if (end(curEdit, oldCurrentLine, newCurrentLine) && ++curIdx < edits.size) {
|
||||||
|
curEdit = edits[curIdx]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hunksList.add(Hunk(headerText, lines))
|
hunksList.add(Hunk(headerText, lines))
|
||||||
@ -206,8 +123,10 @@ class HunkDiffGenerator @Inject constructor(
|
|||||||
|
|
||||||
private fun findCombinedEnd(edits: List<Edit>, i: Int): Int {
|
private fun findCombinedEnd(edits: List<Edit>, i: Int): Int {
|
||||||
var end = i + 1
|
var end = i + 1
|
||||||
while (end < edits.size
|
|
||||||
&& (combineA(edits, end) || combineB(edits, end))
|
while (
|
||||||
|
end < edits.size &&
|
||||||
|
(combineA(edits, end) || combineB(edits, end))
|
||||||
) end++
|
) end++
|
||||||
return end - 1
|
return end - 1
|
||||||
}
|
}
|
||||||
@ -225,22 +144,3 @@ class HunkDiffGenerator @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class DiffResult(
|
|
||||||
val diffEntry: DiffEntry,
|
|
||||||
) {
|
|
||||||
class Text(
|
|
||||||
diffEntry: DiffEntry,
|
|
||||||
val hunks: List<Hunk>
|
|
||||||
) : DiffResult(diffEntry)
|
|
||||||
|
|
||||||
class TextSplit(
|
|
||||||
diffEntry: DiffEntry,
|
|
||||||
val hunks: List<SplitHunk>
|
|
||||||
) : DiffResult(diffEntry)
|
|
||||||
|
|
||||||
class NonText(
|
|
||||||
diffEntry: DiffEntry,
|
|
||||||
val oldBinaryContent: EntryContent,
|
|
||||||
val newBinaryContent: EntryContent,
|
|
||||||
) : DiffResult(diffEntry)
|
|
||||||
}
|
|
@ -0,0 +1,55 @@
|
|||||||
|
package com.jetpackduba.gitnuro.git.diff
|
||||||
|
|
||||||
|
import com.jetpackduba.gitnuro.git.EntryContent
|
||||||
|
import com.jetpackduba.gitnuro.git.RawFileManager
|
||||||
|
import org.eclipse.jgit.diff.DiffEntry
|
||||||
|
import org.eclipse.jgit.diff.DiffFormatter
|
||||||
|
import org.eclipse.jgit.lib.Repository
|
||||||
|
import org.eclipse.jgit.treewalk.AbstractTreeIterator
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.InvalidObjectException
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class GetDiffContentUseCase @Inject constructor(
|
||||||
|
private val rawFileManager: RawFileManager,
|
||||||
|
) {
|
||||||
|
operator fun invoke(
|
||||||
|
repository: Repository,
|
||||||
|
diffEntry: DiffEntry,
|
||||||
|
oldTreeIterator: AbstractTreeIterator?,
|
||||||
|
newTreeIterator: AbstractTreeIterator?,
|
||||||
|
): DiffContent {
|
||||||
|
val outputStream = ByteArrayOutputStream() // Dummy output stream used for the diff formatter
|
||||||
|
outputStream.use {
|
||||||
|
val diffFormatter = DiffFormatter(outputStream).apply {
|
||||||
|
setRepository(repository)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldTreeIterator != null && newTreeIterator != null) {
|
||||||
|
diffFormatter.scan(oldTreeIterator, newTreeIterator)
|
||||||
|
}
|
||||||
|
|
||||||
|
val fileHeader = diffFormatter.toFileHeader(diffEntry)
|
||||||
|
|
||||||
|
val rawOld = rawFileManager.getRawContent(
|
||||||
|
repository,
|
||||||
|
DiffEntry.Side.OLD,
|
||||||
|
diffEntry,
|
||||||
|
oldTreeIterator,
|
||||||
|
newTreeIterator
|
||||||
|
)
|
||||||
|
val rawNew = rawFileManager.getRawContent(
|
||||||
|
repository,
|
||||||
|
DiffEntry.Side.NEW,
|
||||||
|
diffEntry,
|
||||||
|
oldTreeIterator,
|
||||||
|
newTreeIterator
|
||||||
|
)
|
||||||
|
|
||||||
|
if (rawOld == EntryContent.InvalidObjectBlob || rawNew == EntryContent.InvalidObjectBlob)
|
||||||
|
throw InvalidObjectException("Invalid object in diff format")
|
||||||
|
|
||||||
|
return DiffContent(fileHeader, rawOld, rawNew)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -30,6 +30,7 @@ private const val PREF_WINDOW_PLACEMENT = "windowsPlacement"
|
|||||||
private const val PREF_CUSTOM_THEME = "customTheme"
|
private const val PREF_CUSTOM_THEME = "customTheme"
|
||||||
private const val PREF_UI_SCALE = "ui_scale"
|
private const val PREF_UI_SCALE = "ui_scale"
|
||||||
private const val PREF_DIFF_TYPE = "diffType"
|
private const val PREF_DIFF_TYPE = "diffType"
|
||||||
|
private const val PREF_DIFF_FULL_FILE = "diffFullFile"
|
||||||
private const val PREF_SWAP_UNCOMMITED_CHANGES = "inverseUncommitedChanges"
|
private const val PREF_SWAP_UNCOMMITED_CHANGES = "inverseUncommitedChanges"
|
||||||
|
|
||||||
|
|
||||||
@ -72,6 +73,9 @@ class AppSettings @Inject constructor() {
|
|||||||
private val _textDiffTypeFlow = MutableStateFlow(textDiffType)
|
private val _textDiffTypeFlow = MutableStateFlow(textDiffType)
|
||||||
val textDiffTypeFlow = _textDiffTypeFlow.asStateFlow()
|
val textDiffTypeFlow = _textDiffTypeFlow.asStateFlow()
|
||||||
|
|
||||||
|
private val _textDiffFullFileFlow = MutableStateFlow(diffDisplayFullFile)
|
||||||
|
val diffDisplayFullFileFlow = _textDiffFullFileFlow.asStateFlow()
|
||||||
|
|
||||||
var latestTabsOpened: String
|
var latestTabsOpened: String
|
||||||
get() = preferences.get(PREF_LATEST_REPOSITORIES_TABS_OPENED, "")
|
get() = preferences.get(PREF_LATEST_REPOSITORIES_TABS_OPENED, "")
|
||||||
set(value) {
|
set(value) {
|
||||||
@ -188,6 +192,16 @@ class AppSettings @Inject constructor() {
|
|||||||
_textDiffTypeFlow.value = textDiffType
|
_textDiffTypeFlow.value = textDiffType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var diffDisplayFullFile: Boolean
|
||||||
|
get() {
|
||||||
|
return preferences.getBoolean(PREF_DIFF_FULL_FILE, false)
|
||||||
|
}
|
||||||
|
set(newValue) {
|
||||||
|
preferences.putBoolean(PREF_DIFF_TYPE, newValue)
|
||||||
|
|
||||||
|
_textDiffFullFileFlow.value = newValue
|
||||||
|
}
|
||||||
|
|
||||||
fun saveCustomTheme(filePath: String) {
|
fun saveCustomTheme(filePath: String) {
|
||||||
val file = File(filePath)
|
val file = File(filePath)
|
||||||
val content = file.readText()
|
val content = file.readText()
|
||||||
|
@ -55,6 +55,7 @@ import com.jetpackduba.gitnuro.keybindings.matchesBinding
|
|||||||
import com.jetpackduba.gitnuro.theme.*
|
import com.jetpackduba.gitnuro.theme.*
|
||||||
import com.jetpackduba.gitnuro.ui.components.ScrollableLazyColumn
|
import com.jetpackduba.gitnuro.ui.components.ScrollableLazyColumn
|
||||||
import com.jetpackduba.gitnuro.ui.components.SecondaryButton
|
import com.jetpackduba.gitnuro.ui.components.SecondaryButton
|
||||||
|
import com.jetpackduba.gitnuro.ui.components.Tooltip
|
||||||
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu
|
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu
|
||||||
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenuElement
|
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenuElement
|
||||||
import com.jetpackduba.gitnuro.ui.context_menu.CustomTextContextMenu
|
import com.jetpackduba.gitnuro.ui.context_menu.CustomTextContextMenu
|
||||||
@ -80,6 +81,7 @@ fun Diff(
|
|||||||
) {
|
) {
|
||||||
val diffResultState = diffViewModel.diffResult.collectAsState()
|
val diffResultState = diffViewModel.diffResult.collectAsState()
|
||||||
val diffType by diffViewModel.diffTypeFlow.collectAsState()
|
val diffType by diffViewModel.diffTypeFlow.collectAsState()
|
||||||
|
val isDisplayFullFile by diffViewModel.isDisplayFullFile.collectAsState()
|
||||||
val viewDiffResult = diffResultState.value ?: return
|
val viewDiffResult = diffResultState.value ?: return
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
@ -116,9 +118,11 @@ fun Diff(
|
|||||||
diffEntry = diffEntry,
|
diffEntry = diffEntry,
|
||||||
onCloseDiffView = onCloseDiffView,
|
onCloseDiffView = onCloseDiffView,
|
||||||
diffType = diffType,
|
diffType = diffType,
|
||||||
|
isDisplayFullFile = isDisplayFullFile,
|
||||||
onStageFile = { diffViewModel.stageFile(it) },
|
onStageFile = { diffViewModel.stageFile(it) },
|
||||||
onUnstageFile = { diffViewModel.unstageFile(it) },
|
onUnstageFile = { diffViewModel.unstageFile(it) },
|
||||||
onChangeDiffType = { diffViewModel.changeTextDiffType(it) }
|
onChangeDiffType = { diffViewModel.changeTextDiffType(it) },
|
||||||
|
onDisplayFullFile = { diffViewModel.changeDisplayFullFile(it) },
|
||||||
)
|
)
|
||||||
|
|
||||||
val scrollState by diffViewModel.lazyListState.collectAsState()
|
val scrollState by diffViewModel.lazyListState.collectAsState()
|
||||||
@ -773,10 +777,12 @@ private fun DiffHeader(
|
|||||||
diffEntryType: DiffEntryType,
|
diffEntryType: DiffEntryType,
|
||||||
diffEntry: DiffEntry,
|
diffEntry: DiffEntry,
|
||||||
diffType: TextDiffType,
|
diffType: TextDiffType,
|
||||||
|
isDisplayFullFile: Boolean,
|
||||||
onCloseDiffView: () -> Unit,
|
onCloseDiffView: () -> Unit,
|
||||||
onStageFile: (StatusEntry) -> Unit,
|
onStageFile: (StatusEntry) -> Unit,
|
||||||
onUnstageFile: (StatusEntry) -> Unit,
|
onUnstageFile: (StatusEntry) -> Unit,
|
||||||
onChangeDiffType: (TextDiffType) -> Unit,
|
onChangeDiffType: (TextDiffType) -> Unit,
|
||||||
|
onDisplayFullFile: (Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -824,9 +830,16 @@ private fun DiffHeader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
if (diffEntryType.statusType != StatusType.ADDED && diffEntryType.statusType != StatusType.REMOVED) {
|
if (diffEntryType.statusType != StatusType.ADDED && diffEntryType.statusType != StatusType.REMOVED) {
|
||||||
DiffTypeButtons(diffType = diffType, onChangeDiffType = onChangeDiffType)
|
DiffTypeButtons(
|
||||||
|
diffType = diffType,
|
||||||
|
isDisplayFullFile = isDisplayFullFile,
|
||||||
|
onChangeDiffType = onChangeDiffType,
|
||||||
|
onDisplayFullFile = onDisplayFullFile,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (diffEntryType is DiffEntryType.UncommitedDiff) {
|
if (diffEntryType is DiffEntryType.UncommitedDiff) {
|
||||||
@ -853,41 +866,115 @@ private fun DiffHeader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DiffTypeButtons(diffType: TextDiffType, onChangeDiffType: (TextDiffType) -> Unit) {
|
fun StateIcon(
|
||||||
|
icon: String,
|
||||||
|
tooltip: String,
|
||||||
|
isToggled: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Tooltip(tooltip) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(4.dp))
|
||||||
|
.run {
|
||||||
|
if (isToggled)
|
||||||
|
this.background(MaterialTheme.colors.onSurface.copy(alpha = 0.2f))
|
||||||
|
else
|
||||||
|
this
|
||||||
|
}
|
||||||
|
.handMouseClickable { if(!isToggled) onClick() }
|
||||||
|
.padding(4.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painterResource(icon),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colors.onSurface,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DiffTypeButtons(
|
||||||
|
diffType: TextDiffType,
|
||||||
|
isDisplayFullFile: Boolean,
|
||||||
|
onChangeDiffType: (TextDiffType) -> Unit,
|
||||||
|
onDisplayFullFile: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.padding(horizontal = 16.dp)
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
|
||||||
"Unified",
|
Row(
|
||||||
color = MaterialTheme.colors.onBackground,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
style = MaterialTheme.typography.caption,
|
modifier = Modifier.padding(end = 16.dp)
|
||||||
|
) {
|
||||||
|
StateIcon(
|
||||||
|
icon = AppIcons.HORIZONTAL_SPLIT,
|
||||||
|
tooltip = "Divide by hunks",
|
||||||
|
isToggled = !isDisplayFullFile,
|
||||||
|
onClick = { onDisplayFullFile(false) },
|
||||||
)
|
)
|
||||||
|
|
||||||
Switch(
|
StateIcon(
|
||||||
checked = diffType == TextDiffType.SPLIT,
|
icon = AppIcons.DESCRIPTION,
|
||||||
onCheckedChange = { checked ->
|
tooltip = "View the complete file",
|
||||||
val newType = if (checked)
|
isToggled = isDisplayFullFile,
|
||||||
TextDiffType.SPLIT
|
onClick = { onDisplayFullFile(true) },
|
||||||
else
|
|
||||||
TextDiffType.UNIFIED
|
|
||||||
|
|
||||||
onChangeDiffType(newType)
|
|
||||||
},
|
|
||||||
colors = SwitchDefaults.colors(
|
|
||||||
uncheckedThumbColor = MaterialTheme.colors.secondaryVariant,
|
|
||||||
uncheckedTrackColor = MaterialTheme.colors.secondaryVariant,
|
|
||||||
uncheckedTrackAlpha = 0.54f
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
StateIcon(
|
||||||
|
icon = AppIcons.UNIFIED,
|
||||||
|
tooltip = "Unified diff",
|
||||||
|
isToggled = diffType == TextDiffType.UNIFIED,
|
||||||
|
onClick = { onChangeDiffType(TextDiffType.UNIFIED) },
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
StateIcon(
|
||||||
"Split",
|
icon = AppIcons.VERTICAL_SPLIT,
|
||||||
color = MaterialTheme.colors.onBackground,
|
tooltip = "Split diff",
|
||||||
// modifier = Modifier.padding(horizontal = 4.dp),
|
isToggled = diffType == TextDiffType.SPLIT,
|
||||||
style = MaterialTheme.typography.caption,
|
onClick = { onChangeDiffType(TextDiffType.SPLIT) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
//
|
||||||
|
// Text(
|
||||||
|
// "Unified",
|
||||||
|
// color = MaterialTheme.colors.onBackground,
|
||||||
|
// style = MaterialTheme.typography.caption,
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// Switch(
|
||||||
|
// checked = diffType == TextDiffType.SPLIT,
|
||||||
|
// onCheckedChange = { checked ->
|
||||||
|
// val newType = if (checked)
|
||||||
|
// TextDiffType.SPLIT
|
||||||
|
// else
|
||||||
|
// TextDiffType.UNIFIED
|
||||||
|
//
|
||||||
|
// onChangeDiffType(newType)
|
||||||
|
// },
|
||||||
|
// colors = SwitchDefaults.colors(
|
||||||
|
// uncheckedThumbColor = MaterialTheme.colors.secondaryVariant,
|
||||||
|
// uncheckedTrackColor = MaterialTheme.colors.secondaryVariant,
|
||||||
|
// uncheckedTrackAlpha = 0.54f
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// Text(
|
||||||
|
// "Split",
|
||||||
|
// color = MaterialTheme.colors.onBackground,
|
||||||
|
//// modifier = Modifier.padding(horizontal = 4.dp),
|
||||||
|
// style = MaterialTheme.typography.caption,
|
||||||
|
// )
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -15,6 +15,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
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.drop
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.eclipse.jgit.diff.DiffEntry
|
import org.eclipse.jgit.diff.DiffEntry
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -41,20 +42,31 @@ class DiffViewModel @Inject constructor(
|
|||||||
val diffResult: StateFlow<ViewDiffResult?> = _diffResult
|
val diffResult: StateFlow<ViewDiffResult?> = _diffResult
|
||||||
|
|
||||||
val diffTypeFlow = settings.textDiffTypeFlow
|
val diffTypeFlow = settings.textDiffTypeFlow
|
||||||
private var diffEntryType: DiffEntryType? = null
|
val isDisplayFullFile = settings.diffDisplayFullFileFlow
|
||||||
private var diffTypeFlowChangesCount = 0
|
|
||||||
|
|
||||||
|
private var diffEntryType: DiffEntryType? = null
|
||||||
private var diffJob: Job? = null
|
private var diffJob: Job? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
tabScope.launch {
|
tabScope.launch {
|
||||||
diffTypeFlow.collect {
|
diffTypeFlow
|
||||||
|
.drop(1) // Ignore the first time the flow triggers, we only care about updates
|
||||||
|
.collect {
|
||||||
val diffEntryType = this@DiffViewModel.diffEntryType
|
val diffEntryType = this@DiffViewModel.diffEntryType
|
||||||
if (diffTypeFlowChangesCount > 0 && diffEntryType != null) { // Ignore the first time the flow triggers, we only care about updates
|
if (diffEntryType != null) {
|
||||||
updateDiff(diffEntryType)
|
updateDiff(diffEntryType)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
diffTypeFlowChangesCount++
|
tabScope.launch {
|
||||||
|
isDisplayFullFile
|
||||||
|
.drop(1) // Ignore the first time the flow triggers, we only care about updates
|
||||||
|
.collect {
|
||||||
|
val diffEntryType = this@DiffViewModel.diffEntryType
|
||||||
|
if (diffEntryType != null) {
|
||||||
|
updateDiff(diffEntryType)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,7 +122,7 @@ class DiffViewModel @Inject constructor(
|
|||||||
delayMs = if (isFirstLoad) 0 else DIFF_MIN_TIME_IN_MS_TO_SHOW_LOAD,
|
delayMs = if (isFirstLoad) 0 else DIFF_MIN_TIME_IN_MS_TO_SHOW_LOAD,
|
||||||
onDelayTriggered = { _diffResult.value = ViewDiffResult.Loading(diffEntryType.filePath) }
|
onDelayTriggered = { _diffResult.value = ViewDiffResult.Loading(diffEntryType.filePath) }
|
||||||
) {
|
) {
|
||||||
val diffFormat = formatDiffUseCase(git, diffEntryType)
|
val diffFormat = formatDiffUseCase(git, diffEntryType, isDisplayFullFile.value)
|
||||||
val diffEntry = diffFormat.diffEntry
|
val diffEntry = diffFormat.diffEntry
|
||||||
if (
|
if (
|
||||||
diffTypeFlow.value == TextDiffType.SPLIT &&
|
diffTypeFlow.value == TextDiffType.SPLIT &&
|
||||||
@ -178,6 +190,10 @@ class DiffViewModel @Inject constructor(
|
|||||||
settings.textDiffType = newDiffType
|
settings.textDiffType = newDiffType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun changeDisplayFullFile(isDisplayFullFile: Boolean) {
|
||||||
|
settings.diffDisplayFullFile = isDisplayFullFile
|
||||||
|
}
|
||||||
|
|
||||||
fun stageHunkLine(entry: DiffEntry, hunk: Hunk, line: Line) = tabState.runOperation(
|
fun stageHunkLine(entry: DiffEntry, hunk: Hunk, line: Line) = tabState.runOperation(
|
||||||
refreshType = RefreshType.UNCOMMITED_CHANGES,
|
refreshType = RefreshType.UNCOMMITED_CHANGES,
|
||||||
showError = true,
|
showError = true,
|
||||||
|
@ -108,7 +108,7 @@ class HistoryViewModel @Inject constructor(
|
|||||||
|
|
||||||
val diffEntryType = DiffEntryType.CommitDiff(diffEntry)
|
val diffEntryType = DiffEntryType.CommitDiff(diffEntry)
|
||||||
|
|
||||||
val diffResult = formatDiffUseCase(git, diffEntryType)
|
val diffResult = formatDiffUseCase(git, diffEntryType, false) // TODO This hardcoded false should be changed when the UI is implemented
|
||||||
val textDiffType = settings.textDiffType
|
val textDiffType = settings.textDiffType
|
||||||
|
|
||||||
val formattedDiffResult = if (textDiffType == TextDiffType.SPLIT && diffResult is DiffResult.Text) {
|
val formattedDiffResult = if (textDiffType == TextDiffType.SPLIT && diffResult is DiffResult.Text) {
|
||||||
|
1
src/main/resources/description.svg
Normal file
1
src/main/resources/description.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M8 16h8v2H8zm0-4h8v2H8zm6-10H6c-1.1 0-2 .9-2 2v16c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/></svg>
|
After Width: | Height: | Size: 271 B |
1
src/main/resources/horizontal_split.svg
Normal file
1
src/main/resources/horizontal_split.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 15v2H5v-2h14m2-10H3v2h18V5zm0 4H3v2h18V9zm0 4H3v6h18v-6z"/></svg>
|
After Width: | Height: | Size: 219 B |
10
src/main/resources/unified.svg
Normal file
10
src/main/resources/unified.svg
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_402_2142)">
|
||||||
|
<path d="M4 13H20V15H4V13ZM4 17H20V19H4V17ZM4 9H20V11H4V9ZM4 5H20V7H4V5Z" fill="black"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_402_2142">
|
||||||
|
<rect width="24" height="24" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 336 B |
11
src/main/resources/vertical_split.svg
Normal file
11
src/main/resources/vertical_split.svg
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_402_2113)">
|
||||||
|
<path d="M3 13H11V15H3V13ZM3 17H11V19H3V17ZM3 9H11V11H3V9ZM3 5H11V7H3V5Z" fill="black"/>
|
||||||
|
<path d="M13 13H21V15H13V13ZM13 17H21V19H13V17ZM13 9H21V11H13V9ZM13 5H21V7H13V5Z" fill="black"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_402_2113">
|
||||||
|
<rect width="24" height="24" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 433 B |
Loading…
Reference in New Issue
Block a user