Added basic split diff
This commit is contained in:
parent
34164fb2bc
commit
5659bf8918
@ -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<List<TabInformation>>(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<Int>,
|
||||
|
@ -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)
|
||||
|
@ -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<List<String>>(repositoriesSaved)
|
||||
@ -77,7 +77,7 @@ class AppStateManager @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
val repositoriesPathsSaved = appPreferences.latestOpenedRepositoriesPath
|
||||
val repositoriesPathsSaved = appSettings.latestOpenedRepositoriesPath
|
||||
if (repositoriesPathsSaved.isNotEmpty()) {
|
||||
val repositories = Json.decodeFromString<List<String>>(repositoriesPathsSaved)
|
||||
_latestOpenedRepositoriesPaths.addAll(repositories)
|
||||
|
@ -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
|
||||
}
|
@ -117,4 +117,13 @@ val DiffEntry.iconColor: Color
|
||||
DiffEntry.ChangeType.RENAME -> MaterialTheme.colors.modifyFile
|
||||
else -> throw NotImplementedError("Unexpected ChangeType")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
@ -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
|
||||
|
||||
}
|
||||
|
@ -130,4 +130,18 @@ fun prepareTreeParser(repository: Repository, commit: RevCommit): AbstractTreeIt
|
||||
|
||||
return treeParser
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,8 @@ package app.git.diff
|
||||
|
||||
data class Hunk(val header: String, val lines: List<Line>)
|
||||
|
||||
data class SplitHunk(val hunk: Hunk, val lines: List<Pair<Line?, Line?>>)
|
||||
|
||||
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
|
||||
|
@ -221,6 +221,10 @@ sealed class DiffResult(
|
||||
diffEntry: DiffEntry,
|
||||
val hunks: List<Hunk>
|
||||
) : DiffResult(diffEntry)
|
||||
class TextSplit(
|
||||
diffEntry: DiffEntry,
|
||||
val hunks: List<SplitHunk>
|
||||
) : DiffResult(diffEntry)
|
||||
|
||||
class NonText(
|
||||
diffEntry: DiffEntry,
|
||||
|
@ -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<Float> = _scaleUiFlow
|
||||
|
||||
private val _textDiffTypeFlow = MutableStateFlow(textDiffType)
|
||||
val textDiffTypeFlow: StateFlow<TextDiffType> = _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)
|
@ -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),
|
||||
)
|
@ -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(
|
||||
|
@ -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"),
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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<List<Ref>>(listOf())
|
||||
val branches: StateFlow<List<Ref>>
|
||||
@ -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(
|
||||
|
@ -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>(ViewDiffResult.Loading)
|
||||
private val _diffResult = MutableStateFlow<ViewDiffResult>(ViewDiffResult.Loading(""))
|
||||
val diffResult: StateFlow<ViewDiffResult?> = _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<SplitHunk> {
|
||||
val unifiedHunksList = diffFormat.hunks
|
||||
val hunksList = mutableListOf<SplitHunk>()
|
||||
|
||||
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<Line?>(maxLinesCountOfBothParts)
|
||||
val newLinesArray = arrayOfNulls<Line?>(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<Pair<Line?, Line?>>()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
@ -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>(HistoryState.Loading(""))
|
||||
val historyState: StateFlow<HistoryState> = _historyState
|
||||
|
@ -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>(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(
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue
Block a user