diff --git a/src/main/kotlin/app/App.kt b/src/main/kotlin/app/App.kt index d6c0d7a..7455815 100644 --- a/src/main/kotlin/app/App.kt +++ b/src/main/kotlin/app/App.kt @@ -26,8 +26,10 @@ import androidx.compose.ui.window.rememberWindowState import app.di.DaggerAppComponent import app.extensions.preferenceValue import app.extensions.toWindowPlacement -import app.preferences.AppPreferences +import app.logging.printLog +import app.preferences.AppSettings import app.theme.AppTheme +import app.theme.Theme import app.theme.primaryTextColor import app.theme.secondaryTextColor import app.ui.AppTab @@ -40,6 +42,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import javax.inject.Inject +private const val TAG = "App" class App { private val appComponent = DaggerAppComponent.create() @@ -48,7 +51,7 @@ class App { lateinit var appStateManager: AppStateManager @Inject - lateinit var appPreferences: AppPreferences + lateinit var appSettings: AppSettings @Inject lateinit var settingsViewModel: SettingsViewModel @@ -60,17 +63,26 @@ class App { private val tabsFlow = MutableStateFlow>(emptyList()) fun start() { - val windowPlacement = appPreferences.windowPlacement.toWindowPlacement + val windowPlacement = appSettings.windowPlacement.toWindowPlacement appStateManager.loadRepositoriesTabs() - appPreferences.loadCustomTheme() + + try { + if (appSettings.theme == Theme.CUSTOM) { + appSettings.loadCustomTheme() + } + } catch (ex: Exception) { + printLog(TAG, "Failed to load custom theme") + ex.printStackTrace() + } + loadTabs() application { var isOpen by remember { mutableStateOf(true) } - val theme by appPreferences.themeState.collectAsState() - val customTheme by appPreferences.customThemeFlow.collectAsState() - val scale by appPreferences.scaleUiFlow.collectAsState() + val theme by appSettings.themeState.collectAsState() + val customTheme by appSettings.customThemeFlow.collectAsState() + val scale by appSettings.scaleUiFlow.collectAsState() val windowState = rememberWindowState( placement = windowPlacement, @@ -78,7 +90,7 @@ class App { ) // Save window state for next time the Window is started - appPreferences.windowPlacement = windowState.placement.preferenceValue + appSettings.windowPlacement = windowState.placement.preferenceValue if (isOpen) { Window( @@ -191,6 +203,7 @@ class App { tabsFlow.value = tabsFlow.value.toMutableList().apply { add(tabInformation) } } + @OptIn(ExperimentalComposeUiApi::class) @Composable fun Tabs( selectedTabKey: MutableState, diff --git a/src/main/kotlin/app/AppConstants.kt b/src/main/kotlin/app/AppConstants.kt index 06eb5d5..597841f 100644 --- a/src/main/kotlin/app/AppConstants.kt +++ b/src/main/kotlin/app/AppConstants.kt @@ -31,5 +31,8 @@ object AppConstants { private val apache__2_0 = License("Apache 2.0", "https://www.apache.org/licenses/LICENSE-2.0") private val edl = License("EDL", "https://www.eclipse.org/org/documents/edl-v10.php") -data class License(val name: String, val url: String) +data class License( + val name: String, + val url: String +) data class Project(val name: String, val url: String, val license: License) diff --git a/src/main/kotlin/app/AppStateManager.kt b/src/main/kotlin/app/AppStateManager.kt index ec1d539..c94c841 100644 --- a/src/main/kotlin/app/AppStateManager.kt +++ b/src/main/kotlin/app/AppStateManager.kt @@ -1,6 +1,6 @@ package app -import app.preferences.AppPreferences +import app.preferences.AppSettings import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex import kotlinx.serialization.decodeFromString @@ -11,7 +11,7 @@ import javax.inject.Singleton @Singleton class AppStateManager @Inject constructor( - private val appPreferences: AppPreferences, + private val appSettings: AppSettings, ) { private val mutex = Mutex() @@ -59,15 +59,15 @@ class AppStateManager @Inject constructor( private suspend fun updateSavedRepositoryTabs() = withContext(Dispatchers.IO) { val tabsList = _openRepositoriesPaths.map { it.value } - appPreferences.latestTabsOpened = Json.encodeToString(tabsList) + appSettings.latestTabsOpened = Json.encodeToString(tabsList) } private suspend fun updateLatestRepositoryTabs() = withContext(Dispatchers.IO) { - appPreferences.latestOpenedRepositoriesPath = Json.encodeToString(_latestOpenedRepositoriesPaths) + appSettings.latestOpenedRepositoriesPath = Json.encodeToString(_latestOpenedRepositoriesPaths) } fun loadRepositoriesTabs() { - val repositoriesSaved = appPreferences.latestTabsOpened + val repositoriesSaved = appSettings.latestTabsOpened if (repositoriesSaved.isNotEmpty()) { val repositoriesList = Json.decodeFromString>(repositoriesSaved) @@ -77,7 +77,7 @@ class AppStateManager @Inject constructor( } } - val repositoriesPathsSaved = appPreferences.latestOpenedRepositoriesPath + val repositoriesPathsSaved = appSettings.latestOpenedRepositoriesPath if (repositoriesPathsSaved.isNotEmpty()) { val repositories = Json.decodeFromString>(repositoriesPathsSaved) _latestOpenedRepositoriesPaths.addAll(repositories) diff --git a/src/main/kotlin/app/di/AppComponent.kt b/src/main/kotlin/app/di/AppComponent.kt index 873acb2..eabc353 100644 --- a/src/main/kotlin/app/di/AppComponent.kt +++ b/src/main/kotlin/app/di/AppComponent.kt @@ -2,7 +2,7 @@ package app.di import app.App import app.AppStateManager -import app.preferences.AppPreferences +import app.preferences.AppSettings import dagger.Component import javax.inject.Singleton @@ -12,5 +12,5 @@ interface AppComponent { fun inject(main: App) fun appStateManager(): AppStateManager - fun appPreferences(): AppPreferences + fun appPreferences(): AppSettings } \ No newline at end of file diff --git a/src/main/kotlin/app/extensions/DiffEntryExtensions.kt b/src/main/kotlin/app/extensions/DiffEntryExtensions.kt index e037eeb..18acccf 100644 --- a/src/main/kotlin/app/extensions/DiffEntryExtensions.kt +++ b/src/main/kotlin/app/extensions/DiffEntryExtensions.kt @@ -117,4 +117,13 @@ val DiffEntry.iconColor: Color DiffEntry.ChangeType.RENAME -> MaterialTheme.colors.modifyFile else -> throw NotImplementedError("Unexpected ChangeType") } - } \ No newline at end of file + } + +fun DiffEntry.toStatusType(): StatusType = when (this.changeType) { + DiffEntry.ChangeType.ADD -> StatusType.ADDED + DiffEntry.ChangeType.MODIFY -> StatusType.MODIFIED + DiffEntry.ChangeType.DELETE -> StatusType.REMOVED + DiffEntry.ChangeType.COPY -> StatusType.ADDED + DiffEntry.ChangeType.RENAME -> StatusType.MODIFIED + else -> throw NotImplementedError("Unexpected ChangeType") +} \ No newline at end of file diff --git a/src/main/kotlin/app/git/DiffEntryType.kt b/src/main/kotlin/app/git/DiffEntryType.kt index b72d121..125bae8 100644 --- a/src/main/kotlin/app/git/DiffEntryType.kt +++ b/src/main/kotlin/app/git/DiffEntryType.kt @@ -1,11 +1,25 @@ package app.git +import app.extensions.filePath +import app.extensions.toStatusType import org.eclipse.jgit.diff.DiffEntry sealed class DiffEntryType { - class CommitDiff(val diffEntry: DiffEntry) : DiffEntryType() + class CommitDiff(val diffEntry: DiffEntry) : DiffEntryType() { + override val filePath: String + get() = diffEntry.filePath - sealed class UncommitedDiff(val statusEntry: StatusEntry) : DiffEntryType() + override val statusType: StatusType + get() = diffEntry.toStatusType() + } + + sealed class UncommitedDiff(val statusEntry: StatusEntry) : DiffEntryType() { + override val filePath: String + get() = statusEntry.filePath + + override val statusType: StatusType + get() = statusEntry.statusType + } sealed class UnstagedDiff(statusEntry: StatusEntry) : UncommitedDiff(statusEntry) sealed class StagedDiff(statusEntry: StatusEntry) : UncommitedDiff(statusEntry) @@ -23,4 +37,7 @@ sealed class DiffEntryType { class SafeStagedDiff(statusEntry: StatusEntry) : StagedDiff(statusEntry) class SafeUnstagedDiff(statusEntry: StatusEntry) : UnstagedDiff(statusEntry) + abstract val filePath: String + abstract val statusType: StatusType + } diff --git a/src/main/kotlin/app/git/DiffManager.kt b/src/main/kotlin/app/git/DiffManager.kt index 3821d93..839fb32 100644 --- a/src/main/kotlin/app/git/DiffManager.kt +++ b/src/main/kotlin/app/git/DiffManager.kt @@ -130,4 +130,18 @@ fun prepareTreeParser(repository: Repository, commit: RevCommit): AbstractTreeIt return treeParser } -} \ No newline at end of file +} + +enum class TextDiffType(val value: Int) { + SPLIT(0), + UNIFIED(1); +} + + +fun textDiffTypeFromValue(diffTypeValue: Int): TextDiffType { + return when (diffTypeValue) { + TextDiffType.SPLIT.value -> TextDiffType.SPLIT + TextDiffType.UNIFIED.value -> TextDiffType.UNIFIED + else -> throw NotImplementedError("Diff type not implemented") + } +} diff --git a/src/main/kotlin/app/git/diff/Hunk.kt b/src/main/kotlin/app/git/diff/Hunk.kt index 196101b..5472f20 100644 --- a/src/main/kotlin/app/git/diff/Hunk.kt +++ b/src/main/kotlin/app/git/diff/Hunk.kt @@ -2,6 +2,8 @@ package app.git.diff data class Hunk(val header: String, val lines: List) +data class SplitHunk(val hunk: Hunk, val lines: List>) + data class Line(val text: String, val oldLineNumber: Int, val newLineNumber: Int, val lineType: LineType) { // lines numbers are stored based on 0 being the first one but on a file the first line is the 1, so increment it! val displayOldLineNumber: Int = oldLineNumber + 1 diff --git a/src/main/kotlin/app/git/diff/HunkDiffGenerator.kt b/src/main/kotlin/app/git/diff/HunkDiffGenerator.kt index 56b0dbf..3996b17 100644 --- a/src/main/kotlin/app/git/diff/HunkDiffGenerator.kt +++ b/src/main/kotlin/app/git/diff/HunkDiffGenerator.kt @@ -221,6 +221,10 @@ sealed class DiffResult( diffEntry: DiffEntry, val hunks: List ) : DiffResult(diffEntry) + class TextSplit( + diffEntry: DiffEntry, + val hunks: List + ) : DiffResult(diffEntry) class NonText( diffEntry: DiffEntry, diff --git a/src/main/kotlin/app/preferences/AppPreferences.kt b/src/main/kotlin/app/preferences/AppSettings.kt similarity index 88% rename from src/main/kotlin/app/preferences/AppPreferences.kt rename to src/main/kotlin/app/preferences/AppSettings.kt index 61b9804..2f59020 100644 --- a/src/main/kotlin/app/preferences/AppPreferences.kt +++ b/src/main/kotlin/app/preferences/AppSettings.kt @@ -1,6 +1,8 @@ package app.preferences import app.extensions.defaultWindowPlacement +import app.git.TextDiffType +import app.git.textDiffTypeFromValue import app.theme.ColorsScheme import app.theme.Theme import kotlinx.coroutines.flow.MutableStateFlow @@ -22,6 +24,7 @@ private const val PREF_COMMITS_LIMIT_ENABLED = "commitsLimitEnabled" 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_GIT_FF_MERGE = "gitFFMerge" @@ -31,7 +34,7 @@ private const val DEFAULT_COMMITS_LIMIT_ENABLED = true const val DEFAULT_UI_SCALE = -1f @Singleton -class AppPreferences @Inject constructor() { +class AppSettings @Inject constructor() { private val preferences: Preferences = Preferences.userRoot().node(PREFERENCES_NAME) private val _themeState = MutableStateFlow(theme) @@ -52,6 +55,9 @@ class AppPreferences @Inject constructor() { private val _scaleUiFlow = MutableStateFlow(scaleUi) val scaleUiFlow: StateFlow = _scaleUiFlow + private val _textDiffTypeFlow = MutableStateFlow(textDiffType) + val textDiffTypeFlow: StateFlow = _textDiffTypeFlow + var latestTabsOpened: String get() = preferences.get(PREF_LATEST_REPOSITORIES_TABS_OPENED, "") set(value) { @@ -128,6 +134,18 @@ class AppPreferences @Inject constructor() { preferences.putInt(PREF_WINDOW_PLACEMENT, placement.value) } + var textDiffType: TextDiffType + get() { + val diffTypeValue = preferences.getInt(PREF_DIFF_TYPE, TextDiffType.UNIFIED.value) + + return textDiffTypeFromValue(diffTypeValue) + } + set(placement) { + preferences.putInt(PREF_DIFF_TYPE, placement.value) + + _textDiffTypeFlow.value = textDiffType + } + fun saveCustomTheme(filePath: String) { try { val file = File(filePath) diff --git a/src/main/kotlin/app/theme/Color.kt b/src/main/kotlin/app/theme/Color.kt index 7708f7e..3617948 100644 --- a/src/main/kotlin/app/theme/Color.kt +++ b/src/main/kotlin/app/theme/Color.kt @@ -25,6 +25,8 @@ val lightTheme = ColorsScheme( dialogOverlay = Color(0xAA000000), normalScrollbar = Color(0xFFCCCCCC), hoverScrollbar = Color(0xFF0070D8), + diffLineAdded = Color(0xFFd7ebd0), + diffLineRemoved = Color(0xFFf0d4d4), ) @@ -50,7 +52,10 @@ val darkBlueTheme = ColorsScheme( conflictingFile = Color(0xFFFFB638), dialogOverlay = Color(0xAA000000), normalScrollbar = Color(0xFF888888), - hoverScrollbar = Color(0xFFCCCCCC) + hoverScrollbar = Color(0xFFCCCCCC), + diffLineAdded = Color(0xFF566f5a), + diffLineRemoved = Color(0xFF6f585e), + ) val darkGrayTheme = ColorsScheme( @@ -75,5 +80,7 @@ val darkGrayTheme = ColorsScheme( conflictingFile = Color(0xFFFFB638), dialogOverlay = Color(0xAA000000), normalScrollbar = Color(0xFF888888), - hoverScrollbar = Color(0xFFCCCCCC) + hoverScrollbar = Color(0xFFCCCCCC), + diffLineAdded = Color(0xFF5b7059), + diffLineRemoved = Color(0xFF74595c), ) \ No newline at end of file diff --git a/src/main/kotlin/app/theme/ColorsScheme.kt b/src/main/kotlin/app/theme/ColorsScheme.kt index 3275f81..8c2f8d2 100644 --- a/src/main/kotlin/app/theme/ColorsScheme.kt +++ b/src/main/kotlin/app/theme/ColorsScheme.kt @@ -14,6 +14,7 @@ import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +// TODO Add line added + removed colors, graph colors and icons color for added/modified/removed files. @Serializable data class ColorsScheme( val primary: Color, @@ -39,6 +40,8 @@ data class ColorsScheme( val dialogOverlay: Color, val normalScrollbar: Color, val hoverScrollbar: Color, + val diffLineAdded: Color, + val diffLineRemoved: Color, ) { fun toComposeColors(): Colors { return Colors( diff --git a/src/main/kotlin/app/theme/Theme.kt b/src/main/kotlin/app/theme/Theme.kt index 99b3b54..90b1691 100644 --- a/src/main/kotlin/app/theme/Theme.kt +++ b/src/main/kotlin/app/theme/Theme.kt @@ -92,6 +92,12 @@ val Colors.secondarySurface: Color val Colors.dialogOverlay: Color get() = appTheme.dialogOverlay +val Colors.diffLineAdded: Color + get() = appTheme.diffLineAdded + +val Colors.diffLineRemoved: Color + get() = appTheme.diffLineRemoved + enum class Theme(val displayName: String) : DropDownOption { LIGHT("Light"), diff --git a/src/main/kotlin/app/ui/FileHistory.kt b/src/main/kotlin/app/ui/FileHistory.kt index 77dfa10..deb630f 100644 --- a/src/main/kotlin/app/ui/FileHistory.kt +++ b/src/main/kotlin/app/ui/FileHistory.kt @@ -162,22 +162,36 @@ fun HistoryContentLoaded( viewDiffResult != null && viewDiffResult is ViewDiffResult.Loaded ) { - val diffResult = viewDiffResult.diffResult - if (diffResult is DiffResult.Text) { - TextDiff( - diffEntryType = viewDiffResult.diffEntryType, - scrollState = scrollState, - diffResult = diffResult, - onUnstageHunk = { _, _ -> }, - onStageHunk = { _, _ -> }, - onResetHunk = { _, _ -> }, - ) - } else { - Box( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colors.background) - ) + when (val diffResult = viewDiffResult.diffResult) { + is DiffResult.Text -> { + HunkUnifiedTextDiff( + diffEntryType = viewDiffResult.diffEntryType, + scrollState = scrollState, + diffResult = diffResult, + onUnstageHunk = { _, _ -> }, + onStageHunk = { _, _ -> }, + onResetHunk = { _, _ -> }, + ) + } + + is DiffResult.TextSplit -> { + HunkSplitTextDiff( + diffEntryType = viewDiffResult.diffEntryType, + scrollState = scrollState, + diffResult = diffResult, + onUnstageHunk = { _, _ -> }, + onStageHunk = { _, _ -> }, + onResetHunk = { _, _ -> }, + ) + } + + else -> { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colors.background) + ) + } } } else { Box( diff --git a/src/main/kotlin/app/ui/Diff.kt b/src/main/kotlin/app/ui/diff/Diff.kt similarity index 58% rename from src/main/kotlin/app/ui/Diff.kt rename to src/main/kotlin/app/ui/diff/Diff.kt index 2ea3d25..6ec4cd3 100644 --- a/src/main/kotlin/app/ui/Diff.kt +++ b/src/main/kotlin/app/ui/diff/Diff.kt @@ -10,10 +10,7 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.text.selection.DisableSelection import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.material.IconButton -import androidx.compose.material.LinearProgressIndicator -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text +import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi @@ -34,10 +31,7 @@ import androidx.compose.ui.unit.sp import app.extensions.lineDelimiter import app.extensions.removeLineDelimiters import app.extensions.toStringWithSpaces -import app.git.DiffEntryType -import app.git.EntryContent -import app.git.StatusEntry -import app.git.StatusType +import app.git.* import app.git.diff.DiffResult import app.git.diff.Hunk import app.git.diff.Line @@ -61,6 +55,7 @@ fun Diff( onCloseDiffView: () -> Unit, ) { val diffResultState = diffViewModel.diffResult.collectAsState() + val diffType by diffViewModel.diffTypeFlow.collectAsState() val viewDiffResult = diffResultState.value ?: return val focusRequester = remember { FocusRequester() } @@ -86,6 +81,7 @@ fun Diff( ViewDiffResult.DiffNotFound -> { onCloseDiffView() } + is ViewDiffResult.Loaded -> { val diffEntryType = viewDiffResult.diffEntryType val diffEntry = viewDiffResult.diffResult.diffEntry @@ -95,14 +91,16 @@ fun Diff( diffEntryType = diffEntryType, diffEntry = diffEntry, onCloseDiffView = onCloseDiffView, - stageFile = { diffViewModel.stageFile(it) }, - unstageFile = { diffViewModel.unstageFile(it) }, + diffType = diffType, + onStageFile = { diffViewModel.stageFile(it) }, + onUnstageFile = { diffViewModel.unstageFile(it) }, + onChangeDiffType = { diffViewModel.changeTextDiffType(it) } ) - if (diffResult is DiffResult.Text) { - val scrollState by diffViewModel.lazyListState.collectAsState() + val scrollState by diffViewModel.lazyListState.collectAsState() - TextDiff( + when (diffResult) { + is DiffResult.TextSplit -> HunkSplitTextDiff( diffEntryType = diffEntryType, scrollState = scrollState, diffResult = diffResult, @@ -116,13 +114,40 @@ fun Diff( diffViewModel.resetHunk(entry, hunk) } ) - } else if (diffResult is DiffResult.NonText) { - NonTextDiff(diffResult) + + is DiffResult.Text -> HunkUnifiedTextDiff( + diffEntryType = diffEntryType, + scrollState = scrollState, + diffResult = diffResult, + onUnstageHunk = { entry, hunk -> + diffViewModel.unstageHunk(entry, hunk) + }, + onStageHunk = { entry, hunk -> + diffViewModel.stageHunk(entry, hunk) + }, + onResetHunk = { entry, hunk -> + diffViewModel.resetHunk(entry, hunk) + } + ) + + is DiffResult.NonText -> { + NonTextDiff(diffResult) + } } } - ViewDiffResult.Loading, ViewDiffResult.None -> { - LinearProgressIndicator(modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colors.primaryVariant) + + is ViewDiffResult.Loading -> { + Column { + PathOnlyDiffHeader(filePath = viewDiffResult.filePath, onCloseDiffView = onCloseDiffView) + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colors.primaryVariant + ) + } + } + + ViewDiffResult.None -> throw NotImplementedError("None should be a possible state in the diff") } @@ -225,8 +250,9 @@ fun BinaryDiff() { ) } + @Composable -fun TextDiff( +fun HunkUnifiedTextDiff( diffEntryType: DiffEntryType, scrollState: LazyListState, diffResult: DiffResult.Text, @@ -246,7 +272,7 @@ fun TextDiff( item { DisableSelection { HunkHeader( - hunk = hunk, + header = hunk.header, diffEntryType = diffEntryType, onUnstageHunk = { onUnstageHunk(diffResult.diffEntry, hunk) }, onStageHunk = { onStageHunk(diffResult.diffEntry, hunk) }, @@ -269,9 +295,74 @@ fun TextDiff( } +@Composable +fun HunkSplitTextDiff( + diffEntryType: DiffEntryType, + scrollState: LazyListState, + diffResult: DiffResult.TextSplit, + onUnstageHunk: (DiffEntry, Hunk) -> Unit, + onStageHunk: (DiffEntry, Hunk) -> Unit, + onResetHunk: (DiffEntry, Hunk) -> Unit, +) { + val hunks = diffResult.hunks + + SelectionContainer { + ScrollableLazyColumn( + modifier = Modifier + .fillMaxSize(), + state = scrollState + ) { + for (splitHunk in hunks) { + item { + DisableSelection { + HunkHeader( + header = splitHunk.hunk.header, + diffEntryType = diffEntryType, + onUnstageHunk = { onUnstageHunk(diffResult.diffEntry, splitHunk.hunk) }, + onStageHunk = { onStageHunk(diffResult.diffEntry, splitHunk.hunk) }, + onResetHunk = { onResetHunk(diffResult.diffEntry, splitHunk.hunk) }, + ) + } + } + + val oldHighestLineNumber = splitHunk.hunk.lines.maxOf { it.displayOldLineNumber } + val newHighestLineNumber = splitHunk.hunk.lines.maxOf { it.displayNewLineNumber } + val highestLineNumber = max(oldHighestLineNumber, newHighestLineNumber) + val highestLineNumberLength = highestLineNumber.toString().count() + + items(splitHunk.lines) { linesPair -> + SplitDiffLine(highestLineNumberLength, linesPair.first, linesPair.second) + } + } + } + } + +} + +@Composable +fun SplitDiffLine(highestLineNumberLength: Int, first: Line?, second: Line?) { + Row( + modifier = Modifier + .background(MaterialTheme.colors.secondarySurface) + ) { + Box( + modifier = Modifier + .weight(1f) + ) { + if (first != null) + SplitDiffLine(highestLineNumberLength, first, first.oldLineNumber + 1) + } + + Box(modifier = Modifier.weight(1f)) { + if (second != null) + SplitDiffLine(highestLineNumberLength, second, second.newLineNumber + 1) + } + } +} + @Composable fun HunkHeader( - hunk: Hunk, + header: String, diffEntryType: DiffEntryType, onUnstageHunk: () -> Unit, onStageHunk: () -> Unit, @@ -285,7 +376,7 @@ fun HunkHeader( verticalAlignment = Alignment.CenterVertically ) { Text( - text = hunk.header, + text = header, color = MaterialTheme.colors.primaryTextColor, style = MaterialTheme.typography.body1, ) @@ -332,13 +423,16 @@ fun HunkHeader( } } +@OptIn(ExperimentalComposeUiApi::class) @Composable -fun DiffHeader( +private fun DiffHeader( diffEntryType: DiffEntryType, diffEntry: DiffEntry, + diffType: TextDiffType, onCloseDiffView: () -> Unit, - stageFile: (StatusEntry) -> Unit, - unstageFile: (StatusEntry) -> Unit, + onStageFile: (StatusEntry) -> Unit, + onUnstageFile: (StatusEntry) -> Unit, + onChangeDiffType: (TextDiffType) -> Unit, ) { Row( modifier = Modifier @@ -362,28 +456,15 @@ fun DiffHeader( Spacer(modifier = Modifier.weight(1f)) + if (diffEntryType.statusType != StatusType.ADDED && diffEntryType.statusType != StatusType.REMOVED) { + DiffTypeButtons(diffType = diffType, onChangeDiffType = onChangeDiffType) + } + if (diffEntryType is DiffEntryType.UncommitedDiff) { - val buttonText: String - val color: Color - - if (diffEntryType is DiffEntryType.StagedDiff) { - buttonText = "Unstage file" - color = MaterialTheme.colors.error - } else { - buttonText = "Stage file" - color = MaterialTheme.colors.primary - } - - SecondaryButton( - text = buttonText, - backgroundButton = color, - onClick = { - if (diffEntryType is DiffEntryType.StagedDiff) { - unstageFile(diffEntryType.statusEntry) - } else { - stageFile(diffEntryType.statusEntry) - } - } + UncommitedDiffFileHeaderButtons( + diffEntryType, + onUnstageFile = onUnstageFile, + onStageFile = onStageFile ) } @@ -401,21 +482,120 @@ fun DiffHeader( } } +@Composable +fun DiffTypeButtons(diffType: TextDiffType, onChangeDiffType: (TextDiffType) -> Unit) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(horizontal = 16.dp) + ) { + Text( + "Unified", + color = MaterialTheme.colors.primaryTextColor, + 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.primaryTextColor, +// modifier = Modifier.padding(horizontal = 4.dp), + style = MaterialTheme.typography.caption, + ) + } +} + +@Composable +fun UncommitedDiffFileHeaderButtons( + diffEntryType: DiffEntryType.UncommitedDiff, + onUnstageFile: (StatusEntry) -> Unit, + onStageFile: (StatusEntry) -> Unit +) { + val buttonText: String + val color: Color + + if (diffEntryType is DiffEntryType.StagedDiff) { + buttonText = "Unstage file" + color = MaterialTheme.colors.error + } else { + buttonText = "Stage file" + color = MaterialTheme.colors.primary + } + + SecondaryButton( + text = buttonText, + backgroundButton = color, + onClick = { + if (diffEntryType is DiffEntryType.StagedDiff) { + onUnstageFile(diffEntryType.statusEntry) + } else { + onStageFile(diffEntryType.statusEntry) + } + } + ) +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +private fun PathOnlyDiffHeader( + filePath: String, + onCloseDiffView: () -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(40.dp) + .background(MaterialTheme.colors.headerBackground) + .padding(start = 8.dp, end = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = filePath, + style = MaterialTheme.typography.body2, + maxLines = 1, + modifier = Modifier.padding(horizontal = 16.dp), + ) + + Spacer(modifier = Modifier.weight(1f)) + + IconButton( + onClick = onCloseDiffView, + modifier = Modifier + .pointerHoverIcon(PointerIconDefaults.Hand) + ) { + Image( + painter = painterResource("close.svg"), + contentDescription = "Close diff", + colorFilter = ColorFilter.tint(MaterialTheme.colors.primaryTextColor), + ) + } + } +} + @Composable fun DiffLine( highestLineNumberLength: Int, line: Line, ) { val backgroundColor = when (line.lineType) { - LineType.ADDED -> { - Color(0x77a9d49b) - } - LineType.REMOVED -> { - Color(0x77dea2a2) - } - LineType.CONTEXT -> { - MaterialTheme.colors.background - } + LineType.ADDED -> MaterialTheme.colors.diffLineAdded + LineType.REMOVED -> MaterialTheme.colors.diffLineRemoved + LineType.CONTEXT -> MaterialTheme.colors.background } Row( modifier = Modifier @@ -442,33 +622,65 @@ fun DiffLine( ) } - Row { - Text( - text = line.text.replace( // TODO this replace is a workaround until this issue gets fixed https://github.com/JetBrains/compose-jb/issues/615 - "\t", - " " - ).removeLineDelimiters(), - modifier = Modifier - .padding(start = 8.dp) - .fillMaxSize(), - fontFamily = FontFamily.Monospace, - style = MaterialTheme.typography.body2, - overflow = TextOverflow.Visible, + DiffLineText(line.text) + } +} + +@Composable +fun SplitDiffLine( + highestLineNumberLength: Int, + line: Line, + lineNumber: Int, +) { + val backgroundColor = when (line.lineType) { + LineType.ADDED -> MaterialTheme.colors.diffLineAdded + LineType.REMOVED -> MaterialTheme.colors.diffLineRemoved + LineType.CONTEXT -> MaterialTheme.colors.background + } + Row( + modifier = Modifier + .background(backgroundColor) + .height(IntrinsicSize.Min) + ) { + DisableSelection { + LineNumber( + text = lineNumber.toStringWithSpaces(highestLineNumberLength), ) - - val lineDelimiter = line.text.lineDelimiter - - // Display line delimiter in its own text with a maxLines = 1. This will fix the issue - // where copying a line didn't contain the line ending & also fix the issue where the text line would - // display multiple lines even if there is only a single line with a line delimiter at the end - if (lineDelimiter != null) { - Text( - text = lineDelimiter, - maxLines = 1, - ) - } - } + + DiffLineText(line.text) + } +} + + +@Composable +fun DiffLineText(text: String) { + Row { + Text( + text = text.replace( + "\t", + " " + ).removeLineDelimiters(), + modifier = Modifier + .padding(start = 8.dp) + .fillMaxSize(), + fontFamily = FontFamily.Monospace, + style = MaterialTheme.typography.body2, + overflow = TextOverflow.Visible, + ) + + val lineDelimiter = text.lineDelimiter + + // Display line delimiter in its own text with a maxLines = 1. This will fix the issue + // where copying a line didn't contain the line ending & also fix the issue where the text line would + // display multiple lines even if there is only a single line with a line delimiter at the end + if (lineDelimiter != null) { + Text( + text = lineDelimiter, + maxLines = 1, + ) + } + } } diff --git a/src/main/kotlin/app/ui/log/Log.kt b/src/main/kotlin/app/ui/log/Log.kt index 29492ba..132f7b1 100644 --- a/src/main/kotlin/app/ui/log/Log.kt +++ b/src/main/kotlin/app/ui/log/Log.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.clipRect import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.PathBuilder import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.key.type @@ -102,6 +103,7 @@ fun Log( val logStatus = logStatusState.value val showLogDialog by logViewModel.logDialog.collectAsState() + val selectedCommit = if (selectedItem is SelectedItem.CommitBasedItem) { selectedItem.revCommit } else { diff --git a/src/main/kotlin/app/viewmodels/BranchesViewModel.kt b/src/main/kotlin/app/viewmodels/BranchesViewModel.kt index 6ee4960..af7a3c0 100644 --- a/src/main/kotlin/app/viewmodels/BranchesViewModel.kt +++ b/src/main/kotlin/app/viewmodels/BranchesViewModel.kt @@ -1,7 +1,7 @@ package app.viewmodels import app.git.* -import app.preferences.AppPreferences +import app.preferences.AppSettings import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import org.eclipse.jgit.api.Git @@ -14,7 +14,7 @@ class BranchesViewModel @Inject constructor( private val mergeManager: MergeManager, private val remoteOperationsManager: RemoteOperationsManager, private val tabState: TabState, - private val appPreferences: AppPreferences, + private val appSettings: AppSettings, ) : ExpandableViewModel(true) { private val _branches = MutableStateFlow>(listOf()) val branches: StateFlow> @@ -51,7 +51,7 @@ class BranchesViewModel @Inject constructor( fun mergeBranch(ref: Ref) = tabState.safeProcessing( refreshType = RefreshType.ALL_DATA, ) { git -> - mergeManager.mergeBranch(git, ref, appPreferences.ffMerge) + mergeManager.mergeBranch(git, ref, appSettings.ffMerge) } fun deleteBranch(branch: Ref) = tabState.safeProcessing( diff --git a/src/main/kotlin/app/viewmodels/DiffViewModel.kt b/src/main/kotlin/app/viewmodels/DiffViewModel.kt index 02b1bce..f6cec03 100644 --- a/src/main/kotlin/app/viewmodels/DiffViewModel.kt +++ b/src/main/kotlin/app/viewmodels/DiffViewModel.kt @@ -1,13 +1,19 @@ +//asdasd package app.viewmodels import androidx.compose.foundation.lazy.LazyListState import app.exceptions.MissingDiffEntryException import app.extensions.delayedStateChange import app.git.* -import app.git.diff.Hunk +import app.git.diff.* +import app.preferences.AppSettings +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch import org.eclipse.jgit.diff.DiffEntry +import java.lang.Integer.max import javax.inject.Inject private const val DIFF_MIN_TIME_IN_MS_TO_SHOW_LOAD = 200L @@ -16,10 +22,30 @@ class DiffViewModel @Inject constructor( private val tabState: TabState, private val diffManager: DiffManager, private val statusManager: StatusManager, + private val settings: AppSettings, ) { - private val _diffResult = MutableStateFlow(ViewDiffResult.Loading) + private val _diffResult = MutableStateFlow(ViewDiffResult.Loading("")) val diffResult: StateFlow = _diffResult + val diffTypeFlow = settings.textDiffTypeFlow + private var diffEntryType: DiffEntryType? = null + private var diffTypeFlowChangesCount = 0 + + private var diffJob: Job? = null + + init { + tabState.managerScope.launch { + diffTypeFlow.collect { + val diffEntryType = this@DiffViewModel.diffEntryType + if (diffTypeFlowChangesCount > 0 && diffEntryType != null) { // Ignore the first time the flow triggers, we only care about updates + updateDiff(diffEntryType) + } + + diffTypeFlowChangesCount++ + } + } + } + val lazyListState = MutableStateFlow( LazyListState( 0, @@ -27,46 +53,155 @@ class DiffViewModel @Inject constructor( ) ) - // TODO Cancel job if the user closed the diff view while loading - fun updateDiff(diffEntryType: DiffEntryType) = tabState.runOperation( - refreshType = RefreshType.NONE, - ) { git -> - var oldDiffEntryType: DiffEntryType? = null - val oldDiffResult = _diffResult.value + fun updateDiff(diffEntryType: DiffEntryType) { + diffJob = tabState.runOperation(refreshType = RefreshType.NONE) { git -> + this.diffEntryType = diffEntryType - if (oldDiffResult is ViewDiffResult.Loaded) { - oldDiffEntryType = oldDiffResult.diffEntryType - } + var oldDiffEntryType: DiffEntryType? = null + val oldDiffResult = _diffResult.value - - // If it's a different file or different state (index or workdir), reset the scroll state - if (oldDiffEntryType != null && - oldDiffEntryType is DiffEntryType.UncommitedDiff && diffEntryType is DiffEntryType.UncommitedDiff && - oldDiffEntryType.statusEntry.filePath != diffEntryType.statusEntry.filePath - ) { - lazyListState.value = LazyListState( - 0, - 0 - ) - } - - try { - delayedStateChange( - delayMs = DIFF_MIN_TIME_IN_MS_TO_SHOW_LOAD, - onDelayTriggered = { _diffResult.value = ViewDiffResult.Loading } - ) { - val diffFormat = diffManager.diffFormat(git, diffEntryType) - _diffResult.value = ViewDiffResult.Loaded(diffEntryType, diffFormat) + if (oldDiffResult is ViewDiffResult.Loaded) { + oldDiffEntryType = oldDiffResult.diffEntryType + } + + // If it's a different file or different state (index or workdir), reset the scroll state + if (oldDiffEntryType != null && + oldDiffEntryType is DiffEntryType.UncommitedDiff && + diffEntryType is DiffEntryType.UncommitedDiff && + oldDiffEntryType.statusEntry.filePath != diffEntryType.statusEntry.filePath + ) { + lazyListState.value = LazyListState( + 0, + 0 + ) + } + + val isFirstLoad = oldDiffResult is ViewDiffResult.Loading && oldDiffResult.filePath.isEmpty() + + try { + delayedStateChange( + delayMs = if (isFirstLoad) 0 else DIFF_MIN_TIME_IN_MS_TO_SHOW_LOAD, + onDelayTriggered = { _diffResult.value = ViewDiffResult.Loading(diffEntryType.filePath) } + ) { + val diffFormat = diffManager.diffFormat(git, diffEntryType) + val diffEntry = diffFormat.diffEntry + if ( + diffTypeFlow.value == TextDiffType.SPLIT && + diffFormat is DiffResult.Text && + diffEntry.changeType != DiffEntry.ChangeType.ADD && + diffEntry.changeType != DiffEntry.ChangeType.DELETE + ) { + val splitHunkList = generateSplitDiffFormat(diffFormat) + _diffResult.value = ViewDiffResult.Loaded( + diffEntryType, + DiffResult.TextSplit(diffEntry, splitHunkList) + ) + } else { + _diffResult.value = ViewDiffResult.Loaded(diffEntryType, diffFormat) + } + } + } catch (ex: Exception) { + if (ex is MissingDiffEntryException) { + tabState.refreshData(refreshType = RefreshType.UNCOMMITED_CHANGES) + _diffResult.value = ViewDiffResult.DiffNotFound + } else + ex.printStackTrace() } - } catch (ex: Exception) { - if (ex is MissingDiffEntryException) { - tabState.refreshData(refreshType = RefreshType.UNCOMMITED_CHANGES) - _diffResult.value = ViewDiffResult.DiffNotFound - } else - ex.printStackTrace() } } + private fun generateSplitDiffFormat(diffFormat: DiffResult.Text): List { + val unifiedHunksList = diffFormat.hunks + val hunksList = mutableListOf() + + for (hunk in unifiedHunksList) { + val linesNewSideCount = + hunk.lines.count { it.lineType == LineType.ADDED || it.lineType == LineType.CONTEXT } + val linesOldSideCount = + hunk.lines.count { it.lineType == LineType.REMOVED || it.lineType == LineType.CONTEXT } + + val addedLines = hunk.lines.filter { it.lineType == LineType.ADDED } + val removedLines = hunk.lines.filter { it.lineType == LineType.REMOVED } + + val maxLinesCountOfBothParts = max(linesNewSideCount, linesOldSideCount) + + val oldLinesArray = arrayOfNulls(maxLinesCountOfBothParts) + val newLinesArray = arrayOfNulls(maxLinesCountOfBothParts) + + val lines = hunk.lines + val firstLineOldNumber = hunk.lines.first().oldLineNumber + val firstLineNewNumber = hunk.lines.first().newLineNumber + + val firstLine = if (maxLinesCountOfBothParts == linesOldSideCount) { + firstLineOldNumber + } else + firstLineNewNumber + + val contextLines = lines.filter { it.lineType == LineType.CONTEXT } + + for (contextLine in contextLines) { + + val lineNumber = if (maxLinesCountOfBothParts == linesOldSideCount) { + contextLine.oldLineNumber + } else + contextLine.newLineNumber + + oldLinesArray[lineNumber - firstLine] = contextLine + newLinesArray[lineNumber - firstLine] = contextLine + } + + for (removedLine in removedLines) { + val previousLinesToCurrent = lines.takeWhile { it != removedLine } + val previousContextLine = previousLinesToCurrent.lastOrNull { it.lineType == LineType.CONTEXT } + + val contextArrayPosition = if (previousContextLine != null) + oldLinesArray.indexOf(previousContextLine) + else + -1 + + // Get the position the list of null position of the array + val availableIndexes = + newLinesArray.mapIndexed { index, line -> + if (line != null) + null + else + index + }.filterNotNull() + val nextAvailableLinePosition = availableIndexes.first { index -> index > contextArrayPosition } + oldLinesArray[nextAvailableLinePosition] = removedLine + } + + for (addedLine in addedLines) { + val previousLinesToCurrent = lines.takeWhile { it != addedLine } + val previousContextLine = previousLinesToCurrent.lastOrNull { it.lineType == LineType.CONTEXT } + + val contextArrayPosition = if (previousContextLine != null) + newLinesArray.indexOf(previousContextLine) + else + -1 + + val availableIndexes = + newLinesArray.mapIndexed { index, line -> if (line != null) null else index }.filterNotNull() + val newLinePosition = availableIndexes.first { index -> index > contextArrayPosition } + + newLinesArray[newLinePosition] = addedLine + } + + val newHunkLines = mutableListOf>() + + for (i in 0 until maxLinesCountOfBothParts) { + val old = oldLinesArray[i] + val new = newLinesArray[i] + + newHunkLines.add(old to new) + } + + hunksList.add(SplitHunk(hunk, newHunkLines)) + } + + return hunksList + } + fun stageHunk(diffEntry: DiffEntry, hunk: Hunk) = tabState.runOperation( refreshType = RefreshType.UNCOMMITED_CHANGES, ) { git -> @@ -97,6 +232,12 @@ class DiffViewModel @Inject constructor( ) { git -> statusManager.unstage(git, statusEntry) } -} + fun cancelRunningJobs() { + diffJob?.cancel() + } + fun changeTextDiffType(newDiffType: TextDiffType) { + settings.textDiffType = newDiffType + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/viewmodels/HistoryViewModel.kt b/src/main/kotlin/app/viewmodels/HistoryViewModel.kt index d6a30a7..1c2e560 100644 --- a/src/main/kotlin/app/viewmodels/HistoryViewModel.kt +++ b/src/main/kotlin/app/viewmodels/HistoryViewModel.kt @@ -7,6 +7,7 @@ import app.git.DiffEntryType import app.git.DiffManager import app.git.RefreshType import app.git.TabState +import app.preferences.AppSettings import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import org.eclipse.jgit.revwalk.RevCommit @@ -15,6 +16,7 @@ import javax.inject.Inject class HistoryViewModel @Inject constructor( private val tabState: TabState, private val diffManager: DiffManager, + private val settings: AppSettings, ) { private val _historyState = MutableStateFlow(HistoryState.Loading("")) val historyState: StateFlow = _historyState diff --git a/src/main/kotlin/app/viewmodels/LogViewModel.kt b/src/main/kotlin/app/viewmodels/LogViewModel.kt index a7f47de..ae9937d 100644 --- a/src/main/kotlin/app/viewmodels/LogViewModel.kt +++ b/src/main/kotlin/app/viewmodels/LogViewModel.kt @@ -6,7 +6,7 @@ import app.extensions.delayedStateChange import app.git.* import app.git.graph.GraphCommitList import app.git.graph.GraphNode -import app.preferences.AppPreferences +import app.preferences.AppSettings import app.ui.SelectedItem import app.ui.log.LogDialog import kotlinx.coroutines.CoroutineScope @@ -40,7 +40,7 @@ class LogViewModel @Inject constructor( private val mergeManager: MergeManager, private val remoteOperationsManager: RemoteOperationsManager, private val tabState: TabState, - private val appPreferences: AppPreferences, + private val appSettings: AppSettings, ) { private val _logStatus = MutableStateFlow(LogStatus.Loading) @@ -66,12 +66,12 @@ class LogViewModel @Inject constructor( init { scope.launch { - appPreferences.commitsLimitEnabledFlow.collect { + appSettings.commitsLimitEnabledFlow.collect { tabState.refreshData(RefreshType.ONLY_LOG) } } scope.launch { - appPreferences.commitsLimitFlow.collect { + appSettings.commitsLimitFlow.collect { tabState.refreshData(RefreshType.ONLY_LOG) } } @@ -90,13 +90,13 @@ class LogViewModel @Inject constructor( ) val hasUncommitedChanges = statusSummary.total > 0 - val commitsLimit = if (appPreferences.commitsLimitEnabled) { - appPreferences.commitsLimit + val commitsLimit = if (appSettings.commitsLimitEnabled) { + appSettings.commitsLimit } else Int.MAX_VALUE - val commitsLimitDisplayed = if (appPreferences.commitsLimitEnabled) { - appPreferences.commitsLimit + val commitsLimitDisplayed = if (appSettings.commitsLimitEnabled) { + appSettings.commitsLimit } else -1 @@ -176,7 +176,7 @@ class LogViewModel @Inject constructor( fun mergeBranch(ref: Ref) = tabState.safeProcessing( refreshType = RefreshType.ALL_DATA, ) { git -> - mergeManager.mergeBranch(git, ref, appPreferences.ffMerge) + mergeManager.mergeBranch(git, ref, appSettings.ffMerge) } fun deleteBranch(branch: Ref) = tabState.safeProcessing( diff --git a/src/main/kotlin/app/viewmodels/SettingsViewModel.kt b/src/main/kotlin/app/viewmodels/SettingsViewModel.kt index 604080c..d1c9e2d 100644 --- a/src/main/kotlin/app/viewmodels/SettingsViewModel.kt +++ b/src/main/kotlin/app/viewmodels/SettingsViewModel.kt @@ -1,60 +1,60 @@ package app.viewmodels -import app.preferences.AppPreferences +import app.preferences.AppSettings import app.theme.Theme import javax.inject.Inject import javax.inject.Singleton @Singleton class SettingsViewModel @Inject constructor( - val appPreferences: AppPreferences, + val appSettings: AppSettings, ) { // Temporary values to detect changed variables var commitsLimit: Int = -1 - val themeState = appPreferences.themeState - val customThemeFlow = appPreferences.customThemeFlow - val ffMergeFlow = appPreferences.ffMergeFlow - val commitsLimitEnabledFlow = appPreferences.commitsLimitEnabledFlow + val themeState = appSettings.themeState + val customThemeFlow = appSettings.customThemeFlow + val ffMergeFlow = appSettings.ffMergeFlow + val commitsLimitEnabledFlow = appSettings.commitsLimitEnabledFlow var scaleUi: Float - get() = appPreferences.scaleUi + get() = appSettings.scaleUi set(value) { - appPreferences.scaleUi = value + appSettings.scaleUi = value } var commitsLimitEnabled: Boolean - get() = appPreferences.commitsLimitEnabled + get() = appSettings.commitsLimitEnabled set(value) { - appPreferences.commitsLimitEnabled = value + appSettings.commitsLimitEnabled = value } var ffMerge: Boolean - get() = appPreferences.ffMerge + get() = appSettings.ffMerge set(value) { - appPreferences.ffMerge = value + appSettings.ffMerge = value } var theme: Theme - get() = appPreferences.theme + get() = appSettings.theme set(value) { - appPreferences.theme = value + appSettings.theme = value } fun saveCustomTheme(filePath: String) { - appPreferences.saveCustomTheme(filePath) + appSettings.saveCustomTheme(filePath) } fun resetInfo() { - commitsLimit = appPreferences.commitsLimit + commitsLimit = appSettings.commitsLimit } fun savePendingChanges() { val commitsLimit = this.commitsLimit - if (appPreferences.commitsLimit != commitsLimit) { - appPreferences.commitsLimit = commitsLimit + if (appSettings.commitsLimit != commitsLimit) { + appSettings.commitsLimit = commitsLimit } } } \ No newline at end of file diff --git a/src/main/kotlin/app/viewmodels/TabViewModel.kt b/src/main/kotlin/app/viewmodels/TabViewModel.kt index f089258..fe0207c 100644 --- a/src/main/kotlin/app/viewmodels/TabViewModel.kt +++ b/src/main/kotlin/app/viewmodels/TabViewModel.kt @@ -352,8 +352,10 @@ class TabViewModel @Inject constructor( diffViewModel = diffViewModelProvider.get() } + diffViewModel?.cancelRunningJobs() diffViewModel?.updateDiff(diffSelected) } else { + diffViewModel?.cancelRunningJobs() diffViewModel = null // Free the view model from the memory if not being used. } } diff --git a/src/main/kotlin/app/viewmodels/ViewDiffResult.kt b/src/main/kotlin/app/viewmodels/ViewDiffResult.kt index 78f9481..b9eae9b 100644 --- a/src/main/kotlin/app/viewmodels/ViewDiffResult.kt +++ b/src/main/kotlin/app/viewmodels/ViewDiffResult.kt @@ -1,11 +1,15 @@ package app.viewmodels import app.git.DiffEntryType +import app.git.TextDiffType import app.git.diff.DiffResult sealed interface ViewDiffResult { object None : ViewDiffResult - object Loading : ViewDiffResult + + data class Loading(val filePath: String) : ViewDiffResult + object DiffNotFound : ViewDiffResult - data class Loaded(val diffEntryType: DiffEntryType, val diffResult: DiffResult) : ViewDiffResult + + data class Loaded(val diffEntryType: DiffEntryType, val diffResult: DiffResult/*, val diffType: TextDiffType*/) : ViewDiffResult } \ No newline at end of file