diff --git a/build.gradle.kts b/build.gradle.kts index 619c28c..ee3f3bf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -86,7 +86,7 @@ kotlin { } tasks.withType { - kotlinOptions.allWarningsAsErrors = false + kotlinOptions.allWarningsAsErrors = true kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" } diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/Icons.kt b/src/main/kotlin/com/jetpackduba/gitnuro/Icons.kt index 0616041..15900a7 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/Icons.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/Icons.kt @@ -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" diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/RawFileManager.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/RawFileManager.kt index 8ddd7ef..599c56d 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/RawFileManager.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/RawFileManager.kt @@ -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, diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/diff/CanGenerateTextDiffUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/diff/CanGenerateTextDiffUseCase.kt new file mode 100644 index 0000000..d03765a --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/diff/CanGenerateTextDiffUseCase.kt @@ -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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/diff/DiffContent.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/diff/DiffContent.kt new file mode 100644 index 0000000..1c83916 --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/diff/DiffContent.kt @@ -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, +) diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/diff/DiffResult.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/diff/DiffResult.kt new file mode 100644 index 0000000..dd03935 --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/diff/DiffResult.kt @@ -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 + ) : DiffResult(diffEntry) + + class TextSplit( + diffEntry: DiffEntry, + val hunks: List + ) : DiffResult(diffEntry) + + class NonText( + diffEntry: DiffEntry, + val oldBinaryContent: EntryContent, + val newBinaryContent: EntryContent, + ) : DiffResult(diffEntry) +} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/diff/FormatDiffUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/diff/FormatDiffUseCase.kt index fa94467..d007a71 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/diff/FormatDiffUseCase.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/diff/FormatDiffUseCase.kt @@ -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 } } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/diff/HunkDiffGenerator.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/diff/FormatHunksUseCase.kt similarity index 55% rename from src/main/kotlin/com/jetpackduba/gitnuro/git/diff/HunkDiffGenerator.kt rename to src/main/kotlin/com/jetpackduba/gitnuro/git/diff/FormatHunksUseCase.kt index 260071d..0451116 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/diff/HunkDiffGenerator.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/diff/FormatHunksUseCase.kt @@ -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 { - 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 { + 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, 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 - ) : DiffResult(diffEntry) - - class TextSplit( - diffEntry: DiffEntry, - val hunks: List - ) : DiffResult(diffEntry) - - class NonText( - diffEntry: DiffEntry, - val oldBinaryContent: EntryContent, - val newBinaryContent: EntryContent, - ) : DiffResult(diffEntry) -} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/diff/GetDiffContentUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/diff/GetDiffContentUseCase.kt new file mode 100644 index 0000000..3220b61 --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/diff/GetDiffContentUseCase.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/preferences/AppSettings.kt b/src/main/kotlin/com/jetpackduba/gitnuro/preferences/AppSettings.kt index 27811df..3f9d0b6 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/preferences/AppSettings.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/preferences/AppSettings.kt @@ -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() diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/diff/Diff.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/diff/Diff.kt index 8b95a71..c107851 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/diff/Diff.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/diff/Diff.kt @@ -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, +// ) } } diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/DiffViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/DiffViewModel.kt index 0236df6..c33e3b8 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/DiffViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/DiffViewModel.kt @@ -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 = _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, diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/HistoryViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/HistoryViewModel.kt index 81794ad..c0515fb 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/HistoryViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/HistoryViewModel.kt @@ -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) { diff --git a/src/main/resources/description.svg b/src/main/resources/description.svg new file mode 100644 index 0000000..8b444aa --- /dev/null +++ b/src/main/resources/description.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/horizontal_split.svg b/src/main/resources/horizontal_split.svg new file mode 100644 index 0000000..1fd7824 --- /dev/null +++ b/src/main/resources/horizontal_split.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/unified.svg b/src/main/resources/unified.svg new file mode 100644 index 0000000..293124b --- /dev/null +++ b/src/main/resources/unified.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/main/resources/vertical_split.svg b/src/main/resources/vertical_split.svg new file mode 100644 index 0000000..18bb8a0 --- /dev/null +++ b/src/main/resources/vertical_split.svg @@ -0,0 +1,11 @@ + + + + + + + + + + +