Started implementation of full file diff (instead of hunks)

This commit is contained in:
Abdelilah El Aissaoui 2023-07-06 21:56:29 +02:00
parent 8903e473a0
commit 6ddcd0c69d
No known key found for this signature in database
GPG Key ID: 7587FC860F594869
17 changed files with 361 additions and 168 deletions

View File

@ -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"
} }

View File

@ -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"

View File

@ -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,

View File

@ -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
}
}

View File

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

View File

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

View File

@ -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
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -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,

View File

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

View 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

View 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

View 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

View 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