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> {
|
||||
kotlinOptions.allWarningsAsErrors = false
|
||||
kotlinOptions.allWarningsAsErrors = true
|
||||
kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@ object AppIcons {
|
||||
const val COPY = "copy.svg"
|
||||
const val CUT = "cut.svg"
|
||||
const val DELETE = "delete.svg"
|
||||
const val DESCRIPTION = "description.svg"
|
||||
const val DONE = "done.svg"
|
||||
const val DOWNLOAD = "download.svg"
|
||||
const val DROPDOWN = "dropdown.svg"
|
||||
@ -24,6 +25,7 @@ object AppIcons {
|
||||
const val EXPAND_MORE = "expand_more.svg"
|
||||
const val FETCH = "fetch.svg"
|
||||
const val GRADE = "grade.svg"
|
||||
const val HORIZONTAL_SPLIT = "horizontal_split.svg"
|
||||
const val HISTORY = "history.svg"
|
||||
const val INFO = "info.svg"
|
||||
const val KEY = "key.svg"
|
||||
@ -55,8 +57,10 @@ object AppIcons {
|
||||
const val TERMINAL = "terminal.svg"
|
||||
const val TOPIC = "topic.svg"
|
||||
const val UNDO = "undo.svg"
|
||||
const val UNIFIED = "unified.svg"
|
||||
const val UPDATE = "update.svg"
|
||||
const val UPLOAD = "upload.svg"
|
||||
const val VERTICAL_SPLIT = "vertical_split.svg"
|
||||
const val VISIBILITY = "visibility.svg"
|
||||
const val VISIBILITY_OFF = "visibility_off.svg"
|
||||
const val WARNING = "warning.svg"
|
||||
|
@ -34,12 +34,6 @@ val animatedImages = arrayOf(
|
||||
class RawFileManager @Inject constructor(
|
||||
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(
|
||||
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(
|
||||
ldr: ObjectLoader,
|
||||
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
|
||||
|
||||
import com.jetpackduba.gitnuro.git.DiffEntryType
|
||||
import com.jetpackduba.gitnuro.git.EntryContent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
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.treewalk.FileTreeIterator
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InvalidObjectException
|
||||
import javax.inject.Inject
|
||||
|
||||
class FormatDiffUseCase @Inject constructor(
|
||||
private val hunkDiffGenerator: HunkDiffGenerator,
|
||||
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 repository = git.repository
|
||||
val diffEntry: DiffEntry
|
||||
@ -54,11 +62,31 @@ class FormatDiffUseCase @Inject constructor(
|
||||
newTree = null
|
||||
}
|
||||
|
||||
return@withContext hunkDiffGenerator.format(
|
||||
repository,
|
||||
diffEntry,
|
||||
oldTree,
|
||||
newTree,
|
||||
)
|
||||
val diffContent = getDiffContentUseCase(repository, diffEntry, oldTree, newTree)
|
||||
val fileHeader = diffContent.fileHeader
|
||||
|
||||
val rawOld = diffContent.rawOld
|
||||
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
|
||||
|
||||
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.lib.Repository
|
||||
import org.eclipse.jgit.patch.FileHeader
|
||||
import org.eclipse.jgit.patch.FileHeader.PatchType
|
||||
import org.eclipse.jgit.treewalk.AbstractTreeIterator
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InvalidObjectException
|
||||
import javax.inject.Inject
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
private const val CONTEXT_LINES = 3
|
||||
private const val CONTEXT_LINES = 2
|
||||
|
||||
/**
|
||||
* Generator of [Hunk] lists from [DiffEntry]
|
||||
*/
|
||||
class HunkDiffGenerator @Inject constructor(
|
||||
private val rawFileManager: RawFileManager,
|
||||
) {
|
||||
fun format(
|
||||
repository: Repository,
|
||||
diffEntry: DiffEntry,
|
||||
oldTreeIterator: AbstractTreeIterator?,
|
||||
newTreeIterator: AbstractTreeIterator?,
|
||||
): 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)
|
||||
class FormatHunksUseCase @Inject constructor() {
|
||||
operator fun invoke(
|
||||
fileHeader: FileHeader,
|
||||
rawOld: RawText,
|
||||
rawNew: RawText,
|
||||
): List<Hunk> {
|
||||
return if (fileHeader.patchType == PatchType.UNIFIED)
|
||||
format(fileHeader.toEditList(), rawOld, rawNew)
|
||||
else
|
||||
emptyList()
|
||||
}
|
||||
@ -169,7 +84,9 @@ class HunkDiffGenerator @Inject constructor(
|
||||
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))
|
||||
@ -206,8 +123,10 @@ class HunkDiffGenerator @Inject constructor(
|
||||
|
||||
private fun findCombinedEnd(edits: List<Edit>, i: Int): Int {
|
||||
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++
|
||||
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_UI_SCALE = "ui_scale"
|
||||
private const val PREF_DIFF_TYPE = "diffType"
|
||||
private const val PREF_DIFF_FULL_FILE = "diffFullFile"
|
||||
private const val PREF_SWAP_UNCOMMITED_CHANGES = "inverseUncommitedChanges"
|
||||
|
||||
|
||||
@ -72,6 +73,9 @@ class AppSettings @Inject constructor() {
|
||||
private val _textDiffTypeFlow = MutableStateFlow(textDiffType)
|
||||
val textDiffTypeFlow = _textDiffTypeFlow.asStateFlow()
|
||||
|
||||
private val _textDiffFullFileFlow = MutableStateFlow(diffDisplayFullFile)
|
||||
val diffDisplayFullFileFlow = _textDiffFullFileFlow.asStateFlow()
|
||||
|
||||
var latestTabsOpened: String
|
||||
get() = preferences.get(PREF_LATEST_REPOSITORIES_TABS_OPENED, "")
|
||||
set(value) {
|
||||
@ -188,6 +192,16 @@ class AppSettings @Inject constructor() {
|
||||
_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) {
|
||||
val file = File(filePath)
|
||||
val content = file.readText()
|
||||
|
@ -55,6 +55,7 @@ import com.jetpackduba.gitnuro.keybindings.matchesBinding
|
||||
import com.jetpackduba.gitnuro.theme.*
|
||||
import com.jetpackduba.gitnuro.ui.components.ScrollableLazyColumn
|
||||
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.ContextMenuElement
|
||||
import com.jetpackduba.gitnuro.ui.context_menu.CustomTextContextMenu
|
||||
@ -80,6 +81,7 @@ fun Diff(
|
||||
) {
|
||||
val diffResultState = diffViewModel.diffResult.collectAsState()
|
||||
val diffType by diffViewModel.diffTypeFlow.collectAsState()
|
||||
val isDisplayFullFile by diffViewModel.isDisplayFullFile.collectAsState()
|
||||
val viewDiffResult = diffResultState.value ?: return
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
@ -116,9 +118,11 @@ fun Diff(
|
||||
diffEntry = diffEntry,
|
||||
onCloseDiffView = onCloseDiffView,
|
||||
diffType = diffType,
|
||||
isDisplayFullFile = isDisplayFullFile,
|
||||
onStageFile = { diffViewModel.stageFile(it) },
|
||||
onUnstageFile = { diffViewModel.unstageFile(it) },
|
||||
onChangeDiffType = { diffViewModel.changeTextDiffType(it) }
|
||||
onChangeDiffType = { diffViewModel.changeTextDiffType(it) },
|
||||
onDisplayFullFile = { diffViewModel.changeDisplayFullFile(it) },
|
||||
)
|
||||
|
||||
val scrollState by diffViewModel.lazyListState.collectAsState()
|
||||
@ -773,10 +777,12 @@ private fun DiffHeader(
|
||||
diffEntryType: DiffEntryType,
|
||||
diffEntry: DiffEntry,
|
||||
diffType: TextDiffType,
|
||||
isDisplayFullFile: Boolean,
|
||||
onCloseDiffView: () -> Unit,
|
||||
onStageFile: (StatusEntry) -> Unit,
|
||||
onUnstageFile: (StatusEntry) -> Unit,
|
||||
onChangeDiffType: (TextDiffType) -> Unit,
|
||||
onDisplayFullFile: (Boolean) -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@ -825,8 +831,15 @@ private fun DiffHeader(
|
||||
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (diffEntryType.statusType != StatusType.ADDED && diffEntryType.statusType != StatusType.REMOVED) {
|
||||
DiffTypeButtons(diffType = diffType, onChangeDiffType = onChangeDiffType)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (diffEntryType.statusType != StatusType.ADDED && diffEntryType.statusType != StatusType.REMOVED) {
|
||||
DiffTypeButtons(
|
||||
diffType = diffType,
|
||||
isDisplayFullFile = isDisplayFullFile,
|
||||
onChangeDiffType = onChangeDiffType,
|
||||
onDisplayFullFile = onDisplayFullFile,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (diffEntryType is DiffEntryType.UncommitedDiff) {
|
||||
@ -853,40 +866,114 @@ private fun DiffHeader(
|
||||
}
|
||||
|
||||
@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(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
) {
|
||||
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
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(end = 16.dp)
|
||||
) {
|
||||
StateIcon(
|
||||
icon = AppIcons.HORIZONTAL_SPLIT,
|
||||
tooltip = "Divide by hunks",
|
||||
isToggled = !isDisplayFullFile,
|
||||
onClick = { onDisplayFullFile(false) },
|
||||
)
|
||||
)
|
||||
|
||||
Text(
|
||||
"Split",
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
// modifier = Modifier.padding(horizontal = 4.dp),
|
||||
style = MaterialTheme.typography.caption,
|
||||
)
|
||||
StateIcon(
|
||||
icon = AppIcons.DESCRIPTION,
|
||||
tooltip = "View the complete file",
|
||||
isToggled = isDisplayFullFile,
|
||||
onClick = { onDisplayFullFile(true) },
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
StateIcon(
|
||||
icon = AppIcons.UNIFIED,
|
||||
tooltip = "Unified diff",
|
||||
isToggled = diffType == TextDiffType.UNIFIED,
|
||||
onClick = { onChangeDiffType(TextDiffType.UNIFIED) },
|
||||
)
|
||||
|
||||
StateIcon(
|
||||
icon = AppIcons.VERTICAL_SPLIT,
|
||||
tooltip = "Split diff",
|
||||
isToggled = diffType == TextDiffType.SPLIT,
|
||||
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,
|
||||
// )
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.launch
|
||||
import org.eclipse.jgit.diff.DiffEntry
|
||||
import javax.inject.Inject
|
||||
@ -41,20 +42,31 @@ class DiffViewModel @Inject constructor(
|
||||
val diffResult: StateFlow<ViewDiffResult?> = _diffResult
|
||||
|
||||
val diffTypeFlow = settings.textDiffTypeFlow
|
||||
private var diffEntryType: DiffEntryType? = null
|
||||
private var diffTypeFlowChangesCount = 0
|
||||
val isDisplayFullFile = settings.diffDisplayFullFileFlow
|
||||
|
||||
private var diffEntryType: DiffEntryType? = null
|
||||
private var diffJob: Job? = null
|
||||
|
||||
init {
|
||||
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
|
||||
if (diffTypeFlowChangesCount > 0 && diffEntryType != null) { // Ignore the first time the flow triggers, we only care about updates
|
||||
if (diffEntryType != null) {
|
||||
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,
|
||||
onDelayTriggered = { _diffResult.value = ViewDiffResult.Loading(diffEntryType.filePath) }
|
||||
) {
|
||||
val diffFormat = formatDiffUseCase(git, diffEntryType)
|
||||
val diffFormat = formatDiffUseCase(git, diffEntryType, isDisplayFullFile.value)
|
||||
val diffEntry = diffFormat.diffEntry
|
||||
if (
|
||||
diffTypeFlow.value == TextDiffType.SPLIT &&
|
||||
@ -178,6 +190,10 @@ class DiffViewModel @Inject constructor(
|
||||
settings.textDiffType = newDiffType
|
||||
}
|
||||
|
||||
fun changeDisplayFullFile(isDisplayFullFile: Boolean) {
|
||||
settings.diffDisplayFullFile = isDisplayFullFile
|
||||
}
|
||||
|
||||
fun stageHunkLine(entry: DiffEntry, hunk: Hunk, line: Line) = tabState.runOperation(
|
||||
refreshType = RefreshType.UNCOMMITED_CHANGES,
|
||||
showError = true,
|
||||
|
@ -108,7 +108,7 @@ class HistoryViewModel @Inject constructor(
|
||||
|
||||
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 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